{ "info": { "author": "Gary Poster", "author_email": "gary@zope.com", "bugtrack_url": null, "classifiers": [ "License :: OSI Approved :: Zope Public License", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython" ], "description": "~~~~~~~~~~~~~~~\nzc.relationship\n~~~~~~~~~~~~~~~\n\nThe zc.relationship package currently contains two main types of\ncomponents: a relationship index, and some relationship containers.\nBoth are designed to be used within the ZODB, although the index is\nflexible enough to be used in other contexts. They share the model that\nrelationships are full-fledged objects that are indexed for optimized\nsearches. They also share the ability to perform optimized intransitive\nand transitive relationship searches, and to support arbitrary filter\nsearches on relationship tokens.\n\nThe index is a very generic component that can be used to optimize searches\nfor N-ary relationships, can be used standalone or within a catalog, can be\nused with pluggable token generation schemes, and generally tries to provide\na relatively policy-free tool. It is expected to be used primarily as an\nengine for more specialized and constrained tools and APIs.\n\nThe relationship containers use the index to manage two-way\nrelationships, using a derived mapping interface. It is a reasonable\nexample of the index in standalone use.\n\nAnother example, using the container model but supporting five-way\nrelationships (\"sources\", \"targets\", \"relation\", \"getContext\", \"state\"), can\nbe found in plone.relations. Its README is a good read.\n\nhttp://dev.plone.org/plone/browser/plone.relations/trunk/plone/relations\n\nThis current document describes the relationship index. See\ncontainer.rst for documentation of the relationship container.\n\n**PLEASE NOTE: the index in zc.relationship, described below, now exists for\nbackwards compatibility. zc.relation.catalog now contains the most recent,\nbackward-incompatible version of the index code.**\n\n=====\nIndex\n=====\n\n.. contents::\n\nOverview\n========\n\nThe index takes a very precise view of the world: instantiation requires\nmultiple arguments specifying the configuration; and using the index\nrequires that you acknowledge that the relationships and their\nassociated indexed values are usually tokenized within the index. This\nprecision trades some ease-of-use for the possibility of flexibility,\npower, and efficiency. That said, the index's API is intended to be\nconsistent, and to largely adhere to \"there's only one way to do it\"\n[#apply]_.\n\nSimplest Example\n----------------\n\nBefore diving into the N-way flexibility and the other more complex\nbits, then, let's have a quick basic demonstration: a two way\nrelationship from one value to another. This will give you a taste of\nthe relationship index, and let you use it reasonably well for\nlight-to-medium usage. If you are going to use more of its features or\nuse it more in a potentially high-volume capacity, please consider\ntrying to understand the entire document.\n\nLet's say that we are modeling a relationship of people to their\nsupervisors: an employee may have a single supervisor.\n\nLet's say further that employee names are unique and can be used to\nrepresent employees. We can use names as our \"tokens\". Tokens are\nsimilar to the primary key in a relational database, or in intid or\nkeyreference in Zope 3--some way to uniquely identify an object, which\nsorts reliably and can be resolved to the object given the right context.\n\n >>> from __future__ import print_function\n >>> from functools import total_ordering\n >>> employees = {} # we'll use this to resolve the \"name\" tokens\n >>> @total_ordering\n ... class Employee(object):\n ... def __init__(self, name, supervisor=None):\n ... if name in employees:\n ... raise ValueError('employee with same name already exists')\n ... self.name = name # expect this to be readonly\n ... self.supervisor = supervisor\n ... employees[name] = self\n ... def __repr__(self): # to make the tests prettier...\n ... return '<' + self.name + '>'\n ... def __eq__(self, other):\n ... return self is other\n ... def __lt__(self, other): # to make the tests prettier...\n ... # pukes if other doesn't have name\n ... return self.name < other.name\n ...\n\nSo, we need to define how to turn employees into their tokens. That's\ntrivial. (We explain the arguments to this function in detail below,\nbut for now we're aiming for \"breezy overview\".)\n\n >>> def dumpEmployees(emp, index, cache):\n ... return emp.name\n ...\n\nWe also need a way to turn tokens into employees. We use our dict for that.\n\n >>> def loadEmployees(token, index, cache):\n ... return employees[token]\n ...\n\nWe also need a way to tell the index to find the supervisor for indexing:\n\n >>> def supervisor(emp, index):\n ... return emp.supervisor # None or another employee\n ...\n\nNow we have enough to get started with an index. The first argument to\nIndex is the attributes to index: we pass the `supervisor` function\n(which is also used in this case to define the index's name, since we do\nnot pass one explicitly), the dump and load functions, and a BTree\nmodule that specifies sets that can hold our tokens (OO or OL should\nalso work). As keyword arguments, we tell the index how to dump and\nload our relationship tokens--the same functions in this case--and what\na reasonable BTree module is for sets (again, we choose OI, but OO or OL\nshould work).\n\n >>> from zc.relationship import index\n >>> import BTrees\n >>> ix = index.Index(\n ... ({'callable': supervisor, 'dump': dumpEmployees,\n ... 'load': loadEmployees, 'btree': BTrees.family32.OI},),\n ... dumpRel=dumpEmployees, loadRel=loadEmployees,\n ... relFamily=BTrees.family32.OI)\n\nNow let's create a few employees.\n\n >>> a = Employee('Alice')\n >>> b = Employee('Betty', a)\n >>> c = Employee('Chuck', a)\n >>> d = Employee('Duane', b)\n >>> e = Employee('Edgar', b)\n >>> f = Employee('Frank', c)\n >>> g = Employee('Grant', c)\n >>> h = Employee('Howie', d)\n\nIn a diagram style with which you will become familiar if you make it to\nthe end of this document, let's show the hierarchy.\n\n::\n\n Alice\n __/ \\__\n Betty Chuck\n / \\ / \\\n Duane Edgar Frank Grant\n |\n Howie\n\nSo who works for Alice? To ask the index, we need to tell it about them.\n\n >>> for emp in (a,b,c,d,e,f,g,h):\n ... ix.index(emp)\n ...\n\nNow we can ask. We always need to ask with tokens. The index provides\na method to try and make this more convenient: `tokenizeQuery`\n[#resolveQuery]_.\n\n.. [#resolveQuery] You can also resolve queries.\n\n >>> ix.resolveQuery({None: 'Alice'})\n {None: }\n >>> ix.resolveQuery({'supervisor': 'Alice'})\n {'supervisor': }\n\nThe spelling of the query is described in more detail\nlater, but the idea is simply that keys in a dictionary specify\nattribute names, and the values specify the constraints.\n\n >>> t = ix.tokenizeQuery\n >>> sorted(ix.findRelationshipTokens(t({'supervisor': a})))\n ['Betty', 'Chuck']\n >>> sorted(ix.findRelationships(t({'supervisor': a})))\n [, ]\n\nHow do we find what the employee's supervisor is? Well, in this case,\nlook at the attribute! If you can use an attribute that will usually be\na win in the ZODB. If you want to look at the data in the index,\nthough, that's easy enough. Who is Howie's supervisor? The None key in\nthe query indicates that we are matching against the relationship token\nitself [#None_details]_.\n\n.. [#None_details] You can search for relations that haven't been indexed.\n\n >>> list(ix.findRelationshipTokens({None: 'Ygritte'}))\n []\n\n You can also combine searches with None, just for completeness.\n\n >>> list(ix.findRelationshipTokens({None: 'Alice', 'supervisor': None}))\n ['Alice']\n >>> list(ix.findRelationshipTokens({None: 'Alice', 'supervisor': 'Betty'}))\n []\n >>> list(ix.findRelationshipTokens({None: 'Betty', 'supervisor': 'Alice'}))\n ['Betty']\n\n >>> h.supervisor\n \n >>> list(ix.findValueTokens('supervisor', t({None: h})))\n ['Duane']\n >>> list(ix.findValues('supervisor', t({None: h})))\n []\n\nWhat about transitive searching? Well, you need to tell the index how to\nwalk the tree. In simple cases like this, the index's\nTransposingTransitiveQueriesFactory will do the trick. We just want to tell\nthe factory to transpose the two keys, None and 'supervisor'. We can then use\nit in queries for transitive searches.\n\n >>> factory = index.TransposingTransitiveQueriesFactory(None, 'supervisor')\n\nWho are all of Howie's supervisors transitively (this looks up in the\ndiagram)?\n\n >>> list(ix.findValueTokens('supervisor', t({None: h}),\n ... transitiveQueriesFactory=factory))\n ['Duane', 'Betty', 'Alice']\n >>> list(ix.findValues('supervisor', t({None: h}),\n ... transitiveQueriesFactory=factory))\n [, , ]\n\nWho are all of the people Betty supervises transitively, breadth first (this\nlooks down in the diagram)?\n\n >>> people = list(ix.findRelationshipTokens(\n ... t({'supervisor': b}), transitiveQueriesFactory=factory))\n >>> sorted(people[:2])\n ['Duane', 'Edgar']\n >>> people[2]\n 'Howie'\n >>> people = list(ix.findRelationships(\n ... t({'supervisor': b}), transitiveQueriesFactory=factory))\n >>> sorted(people[:2])\n [, ]\n >>> people[2]\n \n\nThis transitive search is really the only transitive factory you would want\nhere, so it probably is safe to wire it in as a default. While most\nattributes on the index must be set at instantiation, this happens to be one\nwe can set after the fact.\n\n >>> ix.defaultTransitiveQueriesFactory = factory\n\nNow all searches are transitive.\n\n >>> list(ix.findValueTokens('supervisor', t({None: h})))\n ['Duane', 'Betty', 'Alice']\n >>> list(ix.findValues('supervisor', t({None: h})))\n [, , ]\n >>> people = list(ix.findRelationshipTokens(t({'supervisor': b})))\n >>> sorted(people[:2])\n ['Duane', 'Edgar']\n >>> people[2]\n 'Howie'\n >>> people = list(ix.findRelationships(t({'supervisor': b})))\n >>> sorted(people[:2])\n [, ]\n >>> people[2]\n \n\nWe can force a non-transitive search, or a specific search depth, with\nmaxDepth [#needs_a_transitive_queries_factory]_.\n\n.. [#needs_a_transitive_queries_factory] A search with a maxDepth > 1 but\n no transitiveQueriesFactory raises an error.\n\n >>> ix.defaultTransitiveQueriesFactory = None\n >>> ix.findRelationshipTokens({'supervisor': 'Duane'}, maxDepth=3)\n Traceback (most recent call last):\n ...\n ValueError: if maxDepth not in (None, 1), queryFactory must be available\n\n >>> ix.defaultTransitiveQueriesFactory = factory\n\n >>> list(ix.findValueTokens('supervisor', t({None: h}), maxDepth=1))\n ['Duane']\n >>> list(ix.findValues('supervisor', t({None: h}), maxDepth=1))\n []\n >>> sorted(ix.findRelationshipTokens(t({'supervisor': b}), maxDepth=1))\n ['Duane', 'Edgar']\n >>> sorted(ix.findRelationships(t({'supervisor': b}), maxDepth=1))\n [, ]\n\nTransitive searches can handle recursive loops and have other features as\ndiscussed in the larger example and the interface.\n\nOur last two introductory examples show off three other methods: `isLinked`\n`findRelationshipTokenChains` and `findRelationshipChains`.\n\nisLinked lets you answer whether two queries are linked. Is Alice a\nsupervisor of Howie? What about Chuck? (Note that, if your\nrelationships describe a hierarchy, searching up a hierarchy is usually\nmore efficient, so the second pair of questions is generally preferable\nto the first in that case.)\n\n >>> ix.isLinked(t({'supervisor': a}), targetQuery=t({None: h}))\n True\n >>> ix.isLinked(t({'supervisor': c}), targetQuery=t({None: h}))\n False\n >>> ix.isLinked(t({None: h}), targetQuery=t({'supervisor': a}))\n True\n >>> ix.isLinked(t({None: h}), targetQuery=t({'supervisor': c}))\n False\n\n`findRelationshipTokenChains` and `findRelationshipChains` help you discover\n*how* things are transitively related. A \"chain\" is a transitive path of\nrelationships. For instance, what's the chain of command between Alice and\nHowie?\n\n >>> list(ix.findRelationshipTokenChains(\n ... t({'supervisor': a}), targetQuery=t({None: h})))\n [('Betty', 'Duane', 'Howie')]\n >>> list(ix.findRelationshipChains(\n ... t({'supervisor': a}), targetQuery=t({None: h})))\n [(, , )]\n\nThis gives you a quick overview of the basic index features. This should be\nenough to get you going. Now we'll dig in some more, if you want to know the\ndetails.\n\nStarting the N-Way Examples\n===========================\n\nTo exercise the index further, we'll come up with a somewhat complex\nrelationship to index. Let's say we are modeling a generic set-up like\nSUBJECT RELATIONSHIPTYPE OBJECT in CONTEXT. This could let you let\nusers define relationship types, then index them on the fly. The\ncontext can be something like a project, so we could say\n\n\"Fred\" \"has the role of\" \"Project Manager\" on the \"zope.org redesign project\".\n\nMapped to the parts of the relationship object, that's\n\n[\"Fred\" (SUBJECT)] [\"has the role of\" (RELATIONSHIPTYPE)]\n[\"Project Manager\" (OBJECT)] on the [\"zope.org redesign project\" (CONTEXT)].\n\nWithout the context, you can still do interesting things like\n\n[\"Ygritte\" (SUBJECT)] [\"manages\" (RELATIONSHIPTYPE)] [\"Uther\" (OBJECT)]\n\nIn our new example, we'll leverage the fact that the index can accept\ninterface attributes to index. So let's define a basic interface\nwithout the context, and then an extended interface with the context.\n\n >>> from zope import interface\n >>> class IRelationship(interface.Interface):\n ... subjects = interface.Attribute(\n ... 'The sources of the relationship; the subject of the sentence')\n ... relationshiptype = interface.Attribute(\n ... '''unicode: the single relationship type of this relationship;\n ... usually contains the verb of the sentence.''')\n ... objects = interface.Attribute(\n ... '''the targets of the relationship; usually a direct or\n ... indirect object in the sentence''')\n ...\n >>> class IContextAwareRelationship(IRelationship):\n ... def getContext():\n ... '''return a context for the relationship'''\n ...\n\nNow we'll create an index. To do that, we must minimally pass in an\niterable describing the indexed values. Each item in the iterable must\neither be an interface element (a zope.interface.Attribute or\nzope.interface.Method associated with an interface, typically obtained\nusing a spelling like `IRelationship['subjects']`) or a dict. Each dict\nmust have either the 'element' key, which is the interface element to be\nindexed; or the 'callable' key, which is the callable shown in the\nsimpler, introductory example above [#there_can_be_only_one]_.\n\n.. [#there_can_be_only_one] instantiating an index with a dictionary containing\n both the 'element' and the 'callable' key is an error:\n\n >>> def subjects(obj, index, cache):\n ... return obj.subjects\n ...\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'],\n ... 'callable': subjects, 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n Traceback (most recent call last):\n ...\n ValueError: cannot provide both callable and element\n\n While we're at it, as you might expect, you must provide one of them.\n\n >>> ix = index.Index(\n ... ({'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n Traceback (most recent call last):\n ...\n ValueError: must provide element or callable\n\nIt then\ncan contain other keys to override the default indexing behavior for the\nelement.\n\nThe element's or callable's __name__ will be used to refer to this\nelement in queries, unless the dict has a 'name' key, which must be a\nnon-empty string [#name_errors]_.\n\n.. [#name_errors] It's possible to pass a callable without a name, in which\n case you must explicitly specify a name.\n\n >>> @total_ordering\n ... class AttrGetter(object):\n ... def __init__(self, attr):\n ... self.attr = attr\n ... def __eq__(self, other):\n ... return self is other\n ... def __lt__(self, other):\n ... return self.attr < getattr(other, 'attr', other)\n ... def __call__(self, obj, index, cache):\n ... return getattr(obj, self.attr, None)\n ...\n >>> subjects = AttrGetter('subjects')\n >>> ix = index.Index(\n ... ({'callable': subjects, 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n Traceback (most recent call last):\n ...\n ValueError: no name specified\n >>> ix = index.Index(\n ... ({'callable': subjects, 'multiple': True, 'name': subjects},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n\n It's also an error to specify the same name or element twice,\n however you do it.\n\n >>> ix = index.Index(\n ... ({'callable': subjects, 'multiple': True, 'name': 'objects'},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ('name already used', 'objects')\n\n >>> ix = index.Index(\n ... ({'callable': subjects, 'multiple': True, 'name': 'subjects'},\n ... IRelationship['relationshiptype'],\n ... {'callable': subjects, 'multiple': True, 'name': 'objects'},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ValueError: ('element already indexed',\n )\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['objects'], 'multiple': True,\n ... 'name': 'subjects'},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ValueError: ('element already indexed',\n )\n\nThe element is assumed to be a single value, unless the dict has a 'multiple'\nkey with a value equivalent True. In our example, \"subjects\" and \"objects\" are\npotentially multiple values, while \"relationshiptype\" and \"getContext\" are\nsingle values.\n\nBy default, the values for the element will be tokenized and resolved using an\nintid utility, and stored in a BTrees.IFBTree. This is a good choice if you\nwant to make object tokens easily mergable with typical Zope 3 catalog\nresults. If you need different behavior for any element, you can specify\nthree keys per dict:\n\n- 'dump', the tokenizer, a callable taking (obj, index, cache) and returning a\n token;\n\n- 'load' the token resolver, a callable taking (token, index, cache) to return\n the object which the token represents; and\n\n- 'btree', the btree module to use to store and process the tokens, such as\n BTrees.OOBTree.\n\nIf you provide a custom 'dump' you will almost certainly need to provide a\ncustom 'load'; and if your tokens are not integers then you will need to\nspecify a different 'btree' (either BTrees.OOBTree or BTrees.OIBTree, as of\nthis writing).\n\nThe tokenizing function ('dump') *must* return homogenous, immutable tokens:\nthat is, any given tokenizer should only return tokens that sort\nunambiguously, across Python versions, which usually mean that they are all of\nthe same type. For instance, a tokenizer should only return ints, or only\nreturn strings, or only tuples of strings, and so on. Different tokenizers\nused for different elements in the same index may return different types. They\nalso may return the same value as the other tokenizers to mean different\nobjects: the stores are separate.\n\nNote that both dump and load may also be explicitly None in the dictionary:\nthis will mean that the values are already appropriate to be used as tokens.\nIt enables an optimization described in the\n`Optimizing relationship index use`_ section [#neither_or_both]_.\n\n.. [#neither_or_both] It is not allowed to provide only one or the other of\n 'load' and 'dump'.\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True,\n ... 'name': 'subjects','dump': None},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: either both of 'dump' and 'load' must be None, or neither\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['objects'], 'multiple': True,\n ... 'name': 'subjects','load': None},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: either both of 'dump' and 'load' must be None, or neither\n\n\nIn addition to the one required argument to the class, the signature contains\nfour optional arguments. The 'defaultTransitiveQueriesFactory' is the next,\nand allows you to specify a callable as described in\ninterfaces.ITransitiveQueriesFactory. Without it transitive searches will\nrequire an explicit factory every time, which can be tedious. The index\npackage provides a simple implementation that supports transitive searches\nfollowing two indexed elements (TransposingTransitiveQueriesFactory) and this\ndocument describes more complex possible transitive behaviors that can be\nmodeled. For our example, \"subjects\" and \"objects\" are the default transitive\nfields, so if Ygritte (SUBJECT) manages Uther (OBJECT), and Uther (SUBJECT)\nmanages Emily (OBJECT), a search for all those transitively managed by Ygritte\nwill transpose Uther from OBJECT to SUBJECT and find that Uther manages Emily.\nSimilarly, to find all transitive managers of Emily, Uther will change place\nfrom SUBJECT to OBJECT in the search [#TransposingTransitiveQueriesFactory]_.\n\n.. [#TransposingTransitiveQueriesFactory] The factory lets you specify two\n names, which are transposed for transitive walks. This is usually what\n you want for a hierarchy and similar variations: as the text describes\n later, more complicated traversal might be desired in more complicated\n relationships, as found in genealogy.\n\n It supports both transposing values and relationship tokens, as seen in\n the text.\n\n In this footnote, we'll explore the factory in the small, with index\n stubs.\n\n >>> factory = index.TransposingTransitiveQueriesFactory(\n ... 'subjects', 'objects')\n >>> class StubIndex(object):\n ... def findValueTokenSet(self, rel, name):\n ... return {\n ... ('foo', 'objects'): ('bar',),\n ... ('bar', 'subjects'): ('foo',)}[(rel, name)]\n ...\n >>> ix = StubIndex()\n >>> list(factory(['foo'], {'subjects': 'foo'}, ix, {}))\n [{'subjects': 'bar'}]\n >>> list(factory(['bar'], {'objects': 'bar'}, ix, {}))\n [{'objects': 'foo'}]\n\n If you specify both fields then it won't transpose.\n\n >>> list(factory(['foo'], {'objects': 'bar', 'subjects': 'foo'}, ix, {}))\n []\n\n If you specify additional fields then it keeps them statically.\n\n >>> list(factory(['foo'], {'subjects': 'foo', 'getContext': 'shazam'},\n ... ix, {})) == [{'subjects': 'bar', 'getContext': 'shazam'}]\n True\n\nThe next three arguments, 'dumpRel', 'loadRel' and 'relFamily', have\nto do with the relationship tokens. The default values assume that you will\nbe using intid tokens for the relationships, and so 'dumpRel' and\n'loadRel' tokenize and resolve, respectively, using the intid utility; and\n'relFamily' defaults to BTrees.IFBTree.\n\nIf relationship tokens (from 'findRelationshipChains' or 'apply' or\n'findRelationshipTokenSet', or in a filter to most of the search methods) are\nto be merged with other catalog results, relationship tokens should be based\non intids, as in the default. For instance, if some relationships are only\navailable to some users on the basis of security, and you keep an index of\nthis, then you will want to use a filter based on the relationship tokens\nviewable by the current user as kept by the catalog index.\n\nIf you are unable or unwilling to use intid relationship tokens, tokens must\nstill be homogenous and immutable as described above for indexed values tokens.\n\nThe last argument is 'family', which effectively defaults to BTrees.family32.\nIf you don't expicitly specify BTree modules for your value and relationship\nsets, this value will determine whether you use the 32 bit or the 64 bit\nIFBTrees [#family64]_.\n\n.. [#family64] Here's an example of specifying the family64. This is a \"white\n box\" demonstration that looks at some of the internals.\n\n >>> ix = index.Index( # 32 bit default\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n >>> ix._relTools['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['subjects']['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['objects']['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['getContext']['BTree'] is BTrees.family32.IF.BTree\n True\n\n >>> ix = index.Index( # explicit 32 bit\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'),\n ... family=BTrees.family32)\n >>> ix._relTools['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['subjects']['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['objects']['BTree'] is BTrees.family32.IF.BTree\n True\n >>> ix._attrs['getContext']['BTree'] is BTrees.family32.IF.BTree\n True\n\n >>> ix = index.Index( # explicit 64 bit\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'),\n ... family=BTrees.family64)\n >>> ix._relTools['BTree'] is BTrees.family64.IF.BTree\n True\n >>> ix._attrs['subjects']['BTree'] is BTrees.family64.IF.BTree\n True\n >>> ix._attrs['objects']['BTree'] is BTrees.family64.IF.BTree\n True\n >>> ix._attrs['getContext']['BTree'] is BTrees.family64.IF.BTree\n True\n\nIf we had an IIntId utility registered and wanted to use the defaults, then\ninstantiation of an index for our relationship would look like this:\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n\nThat's the simple case. With relatively little fuss, we have an IIndex, and a\ndefaultTransitiveQueriesFactory, implementing ITransitiveQueriesFactory, that\nswitches subjects and objects as described above.\n\n >>> from zc.relationship import interfaces\n >>> from zope.interface.verify import verifyObject\n >>> verifyObject(interfaces.IIndex, ix)\n True\n >>> verifyObject(\n ... interfaces.ITransitiveQueriesFactory,\n ... ix.defaultTransitiveQueriesFactory)\n True\n\nFor the purposes of a more complex example, though, we are going to exercise\nmore of the index's options--we'll use at least one of 'name', 'dump', 'load',\nand 'btree'.\n\n- 'subjects' and 'objects' will use a custom integer-based token generator.\n They will share tokens, which will let us use the default\n TransposingTransitiveQueriesFactory. We can keep using the IFBTree sets,\n because the tokens are still integers.\n\n- 'relationshiptype' will use a name 'reltype' and will just use the unicode\n value as the token, without translation but with a registration check.\n\n- 'getContext' will use a name 'context' but will continue to use the intid\n utility and use the names from their interface. We will see later that\n making transitive walks between different token sources must be handled with\n care.\n\nWe will also use the intid utility to resolve relationship tokens. See the\nrelationship container (and container.rst) for examples of changing the\nrelationship type, especially in keyref.py.\n\nHere are the methods we'll use for the 'subjects' and 'objects' tokens,\nfollowed by the methods we'll use for the 'relationshiptypes' tokens.\n\n >>> lookup = {}\n >>> counter = [0]\n >>> prefix = '_z_token__'\n >>> def dump(obj, index, cache):\n ... assert (interfaces.IIndex.providedBy(index) and\n ... isinstance(cache, dict)), (\n ... 'did not receive correct arguments')\n ... token = getattr(obj, prefix, None)\n ... if token is None:\n ... token = counter[0]\n ... counter[0] += 1\n ... if counter[0] >= 2147483647:\n ... raise RuntimeError(\"Whoa! That's a lot of ids!\")\n ... assert token not in lookup\n ... setattr(obj, prefix, token)\n ... lookup[token] = obj\n ... return token\n ...\n >>> def load(token, index, cache):\n ... assert (interfaces.IIndex.providedBy(index) and\n ... isinstance(cache, dict)), (\n ... 'did not receive correct arguments')\n ... return lookup[token]\n ...\n >>> relTypes = []\n >>> def relTypeDump(obj, index, cache):\n ... assert obj in relTypes, 'unknown relationshiptype'\n ... return obj\n ...\n >>> def relTypeLoad(token, index, cache):\n ... assert token in relTypes, 'unknown relationshiptype'\n ... return token\n ...\n\nNote that these implementations are completely silly if we actually cared about\nZODB-based persistence: to even make it half-acceptable we should make the\ncounter, lookup, and and relTypes persistently stored somewhere using a\nreasonable persistent data structure. This is just a demonstration example.\n\nNow we can make an index.\n\nAs in our initial example, we are going to use the simple transitive query\nfactory defined in the index module for our default transitive behavior: when\nyou want to do transitive searches, transpose 'subjects' with 'objects' and\nkeep everything else; and if both subjects and objects are provided, don't do\nany transitive search.\n\n >>> from BTrees import OIBTree # could also be OOBTree\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True,\n ... 'dump': dump, 'load': load},\n ... {'element': IRelationship['relationshiptype'],\n ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree,\n ... 'name': 'reltype'},\n ... {'element': IRelationship['objects'], 'multiple': True,\n ... 'dump': dump, 'load': load},\n ... {'element': IContextAwareRelationship['getContext'],\n ... 'name': 'context'}),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n\nWe'll want to put the index somewhere in the system so it can find the intid\nutility. We'll add it as a utility just as part of the example. As long as\nthe index has a valid __parent__ that is itself connected transitively to a\nsite manager with the desired intid utility, everything should work fine, so\nno need to install it as utility. This is just an example.\n\n >>> from zope import interface\n >>> sm = app.getSiteManager()\n >>> sm['rel_index'] = ix\n >>> import zope.component.interfaces\n >>> registry = zope.component.interfaces.IComponentRegistry(sm)\n >>> registry.registerUtility(ix, interfaces.IIndex)\n >>> import transaction\n >>> transaction.commit()\n\nNow we'll create some representative objects that we can relate, and create\nand index our first example relationship.\n\nIn the example, note that the context will only be available as an adapter to\nISpecialRelationship objects: the index tries to adapt objects to the\nappropriate interface, and considers the value to be empty if it cannot adapt.\n\n >>> import persistent\n >>> from zope.app.container.contained import Contained\n >>> class Base(persistent.Persistent, Contained):\n ... def __init__(self, name):\n ... self.name = name\n ... def __repr__(self):\n ... return '<%s %r>' % (self.__class__.__name__, self.name)\n ...\n >>> class Person(Base): pass\n ...\n >>> class Role(Base): pass\n ...\n >>> class Project(Base): pass\n ...\n >>> class Company(Base): pass\n ...\n >>> @interface.implementer(IRelationship)\n ... class Relationship(persistent.Persistent, Contained):\n ... def __init__(self, subjects, relationshiptype, objects):\n ... self.subjects = subjects\n ... assert relationshiptype in relTypes\n ... self.relationshiptype = relationshiptype\n ... self.objects = objects\n ... def __repr__(self):\n ... return '<%r %s %r>' % (\n ... self.subjects, self.relationshiptype, self.objects)\n ...\n >>> class ISpecialRelationship(interface.Interface):\n ... pass\n ...\n >>> from zope import component\n >>> @component.adapter(ISpecialRelationship)\n ... @interface.implementer(IContextAwareRelationship)\n ... class ContextRelationshipAdapter(object):\n ... def __init__(self, adapted):\n ... self.adapted = adapted\n ... def getContext(self):\n ... return getattr(self.adapted, '_z_context__', None)\n ... def setContext(self, value):\n ... self.adapted._z_context__ = value\n ... def __getattr__(self, name):\n ... return getattr(self.adapted, name)\n ...\n >>> component.provideAdapter(ContextRelationshipAdapter)\n >>> @interface.implementer(ISpecialRelationship)\n ... class SpecialRelationship(Relationship):\n ... pass\n ...\n >>> people = {}\n >>> for p in ['Abe', 'Bran', 'Cathy', 'David', 'Emily', 'Fred', 'Gary',\n ... 'Heather', 'Ingrid', 'Jim', 'Karyn', 'Lee', 'Mary',\n ... 'Nancy', 'Olaf', 'Perry', 'Quince', 'Rob', 'Sam', 'Terry',\n ... 'Uther', 'Van', 'Warren', 'Xen', 'Ygritte', 'Zane']:\n ... app[p] = people[p] = Person(p)\n ...\n >>> relTypes.extend(\n ... ['has the role of', 'manages', 'taught', 'commissioned'])\n >>> roles = {}\n >>> for r in ['Project Manager', 'Software Engineer', 'Designer',\n ... 'Systems Administrator', 'Team Leader', 'Mascot']:\n ... app[r] = roles[r] = Role(r)\n ...\n >>> projects = {}\n >>> for p in ['zope.org redesign', 'Zope 3 manual',\n ... 'improved test coverage', 'Vault design and implementation']:\n ... app[p] = projects[p] = Project(p)\n ...\n >>> companies = {}\n >>> for c in ['Ynod Corporation', 'HAL, Inc.', 'Zookd']:\n ... app[c] = companies[c] = Company(c)\n ...\n\n >>> app['fredisprojectmanager'] = rel = SpecialRelationship(\n ... (people['Fred'],), 'has the role of', (roles['Project Manager'],))\n >>> IContextAwareRelationship(rel).setContext(\n ... projects['zope.org redesign'])\n >>> ix.index(rel)\n >>> transaction.commit()\n\nToken conversion\n================\n\nBefore we examine the searching features, we should quickly discuss the\ntokenizing API on the index. All search queries must use value tokens, and\nsearch results can sometimes be value or relationship tokens. Therefore\nconverting between tokens and real values can be important. The index\nprovides a number of conversion methods for this purpose.\n\nArguably the most important is `tokenizeQuery`: it takes a query, in which\neach key and value are the name of an indexed value and an actual value,\nrespectively; and returns a query in which the actual values have been\nconverted to tokens. For instance, consider the following example. It's a\nbit hard to show the conversion reliably (we can't know what the intid tokens\nwill be, for instance) so we just show that the result's values are tokenized\nversions of the inputs.\n\n >>> res = ix.tokenizeQuery(\n ... {'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})\n >>> res['objects'] == dump(roles['Project Manager'], ix, {})\n True\n >>> from zope.app.intid.interfaces import IIntIds\n >>> intids = component.getUtility(IIntIds, context=ix)\n >>> res['context'] == intids.getId(projects['zope.org redesign'])\n True\n\nTokenized queries can be resolved to values again using resolveQuery.\n\n >>> sorted(ix.resolveQuery(res).items()) # doctest: +NORMALIZE_WHITESPACE\n [('context', ),\n ('objects', )]\n\nOther useful conversions are `tokenizeValues`, which returns an iterable of\ntokens for the values of the given index name;\n\n >>> examples = (people['Abe'], people['Bran'], people['Cathy'])\n >>> res = list(ix.tokenizeValues(examples, 'subjects'))\n >>> res == [dump(o, ix, {}) for o in examples]\n True\n\n`resolveValueTokens`, which returns an iterable of values for the tokens of\nthe given index name;\n\n >>> list(ix.resolveValueTokens(res, 'subjects'))\n [, , ]\n\n`tokenizeRelationship`, which returns a token for the given relationship;\n\n >>> res = ix.tokenizeRelationship(rel)\n >>> res == intids.getId(rel)\n True\n\n`resolveRelationshipToken`, which returns a relationship for the given token;\n\n >>> ix.resolveRelationshipToken(res) is rel\n True\n\n`tokenizeRelationships`, which returns an iterable of tokens for the relations\ngiven; and\n\n >>> app['another_rel'] = another_rel = Relationship(\n ... (companies['Ynod Corporation'],), 'commissioned',\n ... (projects['Vault design and implementation'],))\n >>> res = list(ix.tokenizeRelationships((another_rel, rel)))\n >>> res == [intids.getId(r) for r in (another_rel, rel)]\n True\n\n`resolveRelationshipTokens`, which returns an iterable of relations for the\ntokens given.\n\n >>> list(ix.resolveRelationshipTokens(res)) == [another_rel, rel]\n True\n\nBasic searching\n===============\n\nNow we move to the meat of the interface: searching. The index interface\ndefines several searching methods:\n\n- `findValues` and `findValueTokens` ask \"to what is this related?\";\n\n- `findRelationshipChains` and `findRelationshipTokenChains` ask \"how is this\n related?\", especially for transitive searches;\n\n- `isLinked` asks \"does a relationship like this exist?\";\n\n- `findRelationshipTokenSet` asks \"what are the intransitive relationships\n that match my query?\" and is particularly useful for low-level usage of the\n index data structures;\n\n- `findRelationships` asks the same question, but returns an iterable of\n relationships rather than a set of tokens;\n\n- `findValueTokenSet` asks \"what are the value tokens for this particular\n indexed name and this relationship token?\" and is useful for low-level\n usage of the index data structures such as transitive query factories; and\n\n- the standard zope.index method `apply` essentially exposes the\n `findRelationshipTokenSet` and `findValueTokens` methods via a query object\n spelling.\n\n`findRelationshipChains` and `findRelationshipTokenChains` are paired methods,\ndoing the same work but with and without resolving the resulting tokens; and\n`findValues` and `findValueTokens` are also paired in the same way.\n\nIt is very important to note that all queries must use tokens, not actual\nobjects. As introduced above, the index provides a method to ease that\nrequirement, in the form of a `tokenizeQuery` method that converts a dict with\nobjects to a dict with tokens. You'll see below that we shorten our calls by\nstashing `tokenizeQuery` away in the 'q' name.\n\n >>> q = ix.tokenizeQuery\n\nWe have indexed our first example relationship--\"Fred has the role of project\nmanager in the zope.org redesign\"--so we can search for it. We'll first look\nat `findValues` and `findValueTokens`. Here, we ask 'who has the role of\nproject manager in the zope.org redesign?'. We do it first with findValues\nand then with findValueTokens [#findValue_errors]_.\n\n.. [#findValue_errors] `findValueTokens` and `findValues` raise errors if\n you try to get a value that is not indexed.\n\n >>> list(ix.findValues(\n ... 'folks',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))\n Traceback (most recent call last):\n ...\n ValueError: ('name not indexed', 'folks')\n\n >>> list(ix.findValueTokens(\n ... 'folks',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))\n Traceback (most recent call last):\n ...\n ValueError: ('name not indexed', 'folks')\n\n >>> list(ix.findValues(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))\n []\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']}))]\n []\n\nIf you don't pass a query to these methods, you get all indexed values for the\ngiven name in a BTree (don't modify this! this is an internal data structure--\nwe pass it out directly because you can do efficient things with it with BTree\nset operations). In this case, we've only indexed a single relationship,\nso its subjects are the subjects in this result.\n\n >>> res = ix.findValueTokens('subjects', maxDepth=1)\n >>> res # doctest: +ELLIPSIS\n \n >>> [load(t, ix, {}) for t in res]\n []\n\nIf we want to find all the relationships for which Fred is a subject, we can\nuse `findRelationshipTokenSet`. It, combined with `findValueTokenSet`, is\nuseful for querying the index data structures at a fairly low level, when you\nwant to use the data in a way that the other search methods don't support.\n\n`findRelationshipTokenSet`, given a single dictionary of {indexName: token},\nreturns a set (based on the btree family for relationships in the index) of\nrelationship tokens that match it, intransitively.\n\n >>> res = ix.findRelationshipTokenSet(q({'subjects': people['Fred']}))\n >>> res # doctest: +ELLIPSIS\n \n >>> [intids.getObject(t) for t in res]\n [<(,) has the role of (,)>]\n\nIt is in fact equivalent to `findRelationshipTokens` called without\ntransitivity and without any filtering.\n\n >>> res2 = ix.findRelationshipTokens(\n ... q({'subjects': people['Fred']}), maxDepth=1)\n >>> res2 is res\n True\n\nThe `findRelationshipTokenSet` method always returns a set, even if the\nquery does not have any results.\n\n >>> res = ix.findRelationshipTokenSet(q({'subjects': people['Ygritte']}))\n >>> res # doctest: +ELLIPSIS\n \n >>> list(res)\n []\n\nAn empty query returns all relationships in the index (this is true of other\nsearch methods as well).\n\n >>> res = ix.findRelationshipTokenSet({})\n >>> res # doctest: +ELLIPSIS\n \n >>> len(res) == ix.documentCount()\n True\n >>> for r in ix.resolveRelationshipTokens(res):\n ... if r not in ix:\n ... print('oops')\n ... break\n ... else:\n ... print('correct')\n ...\n correct\n\n`findRelationships` can do the same thing but with resolving the relationships.\n\n >>> list(ix.findRelationships(q({'subjects': people['Fred']})))\n [<(,) has the role of (,)>]\n\nHowever, like `findRelationshipTokens` and unlike\n`findRelationshipTokenSet`, `findRelationships` can be used\ntransitively, as shown in the introductory section of this document.\n\n`findValueTokenSet`, given a relationship token and a value name, returns a\nset (based on the btree family for the value) of value tokens for that\nrelationship.\n\n >>> src = ix.findRelationshipTokenSet(q({'subjects': people['Fred']}))\n\n >>> res = ix.findValueTokenSet(list(src)[0], 'subjects')\n >>> res # doctest: +ELLIPSIS\n \n >>> [load(t, ix, {}) for t in res]\n []\n\nLike `findRelationshipTokenSet` and `findRelationshipTokens`,\n`findValueTokenSet` is equivalent to `findValueTokens` without a\ntransitive search or filtering.\n\n >>> res2 = ix.findValueTokenSet(list(src)[0], 'subjects')\n >>> res2 is res\n True\n\nThe apply method, part of the zope.index.interfaces.IIndexSearch interface,\ncan essentially only duplicate the `findValueTokens` and\n`findRelationshipTokenSet` search calls. The only additional functionality\nis that the results always are IFBTree sets: if the tokens requested are not\nin an IFBTree set (on the basis of the 'btree' key during instantiation, for\ninstance) then the index raises a ValueError. A wrapper dict specifies the\ntype of search with the key, and the value should be the arguments for the\nsearch.\n\nHere, we ask for the current known roles on the zope.org redesign.\n\n >>> res = ix.apply({'values':\n ... {'resultName': 'objects', 'query':\n ... q({'reltype': 'has the role of',\n ... 'context': projects['zope.org redesign']})}})\n >>> res # doctest: +ELLIPSIS\n IFSet([...])\n >>> [load(t, ix, {}) for t in res]\n []\n\nIdeally, this would fail, because the tokens, while integers, are not actually\nmergable with a intid-based catalog results. However, the index only complains\nif it can tell that the returning set is not an IFTreeSet or IFSet.\n\nHere, we ask for the relationships that have the 'has the role of' type.\n\n >>> res = ix.apply({'relationships':\n ... q({'reltype': 'has the role of'})})\n >>> res # doctest: +ELLIPSIS\n \n >>> [intids.getObject(t) for t in res]\n [<(,) has the role of (,)>]\n\nHere, we ask for the known relationships types for the zope.org redesign. It\nwill fail, because the result cannot be expressed as an IFBTree.IFTreeSet.\n\n >>> res = ix.apply({'values':\n ... {'resultName': 'reltype', 'query':\n ... q({'context': projects['zope.org redesign']})}})\n ... # doctest: +NORMALIZE_WHITESPACE\n Traceback (most recent call last):\n ...\n ValueError: cannot fulfill `apply` interface because cannot return an\n (I|L)FBTree-based result\n\nThe same kind of error will be raised if you request relationships and the\nrelationships are not stored in IFBTree or LFBTree structures [#apply_errors]_.\n\n.. [#apply_errors] Only one key may be in the dictionary.\n\n >>> res = ix.apply({'values':\n ... {'resultName': 'objects', 'query':\n ... q({'reltype': 'has the role of',\n ... 'context': projects['zope.org redesign']})},\n ... 'relationships': q({'reltype': 'has the role of'})})\n Traceback (most recent call last):\n ...\n ValueError: one key in the primary query dictionary\n\n The keys must be one of 'values' or 'relationships'.\n\n >>> res = ix.apply({'kumquats':\n ... {'resultName': 'objects', 'query':\n ... q({'reltype': 'has the role of',\n ... 'context': projects['zope.org redesign']})}})\n Traceback (most recent call last):\n ...\n ValueError: ('unknown query type', 'kumquats')\n\n If a relationship uses LFBTrees, searches are fine.\n\n >>> ix2 = index.Index( # explicit 64 bit\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'),\n ... family=BTrees.family64)\n\n >>> list(ix2.apply({'values':\n ... {'resultName': 'objects', 'query':\n ... q({'subjects': people['Gary']})}}))\n []\n\n >>> list(ix2.apply({'relationships':\n ... q({'subjects': people['Gary']})}))\n []\n\n But, as with shown in the main text for values, if you are using another\n BTree module for relationships, you'll get an error.\n\n >>> ix2 = index.Index( # explicit 64 bit\n ... ({'element': IRelationship['subjects'], 'multiple': True},\n ... IRelationship['relationshiptype'],\n ... {'element': IRelationship['objects'], 'multiple': True},\n ... IContextAwareRelationship['getContext']),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'),\n ... relFamily=BTrees.OIBTree)\n\n >>> list(ix2.apply({'relationships':\n ... q({'subjects': people['Gary']})}))\n Traceback (most recent call last):\n ...\n ValueError: cannot fulfill `apply` interface because cannot return an (I|L)FBTree-based result\n\nThe last basic search methods, `isLinked`, `findRelationshipTokenChains`, and\n`findRelationshipChains`, are most useful for transitive searches. We\nhave not yet created any relationships that we can use transitively. They\nstill will work with intransitive searches, so we will demonstrate them here\nas an introduction, then discuss them more below when we introduce transitive\nrelationships.\n\n`findRelationshipChains` and `findRelationshipTokenChains` let you find\ntransitive relationship paths. Right now a single relationship--a single\npoint--can't create much of a line. So first, here's a somewhat useless\nexample:\n\n >>> [[intids.getObject(t) for t in path] for path in\n ... ix.findRelationshipTokenChains(\n ... q({'reltype': 'has the role of'}))]\n ... # doctest: +NORMALIZE_WHITESPACE\n [[<(,) has the role of (,)>]]\n\nThat's useless, because there's no chance of it being a transitive search, and\nso you might as well use findRelationshipTokenSet. This will become more\ninteresting later on.\n\nHere's the same example with findRelationshipChains, which resolves the\nrelationship tokens itself.\n\n >>> list(ix.findRelationshipChains(q({'reltype': 'has the role of'})))\n ... # doctest: +NORMALIZE_WHITESPACE\n [(<(,) has the role of (,)>,)]\n\n`isLinked` returns a boolean if there is at least one path that matches the\nsearch--in fact, the implementation is essentially ::\n\n try:\n iter(ix.findRelationshipTokenChains(...args...)).next()\n except StopIteration:\n return False\n else:\n return True\n\nSo, we can say\n\n >>> ix.isLinked(q({'subjects': people['Fred']}))\n True\n >>> ix.isLinked(q({'subjects': people['Gary']}))\n False\n >>> ix.isLinked(q({'subjects': people['Fred'],\n ... 'reltype': 'manages'}))\n False\n\nThis is reasonably useful as is, to test basic assertions. It also works with\ntransitive searches, as we will see below.\n\n\nAn even simpler example\n-----------------------\n\n(This was added to test that searching for a simple relationship works\neven when the transitive query factory is not set.)\n\nLet's create a very simple relation type, using strings as the source\nand target types:\n\n >>> class IStringRelation(interface.Interface):\n ... name = interface.Attribute(\"The name of the value.\")\n ... value = interface.Attribute(\"The value associated with the name.\")\n\n >>> @interface.implementer(IStringRelation)\n ... class StringRelation(persistent.Persistent, Contained):\n ...\n ... def __init__(self, name, value):\n ... self.name = name\n ... self.value = value\n\n >>> app[u\"string-relation-1\"] = StringRelation(\"name1\", \"value1\")\n >>> app[u\"string-relation-2\"] = StringRelation(\"name2\", \"value2\")\n\n >>> transaction.commit()\n\nWe can now create an index that uses these:\n\n >>> from BTrees import OOBTree\n\n >>> sx = index.Index(\n ... ({\"element\": IStringRelation[\"name\"],\n ... \"load\": None, \"dump\": None, \"btree\": OOBTree},\n ... {\"element\": IStringRelation[\"value\"],\n ... \"load\": None, \"dump\": None, \"btree\": OOBTree},\n ... ))\n\n >>> app[\"sx\"] = sx\n >>> transaction.commit()\n\nAnd we'll add the relations to the index:\n\n >>> app[\"sx\"].index(app[\"string-relation-1\"])\n >>> app[\"sx\"].index(app[\"string-relation-2\"])\n\nGetting a relationship back out should be very simple. Let's look for\nall the values associates with \"name1\":\n\n >>> query = sx.tokenizeQuery({\"name\": \"name1\"})\n >>> list(sx.findValues(\"value\", query))\n ['value1']\n\n\n\nSearching for empty sets\n------------------------\n\nWe've examined the most basic search capabilities. One other feature of the\nindex and search is that one can search for relationships to an empty set, or,\nfor single-value relationships like 'reltype' and 'context' in our\nexamples, None.\n\nLet's add a relationship with a 'manages' relationshiptype, and no context; and\na relationship with a 'commissioned' relationship type, and a company context.\n\nNotice that there are two ways of adding indexes, by the way. We have already\nseen that the index has an 'index' method that takes a relationship. Here we\nuse 'index_doc' which is a method defined in zope.index.interfaces.IInjection\nthat requires the token to already be generated. Since we are using intids\nto tokenize the relationships, we must add them to the ZODB app object to give\nthem the possibility of a connection.\n\n >>> app['abeAndBran'] = rel = Relationship(\n ... (people['Abe'],), 'manages', (people['Bran'],))\n >>> ix.index_doc(intids.register(rel), rel)\n >>> app['abeAndVault'] = rel = SpecialRelationship(\n ... (people['Abe'],), 'commissioned',\n ... (projects['Vault design and implementation'],))\n >>> IContextAwareRelationship(rel).setContext(companies['Zookd'])\n >>> ix.index_doc(intids.register(rel), rel)\n\nNow we can search for Abe's relationship that does not have a context. The\nNone value is always used to match both an empty set and a single `None` value.\nThe index does not support any other \"empty\" values at this time.\n\n >>> sorted(\n ... repr(load(t, ix, {})) for t in ix.findValueTokens(\n ... 'objects',\n ... q({'subjects': people['Abe']})))\n [\"\", \"\"]\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'objects', q({'subjects': people['Abe'], 'context': None}))]\n []\n >>> sorted(\n ... repr(v) for v in ix.findValues(\n ... 'objects',\n ... q({'subjects': people['Abe']})))\n [\"\", \"\"]\n >>> list(ix.findValues(\n ... 'objects', q({'subjects': people['Abe'], 'context': None})))\n []\n\nNote that the index does not currently support searching for relationships that\nhave any value, or one of a set of values. This may be added at a later date;\nthe spelling for such queries are among the more troublesome parts.\n\nWorking with transitive searches\n================================\n\nIt's possible to do transitive searches as well. This can let you find all\ntransitive bosses, or transitive subordinates, in our 'manages' relationship\ntype. Let's set up some example relationships. Using letters to represent our\npeople, we'll create three hierarchies like this::\n\n A JK R\n / \\ / \\\n B C LM NOP S T U\n / \\ | | /| | \\\n D E F Q V W X |\n | | \\--Y\n H G |\n | Z\n I\n\nThis means that, for instance, person \"A\" (\"Abe\") manages \"B\" (\"Bran\") and \"C\"\n(\"Cathy\").\n\nWe already have a relationship from Abe to Bran, so we'll only be adding the\nrest.\n\n >>> relmap = (\n ... ('A', 'C'), ('B', 'D'), ('B', 'E'), ('C', 'F'),\n ... ('F', 'G'), ('D', 'H'), ('H', 'I'), ('JK', 'LM'), ('JK', 'NOP'),\n ... ('LM', 'Q'), ('R', 'STU'), ('S', 'VW'), ('T', 'X'), ('UX', 'Y'),\n ... ('Y', 'Z'))\n >>> letters = dict((name[0], ob) for name, ob in people.items())\n >>> for subs, obs in relmap:\n ... subs = tuple(letters[l] for l in subs)\n ... obs = tuple(letters[l] for l in obs)\n ... app['%sManages%s' % (''.join(o.name for o in subs),\n ... ''.join(o.name for o in obs))] = rel = (\n ... Relationship(subs, 'manages', obs))\n ... ix.index(rel)\n ...\n\nNow we can do both transitive and intransitive searches. Here are a few\nexamples.\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'objects': people['Ingrid'],\n ... 'reltype': 'manages'}))\n ... ]\n [, , , ]\n\nHere's the same thing using findValues.\n\n >>> list(ix.findValues(\n ... 'subjects',\n ... q({'objects': people['Ingrid'],\n ... 'reltype': 'manages'})))\n [, , , ]\n\nNotice that they are in order, walking away from the search start. It also\nis breadth-first--for instance, look at the list of superiors to Zane: Xen and\nUther come before Rob and Terry.\n\n >>> res = list(ix.findValues(\n ... 'subjects',\n ... q({'objects': people['Zane'], 'reltype': 'manages'})))\n >>> res[0]\n \n >>> sorted(repr(p) for p in res[1:3])\n [\"\", \"\"]\n >>> sorted(repr(p) for p in res[3:])\n [\"\", \"\"]\n\nNotice that all the elements of the search are maintained as it is walked--only\nthe transposed values are changed, and the rest remain statically. For\ninstance, notice the difference between these two results.\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'objects',\n ... q({'subjects': people['Cathy'], 'reltype': 'manages'}))]\n [, ]\n >>> res = [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'objects',\n ... q({'subjects': people['Cathy']}))]\n >>> res[0]\n \n >>> sorted(repr(i) for i in res[1:])\n [\"\", \"\"]\n\nThe first search got what we expected for our management relationshiptype--\nwalking from Cathy, the relationshiptype was maintained, and we only got the\nGary subordinate. The second search didn't specify the relationshiptype, so\nthe transitive search included the Role we added first (Fred has the role of\nProject Manager for the zope.org redesign).\n\nThe `maxDepth` argument allows control over how far to search. For instance,\nif we only want to search for Bran's subordinates a maximum of two steps deep,\nwe can do so:\n\n >>> res = [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'objects',\n ... q({'subjects': people['Bran']}),\n ... maxDepth=2)]\n >>> sorted(repr(i) for i in res)\n [\"\", \"\", \"\"]\n\nThe same is true for findValues.\n\n >>> res = list(ix.findValues(\n ... 'objects',\n ... q({'subjects': people['Bran']}), maxDepth=2))\n >>> sorted(repr(i) for i in res)\n [\"\", \"\", \"\"]\n\nA minimum depth--a number of relationships that must be traversed before\nresults are desired--can also be achieved trivially using the targetFilter\nargument described soon below. For now, we will continue in the order of the\narguments list, so `filter` is up next.\n\nThe `filter` argument takes an object (such as a function) that provides\ninterfaces.IFilter. As the interface lists, it receives the current chain\nof relationship tokens (\"relchain\"), the original query that started the search\n(\"query\"), the index object (\"index\"), and a dictionary that will be used\nthroughout the search and then discarded that can be used for optimizations\n(\"cache\"). It should return a boolean, which determines whether the given\nrelchain should be used at all--traversed or returned. For instance, if\nsecurity dictates that the current user can only see certain relationships,\nthe filter could be used to make only the available relationships traversable.\nOther uses are only getting relationships that were created after a given time,\nor that have some annotation (available after resolving the token).\n\nLet's look at an example of a filter that only allows relationships in a given\nset, the way a security-based filter might work. We'll then use it to model\na situation in which the current user can't see that Ygritte is managed by\nUther, in addition to Xen.\n\n >>> s = set(intids.getId(r) for r in app.values()\n ... if IRelationship.providedBy(r))\n >>> relset = list(\n ... ix.findRelationshipTokenSet(q({'subjects': people['Xen']})))\n >>> len(relset)\n 1\n >>> s.remove(relset[0])\n >>> dump(people['Uther'], ix, {}) in list(\n ... ix.findValueTokens('subjects', q({'objects': people['Ygritte']})))\n True\n >>> dump(people['Uther'], ix, {}) in list(ix.findValueTokens(\n ... 'subjects', q({'objects': people['Ygritte']}),\n ... filter=lambda relchain, query, index, cache: relchain[-1] in s))\n False\n >>> people['Uther'] in list(\n ... ix.findValues('subjects', q({'objects': people['Ygritte']})))\n True\n >>> people['Uther'] in list(ix.findValues(\n ... 'subjects', q({'objects': people['Ygritte']}),\n ... filter=lambda relchain, query, index, cache: relchain[-1] in s))\n False\n\nThe next two search arguments are the targetQuery and the targetFilter. They\nboth are filters on the output of the search methods, while not affecting the\ntraversal/search process. The targetQuery takes a query identical to the main\nquery, and the targetFilter takes an IFilter identical to the one used by the\n`filter` argument. The targetFilter can do all of the work of the targetQuery,\nbut the targetQuery makes a common case--wanting to find the paths between two\nobjects, or if two objects are linked at all, for instance--convenient.\n\nWe'll skip over targetQuery for a moment (we'll return when we revisit\n`findRelationshipChains` and `isLinked`), and look at targetFilter.\ntargetFilter can be used for many tasks, such as only returning values that\nare in specially annotated relationships, or only returning values that have\ntraversed a certain hinge relationship in a two-part search, or other tasks.\nA very simple one, though, is to effectively specify a minimum traversal depth.\nHere, we find the people who are precisely two steps down from Bran, no more\nand no less. We do it twice, once with findValueTokens and once with\nfindValues.\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'objects', q({'subjects': people['Bran']}), maxDepth=2,\n ... targetFilter=lambda relchain, q, i, c: len(relchain)>=2)]\n []\n >>> list(ix.findValues(\n ... 'objects', q({'subjects': people['Bran']}), maxDepth=2,\n ... targetFilter=lambda relchain, q, i, c: len(relchain)>=2))\n []\n\nHeather is the only person precisely two steps down from Bran.\n\nNotice that we specified both maxDepth and targetFilter. We could have\nreceived the same output by specifying a targetFilter of `len(relchain)==2`\nand no maxDepth, but there is an important difference in efficiency. maxDepth\nand filter can reduce the amount of work done by the index because they can\nstop searching after reaching the maxDepth, or failing the filter; the\ntargetFilter and targetQuery arguments simply hide the results obtained, which\ncan reduce a bit of work in the case of getValues but generally don't reduce\nany of the traversal work.\n\nThe last argument to the search methods is `transitiveQueriesFactory`. It is\na powertool that replaces the index's default traversal factory for the\nduration of the search. This allows custom traversal for individual searches,\nand can support a number of advanced use cases. For instance, our index\nassumes that you want to traverse objects and sources, and that the context\nshould be constant; that may not always be the desired traversal behavior. If\nwe had a relationship of PERSON1 TAUGHT PERSON2 (the lessons of PERSON3) then\nto find the teachers of any given person you might want to traverse PERSON1,\nbut sometimes you might want to traverse PERSON3 as well. You can change the\nbehavior by providing a different factory.\n\nTo show this example we will need to add a few more relationships. We will say\nthat Mary teaches Rob the lessons of Abe; Olaf teaches Zane the lessons of\nBran; Cathy teaches Bran the lessons of Lee; David teaches Abe the lessons of\nZane; and Emily teaches Mary the lessons of Ygritte.\n\nIn the diagram, left-hand lines indicate \"taught\" and right-hand lines indicate\n\"the lessons of\", so ::\n\n E Y\n \\ /\n M\n\nshould be read as \"Emily taught Mary the lessons of Ygritte\". Here's the full\ndiagram::\n\n C L\n \\ /\n O B\n \\ /\n E Y D Z\n \\ / \\ /\n M A\n \\ /\n \\ /\n R\n\nYou can see then that the transitive path of Rob's teachers is Mary and Emily,\nbut the transitive path of Rob's lessons is Abe, Zane, Bran, and Lee.\n\nTransitive queries factories must do extra work when the transitive walk is\nacross token types. We have used the TransposingTransitiveQueriesFactory to\nbuild our transposers before, but now we need to write a custom one that\ntranslates the tokens (ooh! a\nTokenTranslatingTransposingTransitiveQueriesFactory! ...maybe we won't go that\nfar...).\n\nWe will add the relationships, build the custom transitive factory, and then\nagain do the search work twice, once with findValueTokens and once with\nfindValues.\n\n >>> for triple in ('EMY', 'MRA', 'DAZ', 'OZB', 'CBL'):\n ... teacher, student, source = (letters[l] for l in triple)\n ... rel = SpecialRelationship((teacher,), 'taught', (student,))\n ... app['%sTaught%sTo%s' % (\n ... teacher.name, source.name, student.name)] = rel\n ... IContextAwareRelationship(rel).setContext(source)\n ... ix.index_doc(intids.register(rel), rel)\n ...\n\n >>> def transitiveFactory(relchain, query, index, cache):\n ... dynamic = cache.get('dynamic')\n ... if dynamic is None:\n ... intids = cache['intids'] = component.getUtility(\n ... IIntIds, context=index)\n ... static = cache['static'] = {}\n ... dynamic = cache['dynamic'] = []\n ... names = ['objects', 'context']\n ... for nm, val in query.items():\n ... try:\n ... ix = names.index(nm)\n ... except ValueError:\n ... static[nm] = val\n ... else:\n ... if dynamic:\n ... # both were specified: no transitive search known.\n ... del dynamic[:]\n ... cache['intids'] = False\n ... break\n ... else:\n ... dynamic.append(nm)\n ... dynamic.append(names[not ix])\n ... else:\n ... intids = component.getUtility(IIntIds, context=index)\n ... if dynamic[0] == 'objects':\n ... def translate(t):\n ... return dump(intids.getObject(t), index, cache)\n ... else:\n ... def translate(t):\n ... return intids.register(load(t, index, cache))\n ... cache['translate'] = translate\n ... else:\n ... static = cache['static']\n ... translate = cache['translate']\n ... if dynamic:\n ... for r in index.findValueTokenSet(relchain[-1], dynamic[1]):\n ... res = {dynamic[0]: translate(r)}\n ... res.update(static)\n ... yield res\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'objects': people['Rob'], 'reltype': 'taught'}))]\n [, ]\n >>> [intids.getObject(t) for t in ix.findValueTokens(\n ... 'context',\n ... q({'objects': people['Rob'], 'reltype': 'taught'}),\n ... transitiveQueriesFactory=transitiveFactory)]\n [, , , ]\n\n >>> list(ix.findValues(\n ... 'subjects',\n ... q({'objects': people['Rob'], 'reltype': 'taught'})))\n [, ]\n >>> list(ix.findValues(\n ... 'context',\n ... q({'objects': people['Rob'], 'reltype': 'taught'}),\n ... transitiveQueriesFactory=transitiveFactory))\n [, , , ]\n\ntransitiveQueryFactories can be very powerful, and we aren't finished talking\nabout them in this document: see \"Transitively mapping multiple elements\"\nbelow.\n\nWe have now discussed, or at least mentioned, all of the available search\narguments. The `apply` method's 'values' search has the same arguments and\nfeatures as `findValues`, so it can also do these transitive tricks. Let's\nget all of Karyn's subordinates.\n\n >>> res = ix.apply({'values':\n ... {'resultName': 'objects', 'query':\n ... q({'reltype': 'manages',\n ... 'subjects': people['Karyn']})}})\n >>> res # doctest: +ELLIPSIS\n IFSet([...])\n >>> sorted(repr(load(t, ix, {})) for t in res)\n ... # doctest: +NORMALIZE_WHITESPACE\n [\"\", \"\", \"\",\n \"\", \"\", \"\"]\n\nAs we return to `findRelationshipChains` and `findRelationshipTokenChains`, we\nalso return to the search argument we postponed above: targetQuery.\n\nThe `findRelationshipChains` and `findRelationshipTokenChains` can simply find\nall paths:\n\n >>> res = [repr([intids.getObject(t) for t in path]) for path in\n ... ix.findRelationshipTokenChains(\n ... q({'reltype': 'manages', 'subjects': people['Jim']}\n ... ))]\n >>> len(res)\n 3\n >>> sorted(res[:2]) # doctest: +NORMALIZE_WHITESPACE\n [\"[<(, ) manages\n (, )>]\",\n \"[<(, ) manages\n (, , )>]\"]\n >>> res[2] # doctest: +NORMALIZE_WHITESPACE\n \"[<(, ) manages\n (, )>,\n <(, ) manages\n (,)>]\"\n >>> res == [repr(list(p)) for p in\n ... ix.findRelationshipChains(\n ... q({'reltype': 'manages', 'subjects': people['Jim']}\n ... ))]\n True\n\nLike `findValues`, this is a breadth-first search.\n\nIf we use a targetQuery with `findRelationshipChains`, you can find all paths\nbetween two searches. For instance, consider the paths between Rob and\nYgritte. While a `findValues` search would only include Rob once if asked to\nsearch for supervisors, there are two paths. These can be found with the\ntargetQuery.\n\n >>> res = [repr([intids.getObject(t) for t in path]) for path in\n ... ix.findRelationshipTokenChains(\n ... q({'reltype': 'manages', 'subjects': people['Rob']}),\n ... targetQuery=q({'objects': people['Ygritte']}))]\n >>> len(res)\n 2\n >>> sorted(res[:2]) # doctest: +NORMALIZE_WHITESPACE\n [\"[<(,) manages\n (, , )>,\n <(,) manages (,)>,\n <(, ) manages (,)>]\",\n \"[<(,) manages\n (, , )>,\n <(, ) manages (,)>]\"]\n\nHere's a query with no results:\n\n >>> len(list(ix.findRelationshipTokenChains(\n ... q({'reltype': 'manages', 'subjects': people['Rob']}),\n ... targetQuery=q({'objects': companies['Zookd']}))))\n 0\n\nYou can combine targetQuery with targetFilter. Here we arbitrarily say we\nare looking for a path between Rob and Ygritte that is at least 3 links long.\n\n >>> res = [repr([intids.getObject(t) for t in path]) for path in\n ... ix.findRelationshipTokenChains(\n ... q({'reltype': 'manages', 'subjects': people['Rob']}),\n ... targetQuery=q({'objects': people['Ygritte']}),\n ... targetFilter=lambda relchain, q, i, c: len(relchain)>=3)]\n >>> len(res)\n 1\n >>> res # doctest: +NORMALIZE_WHITESPACE\n [\"[<(,) manages\n (, , )>,\n <(,) manages (,)>,\n <(, ) manages (,)>]\"]\n\n`isLinked` takes the same arguments as all of the other transitive-aware\nmethods. For instance, Rob and Ygritte are transitively linked, but Abe and\nZane are not.\n\n >>> ix.isLinked(\n ... q({'reltype': 'manages', 'subjects': people['Rob']}),\n ... targetQuery=q({'objects': people['Ygritte']}))\n True\n >>> ix.isLinked(\n ... q({'reltype': 'manages', 'subjects': people['Abe']}),\n ... targetQuery=q({'objects': people['Ygritte']}))\n False\n\nDetecting cycles\n----------------\n\nSuppose we're modeling a 'king in disguise': someone high up in management also\nworks as a peon to see how his employees' lives are. We could model this a\nnumber of ways that might make more sense than what we'll do now, but to show\ncycles at work we'll just add an additional relationship so that Abe works for\nGary. That means that the very longest path from Ingrid up gets a lot longer--\nin theory, it's infinitely long, because of the cycle.\n\nThe index keeps track of this and stops right when the cycle happens, and right\nbefore the cycle duplicates any relationships. It marks the chain that has\ncycle as a special kind of tuple that implements ICircularRelationshipPath.\nThe tuple has a 'cycled' attribute that contains the one or more searches\nthat would be equivalent to following the cycle (given the same transitiveMap).\n\nLet's actually look at the example we described.\n\n >>> res = list(ix.findRelationshipTokenChains(\n ... q({'objects': people['Ingrid'], 'reltype': 'manages'})))\n >>> len(res)\n 4\n >>> len(res[3])\n 4\n >>> interfaces.ICircularRelationshipPath.providedBy(res[3])\n False\n >>> rel = Relationship(\n ... (people['Gary'],), 'manages', (people['Abe'],))\n >>> app['GaryManagesAbe'] = rel\n >>> ix.index(rel)\n >>> res = list(ix.findRelationshipTokenChains(\n ... q({'objects': people['Ingrid'], 'reltype': 'manages'})))\n >>> len(res)\n 8\n >>> len(res[7])\n 8\n >>> interfaces.ICircularRelationshipPath.providedBy(res[7])\n True\n >>> [sorted(ix.resolveQuery(search).items()) for search in res[7].cycled]\n [[('objects', ), ('reltype', 'manages')]]\n >>> tuple(ix.resolveRelationshipTokens(res[7]))\n ... # doctest: +NORMALIZE_WHITESPACE\n (<(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>)\n\nThe same kind of thing works for `findRelationshipChains`. Notice that the\nquery in the .cycled attribute is not resolved: it is still the query that\nwould be needed to continue the cycle.\n\n >>> res = list(ix.findRelationshipChains(\n ... q({'objects': people['Ingrid'], 'reltype': 'manages'})))\n >>> len(res)\n 8\n >>> len(res[7])\n 8\n >>> interfaces.ICircularRelationshipPath.providedBy(res[7])\n True\n >>> [sorted(ix.resolveQuery(search).items()) for search in res[7].cycled]\n [[('objects', ), ('reltype', 'manages')]]\n >>> res[7] # doctest: +NORMALIZE_WHITESPACE\n cycle(<(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>,\n <(,) manages (,)>)\n\nNotice that there is nothing special about the new relationship, by the way.\nIf we had started to look for Fred's supervisors, the cycle marker would have\nbeen given for the relationship that points back to Fred as a supervisor to\nhimself. There's no way for the computer to know which is the \"cause\" without\nfurther help and policy.\n\nHandling cycles can be tricky. Now imagine that we have a cycle that involves\na relationship with two objects, only one of which causes the cycle. The other\nobject should continue to be followed.\n\nFor instance, lets have Q manage L and Y. The link to L will be a cycle, but\nthe link to Y is not, and should be followed. This means that only the middle\nrelationship chain will be marked as a cycle.\n\n >>> rel = Relationship((people['Quince'],), 'manages',\n ... (people['Lee'], people['Ygritte']))\n >>> app['QuinceManagesLeeYgritte'] = rel\n >>> ix.index_doc(intids.register(rel), rel)\n >>> res = [p for p in ix.findRelationshipTokenChains(\n ... q({'reltype': 'manages', 'subjects': people['Mary']}))]\n >>> [interfaces.ICircularRelationshipPath.providedBy(p) for p in res]\n [False, True, False]\n >>> [[intids.getObject(t) for t in p] for p in res]\n ... # doctest: +NORMALIZE_WHITESPACE\n [[<(, ) manages (,)>],\n [<(, ) manages (,)>,\n <(,) manages (, )>],\n [<(, ) manages (,)>,\n <(,) manages (, )>,\n <(,) manages (,)>]]\n >>> [sorted(\n ... (nm, nm == 'reltype' and t or load(t, ix, {}))\n ... for nm, t in search.items()) for search in res[1].cycled]\n [[('reltype', 'manages'), ('subjects', )]]\n\nTransitively mapping multiple elements\n--------------------------------------\n\nTransitive searches can do whatever searches the transitiveQueriesFactory\nreturns, which means that complex transitive behavior can be modeled. For\ninstance, imagine genealogical relationships. Let's say the basic\nrelationship is \"MALE and FEMALE had CHILDREN\". Walking transitively to get\nancestors or descendants would need to distinguish between male children and\nfemale children in order to correctly generate the transitive search. This\ncould be accomplished by resolving each child token and examining the object\nor, probably more efficiently, getting an indexed collection of males and\nfemales (and cacheing it in the cache dictionary for further transitive steps)\nand checking the gender by membership in the indexed collections. Either of\nthese approaches could be performed by a transitiveQueriesFactory. A full\nexample is left as an exercise to the reader.\n\nLies, damn lies, and statistics\n===============================\n\nThe zope.index.interfaces.IStatistics methods are implemented to provide\nminimal introspectability. wordCount always returns 0, because words are\nirrelevant to this kind of index. documentCount returns the number of\nrelationships indexed.\n\n >>> ix.wordCount()\n 0\n >>> ix.documentCount()\n 25\n\nReindexing and removing relationships\n=====================================\n\nUsing an index over an application's lifecycle usually requires changes to the\nindexed objects. As per the zope.index interfaces, `index_doc` can reindex\nrelationships, `unindex_doc` can remove them, and `clear` can clear the entire\nindex.\n\nHere we change the zope.org project manager from Fred to Emily.\n\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']}))]\n []\n >>> rel = intids.getObject(list(ix.findRelationshipTokenSet(\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))[0])\n >>> rel.subjects = (people['Emily'],)\n >>> ix.index_doc(intids.register(rel), rel)\n >>> q = ix.tokenizeQuery\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']}))]\n []\n\nHere we remove the relationship that made a cycle for Abe in the 'king in\ndisguise' scenario.\n\n >>> res = list(ix.findRelationshipTokenChains(\n ... q({'objects': people['Ingrid'],\n ... 'reltype': 'manages'})))\n >>> len(res)\n 8\n >>> len(res[7])\n 8\n >>> interfaces.ICircularRelationshipPath.providedBy(res[7])\n True\n >>> rel = intids.getObject(list(ix.findRelationshipTokenSet(\n ... q({'subjects': people['Gary'], 'reltype': 'manages',\n ... 'objects': people['Abe']})))[0])\n >>> ix.unindex(rel) # == ix.unindex_doc(intids.getId(rel))\n >>> ix.documentCount()\n 24\n >>> res = list(ix.findRelationshipTokenChains(\n ... q({'objects': people['Ingrid'], 'reltype': 'manages'})))\n >>> len(res)\n 4\n >>> len(res[3])\n 4\n >>> interfaces.ICircularRelationshipPath.providedBy(res[3])\n False\n\nFinally we clear out the whole index.\n\n >>> ix.clear()\n >>> ix.documentCount()\n 0\n >>> list(ix.findRelationshipTokenChains(\n ... q({'objects': people['Ingrid'], 'reltype': 'manages'})))\n []\n >>> [load(t, ix, {}) for t in ix.findValueTokens(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']}))]\n []\n\nOptimizing relationship index use\n=================================\n\nThere are three optimization opportunities built into the index.\n\n- use the cache to load and dump tokens;\n\n- don't load or dump tokens (the values themselves may be used as tokens); and\n\n- have the returned value be of the same btree family as the result family.\n\nFor some operations, particularly with hundreds or thousands of members in a\nsingle relationship value, some of these optimizations can speed up some\ncommon-case reindexing work by around 100 times.\n\nThe easiest (and perhaps least useful) optimization is that all dump\ncalls and all load calls generated by a single operation share a cache\ndictionary per call type (dump/load), per indexed relationship value.\nTherefore, for instance, we could stash an intids utility, so that we\nonly had to do a utility lookup once, and thereafter it was only a\nsingle dictionary lookup. This is what the default `generateToken` and\n`resolveToken` functions in index.py do: look at them for an example.\n\nA further optimization is to not load or dump tokens at all, but use values\nthat may be tokens. This will be particularly useful if the tokens have\n__cmp__ (or equivalent) in C, such as built-in types like ints. To specify\nthis behavior, you create an index with the 'load' and 'dump' values for the\nindexed attribute descriptions explicitly set to None.\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True,\n ... 'dump': None, 'load': None},\n ... {'element': IRelationship['relationshiptype'],\n ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree,\n ... 'name': 'reltype'},\n ... {'element': IRelationship['objects'], 'multiple': True,\n ... 'dump': None, 'load': None},\n ... {'element': IContextAwareRelationship['getContext'],\n ... 'name': 'context'}),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ...\n >>> sm['rel_index_2'] = ix\n >>> app['ex_rel_1'] = rel = Relationship((1,), 'has the role of', (2,))\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 1}))\n [2]\n\nFinally, if you have single relationships that relate hundreds or thousands\nof objects, it can be a huge win if the value is a 'multiple' of the same type\nas the stored BTree for the given attribute. The default BTree family for\nattributes is IFBTree; IOBTree is also a good choice, and may be preferrable\nfor some applications.\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True,\n ... 'dump': None, 'load': None},\n ... {'element': IRelationship['relationshiptype'],\n ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree,\n ... 'name': 'reltype'},\n ... {'element': IRelationship['objects'], 'multiple': True,\n ... 'dump': None, 'load': None},\n ... {'element': IContextAwareRelationship['getContext'],\n ... 'name': 'context'}),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n ...\n >>> sm['rel_index_3'] = ix\n >>> from BTrees import IFBTree\n >>> app['ex_rel_2'] = rel = Relationship(\n ... IFBTree.IFTreeSet((1,)), 'has the role of', IFBTree.IFTreeSet())\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 1}))\n []\n >>> list(ix.findValueTokens('subjects', {'objects': None}))\n [1]\n\nReindexing is where some of the big improvements can happen. The following\ngyrations exercise the optimization code.\n\n >>> rel.objects.insert(2)\n 1\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 1}))\n [2]\n >>> rel.subjects = IFBTree.IFTreeSet((3,4,5))\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 3}))\n [2]\n\n >>> rel.subjects.insert(6)\n 1\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 6}))\n [2]\n\n >>> rel.subjects.update(range(100, 200))\n 100\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 100}))\n [2]\n\n >>> rel.subjects = IFBTree.IFTreeSet((3,4,5,6))\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 3}))\n [2]\n\n >>> rel.subjects = IFBTree.IFTreeSet(())\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 3}))\n []\n\n >>> rel.subjects = IFBTree.IFTreeSet((3,4,5))\n >>> ix.index(rel)\n >>> list(ix.findValueTokens('objects', {'subjects': 3}))\n [2]\n\ntokenizeValues and resolveValueTokens work correctly without loaders and\ndumpers--that is, they do nothing.\n\n >>> ix.tokenizeValues((3,4,5), 'subjects')\n (3, 4, 5)\n >>> ix.resolveValueTokens((3,4,5), 'subjects')\n (3, 4, 5)\n\n__contains__ and Unindexing\n=============================\n\nYou can test whether a relationship is in an index with __contains__. Note\nthat this uses the actual relationship, not the relationship token.\n\n >>> ix = index.Index(\n ... ({'element': IRelationship['subjects'], 'multiple': True,\n ... 'dump': dump, 'load': load},\n ... {'element': IRelationship['relationshiptype'],\n ... 'dump': relTypeDump, 'load': relTypeLoad, 'btree': OIBTree,\n ... 'name': 'reltype'},\n ... {'element': IRelationship['objects'], 'multiple': True,\n ... 'dump': dump, 'load': load},\n ... {'element': IContextAwareRelationship['getContext'],\n ... 'name': 'context'}),\n ... index.TransposingTransitiveQueriesFactory('subjects', 'objects'))\n >>> ix.documentCount()\n 0\n >>> app['fredisprojectmanager'].subjects = (people['Fred'],)\n >>> ix.index(app['fredisprojectmanager'])\n >>> ix.index(app['another_rel'])\n >>> ix.documentCount()\n 2\n >>> app['fredisprojectmanager'] in ix\n True\n >>> list(ix.findValues(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))\n []\n\n >>> app['another_rel'] in ix\n True\n\n >>> app['abeAndBran'] in ix\n False\n\nAs noted, you can unindex using unindex(relationship) or\nunindex_doc(relationship token).\n\n >>> ix.unindex_doc(ix.tokenizeRelationship(app['fredisprojectmanager']))\n >>> app['fredisprojectmanager'] in ix\n False\n >>> list(ix.findValues(\n ... 'subjects',\n ... q({'reltype': 'has the role of',\n ... 'objects': roles['Project Manager'],\n ... 'context': projects['zope.org redesign']})))\n []\n\n >>> ix.unindex(app['another_rel'])\n >>> app['another_rel'] in ix\n False\n\nAs defined by zope.index.interfaces.IInjection, if the relationship is\nnot in the index then calling unindex_doc is a no-op; the same holds\ntrue for unindex.\n\n >>> ix.unindex(app['abeAndBran'])\n >>> ix.unindex_doc(ix.tokenizeRelationship(app['abeAndBran']))\n\n.. ......... ..\n.. FOOTNOTES ..\n.. ......... ..\n\n.. [#apply] `apply` and the other zope.index-related methods are the obvious\n exceptions.\n\n\n=====================\nRelationshipContainer\n=====================\n\nThe relationship container holds IRelationship objects. It includes an API to\nsearch for relationships and the objects to which they link, transitively and\nintransitively. The relationships are objects in and of themselves, and they\ncan themselves be related as sources or targets in other relationships.\n\nThere are currently two implementations of the interface in this package. One\nuses intids, and the other uses key references. They have different\nadvantages and disadvantages.\n\nThe intids makes it possible to get intid values directly. This can make it\neasier to merge the results with catalog searches and other intid-based\nindexes. Possibly more importantly, it does not create ghosted objects for\nthe relationships as they are searched unless absolutely necessary (for\ninstance, using a relationship filter), but uses the intids alone for\nsearches. This can be very important if you are searching large databases of\nrelationships: the relationship objects and the associated keyref links in the\nother implementation can flush the entire ZODB object cache, possibly leading\nto unpleasant performance characteristics for your entire application.\n\nOn the other hand, there are a limited number of intids available: sys.maxint,\nor 2147483647 on a 32 bit machine. As the intid usage increases, the\nefficiency of finding unique intids decreases. This can be addressed by\nincreasing IOBTrees maximum integer to be 64 bit (9223372036854775807) or by\nusing the keyref implementation. The keyref implementation also eliminates a\ndependency--the intid utility itself--if that is desired. This can be\nimportant if you can't rely on having an intid utility, or if objects to be\nrelated span intid utilities. Finally, it's possible that the direct\nattribute access that underlies the keyref implementation might be quicker\nthan the intid dereferencing, but this is unproven and may be false.\n\nFor our examples, we'll assume we've already imported a container and a\nrelationship from one of the available sources. You can use a relationship\nspecific to your usage, or the generic one in shared, as long as it meets the\ninterface requirements.\n\nIt's also important to note that, while the relationship objects are an\nimportant part of the design, they should not be abused. If you want to store\nother data on the relationship, it should be stored in another persistent\nobject, such as an attribute annotation's btree. Typically relationship\nobjects will differ on the basis of interfaces, annotations, and possibly\nsmall lightweight values on the objects themselves.\n\nWe'll assume that there is an application named `app` with 30 objects in it\n(named 'ob0' through 'ob29') that we'll be relating.\n\nCreating a relationship container is easy. We'll use an abstract Container,\nbut it could be from the keyref or the intid modules.\n\n >>> from zc.relationship import interfaces\n >>> container = Container()\n >>> from zope.interface.verify import verifyObject\n >>> verifyObject(interfaces.IRelationshipContainer, container)\n True\n\nThe containers can be used as parts of other objects, or as standalone local\nutilities. Here's an example of adding one as a local utilty.\n\n >>> sm = app.getSiteManager()\n >>> sm['lineage_relationship'] = container\n >>> import zope.component.interfaces\n >>> registry = zope.component.interfaces.IComponentRegistry(sm)\n >>> registry.registerUtility(\n ... container, interfaces.IRelationshipContainer, 'lineage')\n >>> import transaction\n >>> transaction.commit()\n\nAdding relationships is also easy: instantiate and add. The `add` method adds\nobjects and assigns them random alphanumeric keys.\n\n >>> rel = Relationship((app['ob0'],), (app['ob1'],))\n >>> verifyObject(interfaces.IRelationship, rel)\n True\n >>> container.add(rel)\n\nAlthough the container does not have `__setitem__` and `__delitem__` (defining\n`add` and `remove` instead), it does define the read-only elements of the basic\nPython mapping interface.\n\n >>> container[rel.__name__] is rel\n True\n >>> len(container)\n 1\n >>> list(container.keys()) == [rel.__name__]\n True\n >>> list(container) == [rel.__name__]\n True\n >>> list(container.values()) == [rel]\n True\n >>> container.get(rel.__name__) is rel\n True\n >>> container.get('17') is None\n True\n >>> rel.__name__ in container\n True\n >>> '17' in container\n False\n >>> list(container.items()) == [(rel.__name__, rel)]\n True\n\nIt also supports four searching methods: `findTargets`, `findSources`,\n`findRelationships`, and `isLinked`. Let's add a few more relationships and\nexamine some relatively simple cases.\n\n >>> container.add(Relationship((app['ob1'],), (app['ob2'],)))\n >>> container.add(Relationship((app['ob1'],), (app['ob3'],)))\n >>> container.add(Relationship((app['ob0'],), (app['ob3'],)))\n >>> container.add(Relationship((app['ob0'],), (app['ob4'],)))\n >>> container.add(Relationship((app['ob2'],), (app['ob5'],)))\n >>> transaction.commit() # this is indicative of a bug in ZODB; if you\n ... # do not do this then new objects will deactivate themselves into\n ... # nothingness when _p_deactivate is called\n\nNow there are six direct relationships (all of the relationships point down\nin the diagram)::\n\n ob0\n | |\\\n ob1 | |\n | | | |\n ob2 ob3 ob4\n |\n ob5\n\nThe mapping methods still have kept up with the new additions.\n\n >>> len(container)\n 6\n >>> len(container.keys())\n 6\n >>> sorted(container.keys()) == sorted(\n ... v.__name__ for v in container.values())\n True\n >>> sorted(container.items()) == sorted(\n ... zip(container.keys(), container.values()))\n True\n >>> len([v for v in container.values() if container[v.__name__] is v])\n 6\n >>> sorted(container.keys()) == sorted(container)\n True\n\nMore interestingly, lets examine some of the searching methods. What are the\ndirect targets of ob0?\n\n >>> container.findTargets(app['ob0']) # doctest: +ELLIPSIS\n \n\nAh-ha! It's a generator! Let's try that again.\n\n >>> sorted(o.id for o in container.findTargets(app['ob0']))\n ['ob1', 'ob3', 'ob4']\n\nOK, what about the ones no more than two relationships away? We use the\n`maxDepth` argument, which is the second placeful argument.\n\n >>> sorted(o.id for o in container.findTargets(app['ob0'], 2))\n ['ob1', 'ob2', 'ob3', 'ob4']\n\nNotice that, even though ob3 is available both through one and two\nrelationships, it is returned only once.\n\nPassing in None will get all related objects--the same here as passing in 3, or\nany greater integer.\n\n >>> sorted(o.id for o in container.findTargets(app['ob0'], None))\n ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']\n >>> sorted(o.id for o in container.findTargets(app['ob0'], 3))\n ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']\n >>> sorted(o.id for o in container.findTargets(app['ob0'], 25))\n ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']\n\nThis is true even if we put in a cycle. We'll put in a cycle between ob5 and\nob1 and look at the results.\n\nAn important aspect of the algorithm used is that it returns closer\nrelationships first, which we can begin to see here.\n\n >>> container.add(Relationship((app['ob5'],), (app['ob1'],)))\n >>> transaction.commit()\n >>> sorted(o.id for o in container.findTargets(app['ob0'], None))\n ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']\n >>> res = list(o.id for o in container.findTargets(app['ob0'], None))\n >>> sorted(res[:3]) # these are all one step away\n ['ob1', 'ob3', 'ob4']\n >>> res[3:] # ob 2 is two steps, and ob5 is three steps.\n ['ob2', 'ob5']\n\nWhen you see the source in the targets, you know you are somewhere inside a\ncycle.\n\n >>> sorted(o.id for o in container.findTargets(app['ob1'], None))\n ['ob1', 'ob2', 'ob3', 'ob5']\n >>> sorted(o.id for o in container.findTargets(app['ob2'], None))\n ['ob1', 'ob2', 'ob3', 'ob5']\n >>> sorted(o.id for o in container.findTargets(app['ob5'], None))\n ['ob1', 'ob2', 'ob3', 'ob5']\n\nIf you ask for objects of a distance that is not a positive integer, you'll get\na ValueError.\n\n >>> container.findTargets(app['ob0'], 0)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findTargets(app['ob0'], -1)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findTargets(app['ob0'], 'kumquat') # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ...\n\nThe `findSources` method is the mirror of `findTargets`: given a target, it\nfinds all sources. Using the same relationship tree built above, we'll search\nfor some sources.\n\n >>> container.findSources(app['ob0']) # doctest: +ELLIPSIS\n \n >>> list(container.findSources(app['ob0']))\n []\n >>> list(o.id for o in container.findSources(app['ob4']))\n ['ob0']\n >>> list(o.id for o in container.findSources(app['ob4'], None))\n ['ob0']\n >>> sorted(o.id for o in container.findSources(app['ob1']))\n ['ob0', 'ob5']\n >>> sorted(o.id for o in container.findSources(app['ob1'], 2))\n ['ob0', 'ob2', 'ob5']\n >>> sorted(o.id for o in container.findSources(app['ob1'], 3))\n ['ob0', 'ob1', 'ob2', 'ob5']\n >>> sorted(o.id for o in container.findSources(app['ob1'], None))\n ['ob0', 'ob1', 'ob2', 'ob5']\n >>> sorted(o.id for o in container.findSources(app['ob3']))\n ['ob0', 'ob1']\n >>> sorted(o.id for o in container.findSources(app['ob3'], None))\n ['ob0', 'ob1', 'ob2', 'ob5']\n >>> list(o.id for o in container.findSources(app['ob5']))\n ['ob2']\n >>> list(o.id for o in container.findSources(app['ob5'], maxDepth=2))\n ['ob2', 'ob1']\n >>> sorted(o.id for o in container.findSources(app['ob5'], maxDepth=3))\n ['ob0', 'ob1', 'ob2', 'ob5']\n >>> container.findSources(app['ob0'], 0)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findSources(app['ob0'], -1)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findSources(app['ob0'], 'kumquat') # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ...\n\nThe `findRelationships` method finds all relationships from, to, or between\ntwo objects. Because it supports transitive relationships, each member of the\nresulting iterator is a tuple of one or more relationships.\n\nAll arguments to findRelationships are optional, but at least one of `source`\nor `target` must be passed in. A search depth defaults to one relationship\ndeep, like the other methods.\n\n >>> container.findRelationships(source=app['ob0']) # doctest: +ELLIPSIS\n \n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(source=app['ob0']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>']]\n >>> list(container.findRelationships(target=app['ob0']))\n []\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(target=app['ob3']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>']]\n >>> list(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... source=app['ob1'], target=app['ob3']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>']]\n >>> container.findRelationships()\n Traceback (most recent call last):\n ...\n ValueError: at least one of `source` and `target` must be provided\n\nThey may also be used as positional arguments, with the order `source` and\n`target`.\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(app['ob1']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>']]\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(app['ob5'], app['ob1']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>']]\n\n`maxDepth` is again available, but it is the third positional argument now, so\nkeyword usage will be more frequent than with the others. Notice that the\nsecond path has two members: from ob1 to ob2, then from ob2 to ob5.\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(app['ob1'], maxDepth=2))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>']]\n\nUnique relationships are returned, rather than unique objects. Therefore,\nwhile ob3 only has two transitive sources, ob1 and ob0, it has three transitive\npaths.\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... target=app['ob3'], maxDepth=2))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>']]\n\nThe same is true for the targets of ob0.\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... source=app['ob0'], maxDepth=2))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>']]\n\nCyclic relationships are returned in a special tuple that implements\nICircularRelationshipPath. For instance, consider all of the paths that lead\nfrom ob0. Notice first that all the paths are in order from shortest to\nlongest.\n\n >>> res = list(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... app['ob0'], maxDepth=None))\n ... # doctest: +NORMALIZE_WHITESPACE\n >>> sorted(res[:3]) # one step away # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>']]\n >>> sorted(res[3:5]) # two steps away # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>']]\n >>> res[5:] # three and four steps away # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>',\n ',) to (,)>',\n ',) to (,)>']]\n\nThe very last one is circular.\n\nNow we'll change the expression to only include paths that implement\nICircularRelationshipPath.\n\n >>> list(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... app['ob0'], maxDepth=None)\n ... if interfaces.ICircularRelationshipPath.providedBy(path))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>',\n ',) to (,)>',\n ',) to (,)>']]\n\nNote that, because relationships may have multiple targets, a relationship\nthat has a cycle may still be traversed for targets that do not generate a\ncycle. The further paths will not be marked as a cycle.\n\nCycle paths not only have a marker interface to identify them, but include a\n`cycled` attribute that is a frozenset of the one or more searches that would\nbe equivalent to following the cycle(s). If a source is provided, the searches\ncycled searches would continue from the end of the path.\n\n >>> path = [path for path in container.findRelationships(\n ... app['ob0'], maxDepth=None)\n ... if interfaces.ICircularRelationshipPath.providedBy(path)][0]\n >>> path.cycled\n [{'source': }]\n >>> app['ob1'] in path[-1].targets\n True\n\nIf only a target is provided, the `cycled` search will continue from the\nfirst relationship in the path.\n\n >>> path = [path for path in container.findRelationships(\n ... target=app['ob5'], maxDepth=None)\n ... if interfaces.ICircularRelationshipPath.providedBy(path)][0]\n >>> path # doctest: +NORMALIZE_WHITESPACE\n cycle(,) to (,)>,\n ,) to (,)>,\n ,) to (,)>)\n >>> path.cycled\n [{'target': }]\n\nmaxDepth can also be used with the combination of source and target.\n\n >>> list(container.findRelationships(\n ... app['ob0'], app['ob5'], maxDepth=None))\n ... # doctest: +NORMALIZE_WHITESPACE\n [(,) to (,)>,\n ,) to (,)>,\n ,) to (,)>)]\n\nAs usual, maxDepth must be a positive integer or None.\n\n >>> container.findRelationships(app['ob0'], maxDepth=0)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findRelationships(app['ob0'], maxDepth=-1)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.findRelationships(app['ob0'], maxDepth='kumquat')\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ...\n\nThe `isLinked` method is a convenient way to test if two objects are linked,\nor if an object is a source or target in the graph. It defaults to a maxDepth\nof 1.\n\n >>> container.isLinked(app['ob0'], app['ob1'])\n True\n >>> container.isLinked(app['ob0'], app['ob2'])\n False\n\nNote that maxDepth is pointless when supplying only one of source or target.\n\n >>> container.isLinked(source=app['ob29'])\n False\n >>> container.isLinked(target=app['ob29'])\n False\n >>> container.isLinked(source=app['ob0'])\n True\n >>> container.isLinked(target=app['ob4'])\n True\n >>> container.isLinked(source=app['ob4'])\n False\n >>> container.isLinked(target=app['ob0'])\n False\n\nSetting maxDepth works as usual when searching for a link between two objects,\nthough.\n\n >>> container.isLinked(app['ob0'], app['ob2'], maxDepth=2)\n True\n >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=2)\n False\n >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=3)\n True\n >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=None)\n True\n\nAs usual, maxDepth must be a positive integer or None.\n\n >>> container.isLinked(app['ob0'], app['ob1'], maxDepth=0)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.isLinked(app['ob0'], app['ob1'], maxDepth=-1)\n Traceback (most recent call last):\n ...\n ValueError: maxDepth must be None or a positive integer\n >>> container.isLinked(app['ob0'], app['ob1'], maxDepth='kumquat')\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: ...\n\nThe `remove` method is the next to last of the core interface: it allows you\nto remove relationships from a container. It takes a relationship object.\n\nAs an example, let's remove the relationship from ob5 to ob1 that we created\nto make the cycle.\n\n >>> res = list(container.findTargets(app['ob2'], None)) # before removal\n >>> len(res)\n 4\n >>> res[:2]\n [, ]\n >>> sorted(repr(o) for o in res[2:])\n ['', '']\n >>> res = list(container.findSources(app['ob2'], None)) # before removal\n >>> res[0]\n \n >>> res[3]\n \n >>> sorted(repr(o) for o in res[1:3])\n ['', '']\n >>> rel = list(container.findRelationships(app['ob5'], app['ob1']))[0][0]\n >>> rel.sources\n (,)\n >>> rel.targets\n (,)\n >>> container.remove(rel)\n >>> list(container.findRelationships(app['ob5'], app['ob1']))\n []\n >>> list(container.findTargets(app['ob2'], None)) # after removal\n []\n >>> list(container.findSources(app['ob2'], None)) # after removal\n [, ]\n\nFinally, the `reindex` method allows objects already in the container to be\nreindexed. The default implementation of the relationship objects calls this\nautomatically when sources and targets are changed.\n\nTo reiterate, the relationships looked like this before. ::\n\n ob0\n | |\\\n ob1 | |\n | | | |\n ob2 ob3 ob4\n |\n ob5\n\nWe'll switch out ob3 and ob4, so the diagram looks like this. ::\n\n ob0\n | |\\\n ob1 | |\n | | | |\n ob2 ob4 ob3\n |\n ob5\n\n >>> sorted(ob.id for ob in container.findTargets(app['ob1']))\n ['ob2', 'ob3']\n >>> sorted(ob.id for ob in container.findSources(app['ob3']))\n ['ob0', 'ob1']\n >>> sorted(ob.id for ob in container.findSources(app['ob4']))\n ['ob0']\n >>> rel = next(\n ... iter(container.findRelationships(app['ob1'], app['ob3'])\n ... ))[0]\n >>> rel.targets\n (,)\n\n >>> rel.targets = [app['ob4']] # this calls reindex\n\n >>> rel.targets\n (,)\n\n >>> sorted(ob.id for ob in container.findTargets(app['ob1']))\n ['ob2', 'ob4']\n >>> sorted(ob.id for ob in container.findSources(app['ob3']))\n ['ob0']\n >>> sorted(ob.id for ob in container.findSources(app['ob4']))\n ['ob0', 'ob1']\n\nThe same sort of thing happens if we change sources. We'll change the\ndiagram to look like this. ::\n\n ob0\n | |\\\n ob1 | |\n | | |\n ob2 | ob3\n | \\ |\n ob5 ob4\n\n >>> rel.sources\n (,)\n >>> rel.sources = (app['ob2'],) # this calls reindex\n >>> rel.sources\n (,)\n\n >>> sorted(ob.id for ob in container.findTargets(app['ob1']))\n ['ob2']\n >>> sorted(ob.id for ob in container.findTargets(app['ob2']))\n ['ob4', 'ob5']\n >>> sorted(ob.id for ob in container.findTargets(app['ob0']))\n ['ob1', 'ob3', 'ob4']\n >>> sorted(ob.id for ob in container.findSources(app['ob4']))\n ['ob0', 'ob2']\n\nAdvanced Usage\n==============\n\nThere are four other advanced tricks that the relationship container can do:\nenable search filters; allow multiple sources and targets for a single\nrelationship; allow relating relationships; and exposing unresolved token\nresults.\n\nSearch Filters\n--------------\n\nBecause relationships are objects themselves, a number of interesting usages\nare possible. They can implement additional interfaces, have annotations,\nand have other attributes. One use for this is to only find objects along\nrelationship paths with relationships that provide a given interface. The\n`filter` argument, allowed in `findSources`, `findTargets`,\n`findRelationships`, and `isLinked`, supports this kind of use case.\n\nFor instance, imagine that we change the relationships to look like the diagram\nbelow. The `xxx` lines indicate a relationship that implements\nISpecialRelationship. ::\n\n ob0\n x |x\n ob1 | x\n x | x\n ob2 | ob3\n | x |\n ob5 ob4\n\nThat is, the relationships from ob0 to ob1, ob0 to ob3, ob1 to ob2, and ob2 to\nob4 implement the special interface. Let's make this happen first.\n\n >>> from zope import interface\n >>> class ISpecialInterface(interface.Interface):\n ... \"\"\"I'm special! So special!\"\"\"\n ...\n >>> for src, tgt in (\n ... (app['ob0'], app['ob1']),\n ... (app['ob0'], app['ob3']),\n ... (app['ob1'], app['ob2']),\n ... (app['ob2'], app['ob4'])):\n ... rel = list(container.findRelationships(src, tgt))[0][0]\n ... interface.directlyProvides(rel, ISpecialInterface)\n ...\n\nNow we can use `ISpecialInterface.providedBy` as a filter for all of the\nmethods mentioned above.\n\n`findTargets`\n\n >>> sorted(ob.id for ob in container.findTargets(app['ob0']))\n ['ob1', 'ob3', 'ob4']\n >>> sorted(ob.id for ob in container.findTargets(\n ... app['ob0'], filter=ISpecialInterface.providedBy))\n ['ob1', 'ob3']\n >>> sorted(ob.id for ob in container.findTargets(\n ... app['ob0'], maxDepth=None))\n ['ob1', 'ob2', 'ob3', 'ob4', 'ob5']\n >>> sorted(ob.id for ob in container.findTargets(\n ... app['ob0'], maxDepth=None, filter=ISpecialInterface.providedBy))\n ['ob1', 'ob2', 'ob3', 'ob4']\n\n`findSources`\n\n >>> sorted(ob.id for ob in container.findSources(app['ob4']))\n ['ob0', 'ob2']\n >>> sorted(ob.id for ob in container.findSources(\n ... app['ob4'], filter=ISpecialInterface.providedBy))\n ['ob2']\n >>> sorted(ob.id for ob in container.findSources(\n ... app['ob4'], maxDepth=None))\n ['ob0', 'ob1', 'ob2']\n >>> sorted(ob.id for ob in container.findSources(\n ... app['ob4'], maxDepth=None, filter=ISpecialInterface.providedBy))\n ['ob0', 'ob1', 'ob2']\n >>> sorted(ob.id for ob in container.findSources(\n ... app['ob5'], maxDepth=None))\n ['ob0', 'ob1', 'ob2']\n >>> list(ob.id for ob in container.findSources(\n ... app['ob5'], filter=ISpecialInterface.providedBy))\n []\n\n`findRelationships`\n\n >>> len(list(container.findRelationships(\n ... app['ob0'], app['ob4'], maxDepth=None)))\n 2\n >>> len(list(container.findRelationships(\n ... app['ob0'], app['ob4'], maxDepth=None,\n ... filter=ISpecialInterface.providedBy)))\n 1\n >>> len(list(container.findRelationships(app['ob0'])))\n 3\n >>> len(list(container.findRelationships(\n ... app['ob0'], filter=ISpecialInterface.providedBy)))\n 2\n\n`isLinked`\n\n >>> container.isLinked(app['ob0'], app['ob5'], maxDepth=None)\n True\n >>> container.isLinked(\n ... app['ob0'], app['ob5'], maxDepth=None,\n ... filter=ISpecialInterface.providedBy)\n False\n >>> container.isLinked(\n ... app['ob0'], app['ob2'], maxDepth=None,\n ... filter=ISpecialInterface.providedBy)\n True\n >>> container.isLinked(\n ... app['ob0'], app['ob4'])\n True\n >>> container.isLinked(\n ... app['ob0'], app['ob4'],\n ... filter=ISpecialInterface.providedBy)\n False\n\nMultiple Sources and/or Targets; Duplicate Relationships\n--------------------------------------------------------\n\nRelationships are not always between a single source and a single target. Many\napproaches to this are possible, but a simple one is to allow relationships to\nhave multiple sources and multiple targets. This is an approach that the\nrelationship container supports.\n\n >>> container.add(Relationship(\n ... (app['ob2'], app['ob4'], app['ob5'], app['ob6'], app['ob7']),\n ... (app['ob1'], app['ob4'], app['ob8'], app['ob9'], app['ob10'])))\n >>> container.add(Relationship(\n ... (app['ob10'], app['ob0']),\n ... (app['ob7'], app['ob3'])))\n\nBefore we examine the results, look at those for a second.\n\nAmong the interesting items is that we have duplicated the ob2->ob4\nrelationship in the first example, and duplicated the ob0->ob3 relationship\nin the second. The relationship container does not limit duplicate\nrelationships: it simply adds and indexes them, and will include the additional\nrelationship path in findRelationships.\n\n >>> sorted(o.id for o in container.findTargets(app['ob4']))\n ['ob1', 'ob10', 'ob4', 'ob8', 'ob9']\n >>> sorted(o.id for o in container.findTargets(app['ob10']))\n ['ob3', 'ob7']\n >>> sorted(o.id for o in container.findTargets(app['ob4'], maxDepth=2))\n ['ob1', 'ob10', 'ob2', 'ob3', 'ob4', 'ob7', 'ob8', 'ob9']\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... app['ob2'], app['ob4']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[', , , , )\n to\n (, , , , )>'],\n [',) to (,)>']]\n\nThere's also a reflexive relationship in there, with ob4 pointing to ob4. It's\nmarked as a cycle.\n\n >>> list(container.findRelationships(app['ob4'], app['ob4']))\n ... # doctest: +NORMALIZE_WHITESPACE\n [cycle(, , , , )\n to\n (, , , , )>,)]\n >>> list(container.findRelationships(app['ob4'], app['ob4']))[0].cycled\n [{'source': }]\n\nRelating Relationships and Relationship Containers\n--------------------------------------------------\n\nRelationships are objects. We've already shown and discussed how this means\nthat they can implement different interfaces and be annotated. It also means\nthat relationships are first-class objects that can be related themselves.\nThis allows relationships that keep track of who created other relationships,\nand other use cases.\n\nEven the relationship containers themselves can be nodes in a relationship\ncontainer.\n\n >>> container1 = app['container1'] = Container()\n >>> container2 = app['container2'] = Container()\n >>> rel = Relationship((container1,), (container2,))\n >>> container.add(rel)\n >>> container.isLinked(container1, container2)\n True\n\nExposing Unresolved Tokens\n--------------------------\n\nFor specialized use cases, usually optimizations, sometimes it is useful to\nhave access to raw results from a given implementation. For instance, if a\nrelationship has many members, it might make sense to have an intid-based\nrelationship container return the actual intids.\n\nThe containers include three methods for these sorts of use cases:\n`findTargetTokens`, `findSourceTokens`, and `findRelationshipTokens`. They\ntake the same arguments as their similarly-named cousins.\n\nConvenience classes\n-------------------\n\nThree convenience classes exist for relationships with a single source and/or a\nsingle target only.\n\nOne-To-One Relationship\n~~~~~~~~~~~~~~~~~~~~~~~\n\nA `OneToOneRelationship` relates a single source to a single target.\n\n >>> from zc.relationship.shared import OneToOneRelationship\n >>> rel = OneToOneRelationship(app['ob20'], app['ob21'])\n\n >>> verifyObject(interfaces.IOneToOneRelationship, rel)\n True\n\nAll container methods work as for the general many-to-many relationship. We\nrepeat some of the tests defined in the main section above (all relationships\ndefined there are actually one-to-one relationships).\n\n >>> container.add(rel)\n >>> container.add(OneToOneRelationship(app['ob21'], app['ob22']))\n >>> container.add(OneToOneRelationship(app['ob21'], app['ob23']))\n >>> container.add(OneToOneRelationship(app['ob20'], app['ob23']))\n >>> container.add(OneToOneRelationship(app['ob20'], app['ob24']))\n >>> container.add(OneToOneRelationship(app['ob22'], app['ob25']))\n >>> rel = OneToOneRelationship(app['ob25'], app['ob21'])\n >>> container.add(rel)\n\n`findTargets`\n\n >>> sorted(o.id for o in container.findTargets(app['ob20'], 2))\n ['ob21', 'ob22', 'ob23', 'ob24']\n\n`findSources`\n\n >>> sorted(o.id for o in container.findSources(app['ob21'], 2))\n ['ob20', 'ob22', 'ob25']\n\n`findRelationships`\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(app['ob21'], maxDepth=2))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>']]\n\n >>> sorted(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... target=app['ob23'], maxDepth=2))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>'],\n [',) to (,)>',\n ',) to (,)>']]\n\n >>> list(container.findRelationships(\n ... app['ob20'], app['ob25'], maxDepth=None))\n ... # doctest: +NORMALIZE_WHITESPACE\n [(,) to (,)>,\n ,) to (,)>,\n ,) to (,)>)]\n\n >>> list(\n ... [repr(rel) for rel in path]\n ... for path in container.findRelationships(\n ... app['ob20'], maxDepth=None)\n ... if interfaces.ICircularRelationshipPath.providedBy(path))\n ... # doctest: +NORMALIZE_WHITESPACE\n [[',) to (,)>',\n ',) to (,)>',\n ',) to (,)>',\n ',) to (,)>']]\n\n`isLinked`\n\n >>> container.isLinked(source=app['ob20'])\n True\n >>> container.isLinked(target=app['ob24'])\n True\n >>> container.isLinked(source=app['ob24'])\n False\n >>> container.isLinked(target=app['ob20'])\n False\n >>> container.isLinked(app['ob20'], app['ob22'], maxDepth=2)\n True\n >>> container.isLinked(app['ob20'], app['ob25'], maxDepth=2)\n False\n\n`remove`\n\n >>> res = list(container.findTargets(app['ob22'], None)) # before removal\n >>> res[:2]\n [, ]\n >>> container.remove(rel)\n >>> list(container.findTargets(app['ob22'], None)) # after removal\n []\n\n`reindex`\n\n >>> rel = next(\n ... iter(container.findRelationships(app['ob21'], app['ob23']))\n ... )[0]\n\n >>> rel.target\n \n >>> rel.target = app['ob24'] # this calls reindex\n >>> rel.target\n \n\n >>> rel.source\n \n >>> rel.source = app['ob22'] # this calls reindex\n >>> rel.source\n \n\nManyToOneRelationship\n~~~~~~~~~~~~~~~~~~~~~\n\nA `ManyToOneRelationship` relates multiple sources to a single target.\n\n >>> from zc.relationship.shared import ManyToOneRelationship\n >>> rel = ManyToOneRelationship((app['ob22'], app['ob26']), app['ob24'])\n\n >>> verifyObject(interfaces.IManyToOneRelationship, rel)\n True\n\n >>> container.add(rel)\n >>> container.add(ManyToOneRelationship(\n ... (app['ob26'], app['ob23']),\n ... app['ob20']))\n\nThe relationship diagram now looks like this::\n\n ob20 (ob22, obj26) (ob26, obj23)\n | |\\ | |\n ob21 | | obj24 obj20\n | | |\n ob22 | ob23\n | \\ |\n ob25 ob24\n\nWe created a cycle for obj20 via obj23.\n\n >>> sorted(o.id for o in container.findSources(app['ob24'], None))\n ['ob20', 'ob21', 'ob22', 'ob23', 'ob26']\n\n >>> sorted(o.id for o in container.findSources(app['ob20'], None))\n ['ob20', 'ob23', 'ob26']\n\n >>> list(container.findRelationships(app['ob20'], app['ob20'], None))\n ... # doctest: +NORMALIZE_WHITESPACE\n [cycle(,) to (,)>,\n , ) to (,)>)]\n >>> list(container.findRelationships(\n ... app['ob20'], app['ob20'], 2))[0].cycled\n [{'source': }]\n\nThe `ManyToOneRelationship`'s `sources` attribute is mutable, while it's\n`targets` attribute is immutable.\n\n >>> rel.sources\n (, )\n >>> rel.sources = [app['ob26'], app['ob24']]\n\n >>> rel.targets\n (,)\n >>> rel.targets = (app['ob22'],)\n Traceback (most recent call last):\n ...\n AttributeError: can't set attribute\n\nBut the relationship has an additional mutable `target` attribute.\n\n >>> rel.target\n \n >>> rel.target = app['ob22']\n\nOneToManyRelationship\n~~~~~~~~~~~~~~~~~~~~~\n\nA `OneToManyRelationship` relates a single source to multiple targets.\n\n >>> from zc.relationship.shared import OneToManyRelationship\n >>> rel = OneToManyRelationship(app['ob22'], (app['ob20'], app['ob27']))\n\n >>> verifyObject(interfaces.IOneToManyRelationship, rel)\n True\n\n >>> container.add(rel)\n >>> container.add(OneToManyRelationship(\n ... app['ob20'],\n ... (app['ob23'], app['ob28'])))\n\nThe updated diagram looks like this::\n\n ob20 (ob26, obj24) (ob26, obj23)\n | |\\ | |\n ob21 | | obj22 obj20\n | | | | |\n ob22 | ob23 (ob20, obj27) (ob23, obj28)\n | \\ |\n ob25 ob24\n\nAlltogether there are now three cycles for ob22.\n\n >>> sorted(o.id for o in container.findTargets(app['ob22']))\n ['ob20', 'ob24', 'ob25', 'ob27']\n >>> sorted(o.id for o in container.findTargets(app['ob22'], None))\n ['ob20', 'ob21', 'ob22', 'ob23', 'ob24', 'ob25', 'ob27', 'ob28']\n\n >>> sorted(o.id for o in container.findTargets(app['ob20']))\n ['ob21', 'ob23', 'ob24', 'ob28']\n >>> sorted(o.id for o in container.findTargets(app['ob20'], None))\n ['ob20', 'ob21', 'ob22', 'ob23', 'ob24', 'ob25', 'ob27', 'ob28']\n\n >>> sorted(repr(c) for c in\n ... container.findRelationships(app['ob22'], app['ob22'], None))\n ... # doctest: +NORMALIZE_WHITESPACE\n ['cycle(,) to (, )>,\n ,) to (,)>,\n ,) to (,)>)',\n 'cycle(,) to (, )>,\n ,) to (,)>,\n , ) to (,)>)',\n 'cycle(,) to (,)>,\n , ) to (,)>)']\n\nThe `OneToManyRelationship`'s `targets` attribute is mutable, while it's\n`sources` attribute is immutable.\n\n >>> rel.targets\n (, )\n >>> rel.targets = [app['ob28'], app['ob21']]\n\n >>> rel.sources\n (,)\n >>> rel.sources = (app['ob23'],)\n Traceback (most recent call last):\n ...\n AttributeError: can't set attribute\n\nBut the relationship has an additional mutable `source` attribute.\n\n >>> rel.source\n \n >>> rel.target = app['ob23']\n\n\n=======\nChanges\n=======\n\n2.0.post1 (2018-06-19)\n======================\n\n- Fix PyPI page by using correct ReST syntax.\n\n\n2.0 (2018-06-19)\n================\n\nThe 2.x line is almost completely compatible with the 1.x line.\nThe one notable incompatibility does not affect the use of relationship\ncontainers and is small enough that it will hopefully affect noone.\n\nNew Requirements\n----------------\n\n- zc.relation\n\nIncompatibilities with 1.0\n--------------------------\n\n- `findRelationships` will now use the defaultTransitiveQueriesFactory if it\n is set. Set ``maxDepth`` to 1 if you do not want this behavior.\n\n- Some instantiation exceptions have different error messages.\n\nChanges in 2.0\n--------------\n\n- the relationship index code has been moved out to zc.relation and\n significantly refactored there. A fully backwards compatible subclass\n remains in zc.relationship.index\n\n- support both 64-bit and 32-bit BTree families\n\n- support specifying indexed values by passing callables rather than\n interface elements (which are also still supported).\n\n- in findValues and findValueTokens, `query` argument is now optional. If\n the query evaluates to False in a boolean context, all values, or value\n tokens, are returned. Value tokens are explicitly returned using the\n underlying BTree storage. This can then be used directly for other BTree\n operations.\n\n In these and other cases, you should not ever mutate returned results!\n They may be internal data structures (and are intended to be so, so\n that they can be used for efficient set operations for other uses).\n The interfaces hopefully clarify what calls will return an internal\n data structure.\n\n- README has a new beginning, which both demonstrates some of the new features\n and tries to be a bit simpler than the later sections.\n\n- `findRelationships` and new method `findRelationshipTokens` can find\n relationships transitively and intransitively. `findRelationshipTokens`\n when used intransitively repeats the behavior of `findRelationshipTokenSet`.\n (`findRelationshipTokenSet` remains in the API, not deprecated, a companion\n to `findValueTokenSet`.)\n\n- 100% test coverage (per the usual misleading line analysis :-) of index\n module. (Note that the significantly lower test coverage of the container\n code is unlikely to change without contributions: I use the index\n exclusively. See plone.relations for a zc.relationship container with\n very good test coverage.)\n\n- Tested with Python 2.7 and Python >= 3.5\n\n- Added test extra to declare test dependency on ``zope.app.folder``.\n\n\nBranch 1.1\n==========\n\n(supports Zope 3.4/Zope 2.11/ZODB 3.8)\n\n1.1.0\n-----\n\n- adjust to BTrees changes in ZODB 3.8 (thanks Juergen Kartnaller)\n\n- converted buildout to rely exclusively on eggs\n\nBranch 1.0\n==========\n\n(supports Zope 3.3/Zope 2.10/ZODB 3.7)\n\n1.0.2\n-----\n\n- Incorporated tests and bug fixes to relationship containers from\n Markus Kemmerling:\n\n * ManyToOneRelationship instantiation was broken\n\n * The `findRelationships` method misbehaved if both, `source` and `target`,\n are not None, but `bool(target)` evaluated to False.\n\n * ISourceRelationship and ITargetRelationship had errors.\n\n1.0.1\n-----\n\n- Incorporated test and bug fix from Gabriel Shaar::\n\n if the target parameter is a container with no objects, then\n `shared.AbstractContainer.isLinked` resolves to False in a bool context and\n tokenization fails. `target and tokenize({'target': target})` returns the\n target instead of the result of the tokenize function.\n\n- Made README.rst tests pass on hopefully wider set of machines (this was a\n test improvement; the relationship index did not have the fragility).\n Reported by Gabriel Shaar.\n\n1.0.0\n-----\n\nInitial release\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/zopefoundation/zc.relationship", "keywords": "zope zope3", "license": "ZPL 2.1", "maintainer": "", "maintainer_email": "", "name": "zc.relationship", "package_url": "https://pypi.org/project/zc.relationship/", "platform": "", "project_url": "https://pypi.org/project/zc.relationship/", "project_urls": { "Homepage": "https://github.com/zopefoundation/zc.relationship" }, "release_url": "https://pypi.org/project/zc.relationship/2.0.post1/", "requires_dist": [ "zope.app.intid", "zope.app.keyreference", "zope.app.testing", "zope.component", "zope.index", "zope.interface", "zope.location", "zope.testing", "zope.app.folder; extra == 'test'", "ZODB3 (>=3.8dev)", "setuptools", "six", "zc.relation (>=1.1)", "zope.app.component", "zope.app.container" ], "requires_python": "", "summary": "Zope 3 relationship index. Precursor to zc.relation.", "version": "2.0.post1" }, "last_serial": 3976189, "releases": { "1.0": [ { "comment_text": "", "digests": { "md5": "becccb21ca0245ce70022863e73a24c5", "sha256": "3134ad3f3bb9e6e381000f9579fab897a968c621fea8223cb47aabb40d1712fe" }, "downloads": -1, "filename": "zc.relationship-1.0-py2.4.egg", "has_sig": false, "md5_digest": "becccb21ca0245ce70022863e73a24c5", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 78710, "upload_time": "2006-12-02T03:09:03", "url": "https://files.pythonhosted.org/packages/09/33/aa9b22356a79bc80dd4035484b30484e93bc194162df31a7b1b96cbb8d1d/zc.relationship-1.0-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "ff8332f265a98f3713368a1d8cb8d35f", "sha256": "d89f3ea667df34fa8da888ae24650d4b8b280ec6b9b08342dfbd6ab63ef435c4" }, "downloads": -1, "filename": "zc.relationship-1.0.tar.gz", "has_sig": false, "md5_digest": "ff8332f265a98f3713368a1d8cb8d35f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 72957, "upload_time": "2006-12-02T03:09:04", "url": "https://files.pythonhosted.org/packages/0b/66/08f35627435603ed20178f9753a2009cee5631139d5a297939ef1b6e76f3/zc.relationship-1.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "756628495416bd513cfb22b4a6285c06", "sha256": "96a59201a08912acf5b1e2d724e008dae73ea48e2db85eb509f2e4905419a6e6" }, "downloads": -1, "filename": "zc.relationship-1.0.1-py2.4.egg", "has_sig": false, "md5_digest": "756628495416bd513cfb22b4a6285c06", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 78800, "upload_time": "2007-01-23T21:01:02", "url": "https://files.pythonhosted.org/packages/a4/a8/881ae7daa83c3083d60585ec2e2d660e26b30a57376c42a3f07b4eb492bf/zc.relationship-1.0.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "d4a47403baa55402a2c332d19e146c07", "sha256": "0e4dd2c9ab40080c65f3b324dca203f30f219796166b97b6c76393bc34d06946" }, "downloads": -1, "filename": "zc.relationship-1.0.1.tar.gz", "has_sig": false, "md5_digest": "d4a47403baa55402a2c332d19e146c07", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 73784, "upload_time": "2007-01-23T21:01:01", "url": "https://files.pythonhosted.org/packages/04/de/48b25df5a8c231a86a821cdb046aab96eab3066db04ff7bfc3ba86bbf3ac/zc.relationship-1.0.1.tar.gz" } ], "1.0.2": [ { "comment_text": "", "digests": { "md5": "fa65abf4d6331b8c9808e3fd034eb1ae", "sha256": "c921af04796840e046790abfbdcf02b1f1aa21498b57a5ba4711ae1826ad53c5" }, "downloads": -1, "filename": "zc.relationship-1.0.2-py2.4.egg", "has_sig": false, "md5_digest": "fa65abf4d6331b8c9808e3fd034eb1ae", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 80019, "upload_time": "2007-07-04T15:58:22", "url": "https://files.pythonhosted.org/packages/d4/13/b42c02211bb8f3ba22a4ba7c357936bcdda8081c6b155ac6192e4927ca25/zc.relationship-1.0.2-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "a36d25ae362e4c88a6a5deb6dc13208d", "sha256": "af2d180e856ddcefd87c7c111b50be9e2b57d3b0a26b446a3fb5d55407968de4" }, "downloads": -1, "filename": "zc.relationship-1.0.2.tar.gz", "has_sig": false, "md5_digest": "a36d25ae362e4c88a6a5deb6dc13208d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 74730, "upload_time": "2007-07-04T15:58:24", "url": "https://files.pythonhosted.org/packages/7b/a4/60a9cb0a5bce85bf8a7268cc74658a367f917a17688af779aa9240c249f5/zc.relationship-1.0.2.tar.gz" } ], "1.1": [ { "comment_text": "", "digests": { "md5": "6a1544744dc8a9a9099c89a5e38632e8", "sha256": "3f82dd16446d889bf6c04e3789d9568b8b37c039c73956370d127582df639ec7" }, "downloads": -1, "filename": "zc.relationship-1.1-py2.4.egg", "has_sig": false, "md5_digest": "6a1544744dc8a9a9099c89a5e38632e8", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 80768, "upload_time": "2007-07-04T16:00:17", "url": "https://files.pythonhosted.org/packages/ec/32/b0a7e90e6cf6ed3a2f596a33fb199fd57ccc97ea64eac9308cd3fbc77e3e/zc.relationship-1.1-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "de4cf0dc8e33176ebc1c956899267f64", "sha256": "e039b0f7dc4407d06a2f3c1453ae901cebcb3a251571f9416a168c3996134895" }, "downloads": -1, "filename": "zc.relationship-1.1.tar.gz", "has_sig": false, "md5_digest": "de4cf0dc8e33176ebc1c956899267f64", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 75715, "upload_time": "2007-07-04T16:00:18", "url": "https://files.pythonhosted.org/packages/ee/83/093bc316486400159531e423f45f153dad028d3286727f99bf6784fa86a4/zc.relationship-1.1.tar.gz" } ], "1.1.1": [ { "comment_text": "", "digests": { "md5": "b6461c8378603aceaa848294f92457e9", "sha256": "b39321691d35e10b9513cfe644cd7ac534dcc1fcb6e77d0fe80ed1c2a48d42f7" }, "downloads": -1, "filename": "zc.relationship-1.1.1.tar.gz", "has_sig": false, "md5_digest": "b6461c8378603aceaa848294f92457e9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 76001, "upload_time": "2009-03-17T15:58:10", "url": "https://files.pythonhosted.org/packages/46/c9/dbf0dc917309ae4eebe0cafefe7dcdf65c3a7746c6fa523ff79c93ae5c89/zc.relationship-1.1.1.tar.gz" } ], "1.1a": [ { "comment_text": "", "digests": { "md5": "b14ebe27b293eeb6d78d47d8e964984a", "sha256": "4df61b55df8df74d376e2d46bea7ebb8dd029969ea304f000baebf88e7ec6050" }, "downloads": -1, "filename": "zc.relationship-1.1a-py2.4.egg", "has_sig": false, "md5_digest": "b14ebe27b293eeb6d78d47d8e964984a", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 79139, "upload_time": "2007-02-06T23:18:25", "url": "https://files.pythonhosted.org/packages/ef/da/a490d4666ee230766d116da4c644c8caac91447b7348044bb6a353c4a037/zc.relationship-1.1a-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "3db6c87f2bc8910751c74ab03965cebd", "sha256": "783929e70cb2d4825d9b64524517f493ebe9914e5f3b1eb6dfdb838f734104ac" }, "downloads": -1, "filename": "zc.relationship-1.1a.tar.gz", "has_sig": false, "md5_digest": "3db6c87f2bc8910751c74ab03965cebd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 74161, "upload_time": "2007-02-06T23:18:21", "url": "https://files.pythonhosted.org/packages/9d/3b/75a713a73e41f3df18c5e6a502e14b1e49de174106320bf8eba399b15b0d/zc.relationship-1.1a.tar.gz" } ], "2.0": [ { "comment_text": "", "digests": { "md5": "284aae987716a3458d1b64229a1b395e", "sha256": "dc7a3bc8b291b50b83940e7a7f85d8e39e9b06a75ce460d0ec686243e2b08d55" }, "downloads": -1, "filename": "zc.relationship-2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "284aae987716a3458d1b64229a1b395e", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 109571, "upload_time": "2018-06-19T05:55:10", "url": "https://files.pythonhosted.org/packages/10/ba/901550f8a7654beac021b55c7147bded1bc0abac07f67dc46b6e8310363b/zc.relationship-2.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "7c6dbf9693c8643eb3de8b9b340825b7", "sha256": "659266ab13ba8def26e4d3ddf7fad4a7c29a70bdbb69ab2a7d7c9e43df720ff6" }, "downloads": -1, "filename": "zc.relationship-2.0.tar.gz", "has_sig": false, "md5_digest": "7c6dbf9693c8643eb3de8b9b340825b7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 108042, "upload_time": "2018-06-19T05:55:12", "url": "https://files.pythonhosted.org/packages/19/0f/eb4a0c8e46fe7d9333fb85ef53600b17f7844f98f3708d792e89c3d52a7c/zc.relationship-2.0.tar.gz" } ], "2.0.post1": [ { "comment_text": "", "digests": { "md5": "cbc7cd4e34ff95be9ba52fd8af107298", "sha256": "c1bd00e6ed4744773e97eeff543b4fb105c4f3ecae50a10c79922e866b9eb0a1" }, "downloads": -1, "filename": "zc.relationship-2.0.post1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "cbc7cd4e34ff95be9ba52fd8af107298", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 109843, "upload_time": "2018-06-19T06:01:01", "url": "https://files.pythonhosted.org/packages/d7/ea/adcd13284c6f8bcf88013f11ecd6775b9781f49f8fc4fbd5a95a925bba40/zc.relationship-2.0.post1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f3683db80bf000d082d230a986926161", "sha256": "bf089aeffe5da725925da7f6c165d20b8fbfc68b7f3130f33137ce2f16029f95" }, "downloads": -1, "filename": "zc.relationship-2.0.post1.tar.gz", "has_sig": false, "md5_digest": "f3683db80bf000d082d230a986926161", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 108253, "upload_time": "2018-06-19T06:01:02", "url": "https://files.pythonhosted.org/packages/3b/98/df336a67af2d546f84a8fe71fa4f3d647c89f196d83722db246dbcd3be9b/zc.relationship-2.0.post1.tar.gz" } ], "2.0dev": [ { "comment_text": "", "digests": { "md5": "0ea14abf2728319d7af12826e4f4c356", "sha256": "ef51bf51fb42396f94155a0430ca53d51f83a6d936d0cd473a1e578a98ffd7ab" }, "downloads": -1, "filename": "zc.relationship-2.0dev-py2.4.egg", "has_sig": false, "md5_digest": "0ea14abf2728319d7af12826e4f4c356", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 102425, "upload_time": "2007-07-04T16:10:08", "url": "https://files.pythonhosted.org/packages/9f/0b/49d327e346e424e0f8c5c131f2a2b16f5e0960f447ef29817970f91cac7d/zc.relationship-2.0dev-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "3d5f6da7c9fd326b5fae1acbba54f55e", "sha256": "b0059ea4b722cc8614261746c55c841a250edc4187d051d9baffce81fa9c6aae" }, "downloads": -1, "filename": "zc.relationship-2.0dev.tar.gz", "has_sig": false, "md5_digest": "3d5f6da7c9fd326b5fae1acbba54f55e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 98739, "upload_time": "2007-07-04T16:10:10", "url": "https://files.pythonhosted.org/packages/a7/db/83e6205f2023597beadfa42cb1001a37a38e1bef7555d8991d3b0f0a31d4/zc.relationship-2.0dev.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "cbc7cd4e34ff95be9ba52fd8af107298", "sha256": "c1bd00e6ed4744773e97eeff543b4fb105c4f3ecae50a10c79922e866b9eb0a1" }, "downloads": -1, "filename": "zc.relationship-2.0.post1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "cbc7cd4e34ff95be9ba52fd8af107298", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 109843, "upload_time": "2018-06-19T06:01:01", "url": "https://files.pythonhosted.org/packages/d7/ea/adcd13284c6f8bcf88013f11ecd6775b9781f49f8fc4fbd5a95a925bba40/zc.relationship-2.0.post1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f3683db80bf000d082d230a986926161", "sha256": "bf089aeffe5da725925da7f6c165d20b8fbfc68b7f3130f33137ce2f16029f95" }, "downloads": -1, "filename": "zc.relationship-2.0.post1.tar.gz", "has_sig": false, "md5_digest": "f3683db80bf000d082d230a986926161", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 108253, "upload_time": "2018-06-19T06:01:02", "url": "https://files.pythonhosted.org/packages/3b/98/df336a67af2d546f84a8fe71fa4f3d647c89f196d83722db246dbcd3be9b/zc.relationship-2.0.post1.tar.gz" } ] }