{ "info": { "author": "Zope Project", "author_email": "zope-dev@zope.org", "bugtrack_url": null, "classifiers": [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Framework :: Zope3", "Intended Audience :: Developers", "License :: OSI Approved :: Zope Public License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "*********\nzc.vault\n*********\n\nThe `zc.vault` package provides a low-level versioning support similar\nto revision control systems, with an example usage and several example\nadd-ons. It's ZODB-friendly.\n\n.. See ``src/zc/vault/README.txt`` for details.\n\n\n.. contents::\n\nDetailed Documentation\n**********************\n\n=====\nVault\n=====\n\nVaults model versioned containers. A single revision of a vault is\ntypically viewed and (if not yet frozen) manipulated as an \"inventory\".\nInventories actually manipulate lower-level objects called manifests\nthat are only touched on in this document. Inventories are the primary\nAPI.\n\nInventories *model* containers, but are not traditional mappings:\ncontainment is external to the actual objects in the inventory. You\nmust query the inventory to discover the hierarchy, rather than the\nobjects themselves. For instance, if you put an object in an inventory\nand want to treat it as a versioned folder, you don't put children in\nthe object, but in the inventory node that wraps the object. This will\nbe demonstrated repeatedly and in-depth below.\n\nVaults only contain versioned, frozen manifests, accessed as\ninventories. Working inventories can be made from any inventory in a\nvault. They can then be modified, and committed themselves in the\nvault. Committing an inventory freezes it and all objects it\n\"contains\".\n\nLet's look at an example. Vaults store manifests, so when you first\ncreate one it is empty. Vaults have a basic sequence API, so a `len`\nwill return `0`.\n\n >>> from zc.vault.vault import Vault, Inventory\n >>> from zc.vault.core import Manifest\n >>> from zc.vault import interfaces\n >>> from zope.interface.verify import verifyObject\n >>> v = Vault()\n >>> len(v)\n 0\n >>> verifyObject(interfaces.IVault, v)\n True\n\nThe last inventory--the -1 index--is the current one. A shorthand to this\ninventory is the inventory attribute.\n\n >>> v.inventory # None\n\nVaults and inventories must have a database connection in order to store their\ndata. We'll assume we have a ZODB folder named \"app\" in which we can store\nour information. This is set up in tests.py when this file is run as a test.\n\n >>> app['vault'] = v\n\nCreating an initial working inventory requires us to merely instantiate it.\nUsually we pass a versioned inventory on which to base the new inventory, but\nwithout that we at least pass the vault.\n\n >>> i = Inventory(vault=v)\n >>> verifyObject(interfaces.IInventory, i)\n True\n\nTechnically, what we have done is create a manifest--the core API for managing\nthe contents--and wrapped an inventory API around it.\n\n >>> verifyObject(interfaces.IManifest, i.manifest)\n True\n\nWe could have created the manifest explicitly instead.\n\n >>> manifest = Manifest(vault=v)\n >>> verifyObject(interfaces.IManifest, manifest)\n True\n >>> i = Inventory(manifest)\n >>> verifyObject(interfaces.IInventory, i)\n True\n\nInventories--or at least the manifests on which they rely--must be\nstored somewhere in the database before being committed. They provide\nzope.app.location.interfaces.ILocation so that they can be stored in\nstandard Zope containers as they are being developed.\n\n >>> app['inventory'] = i\n\nInventories have contents that can seem to directly contain objects. They have\na mapping API, and follow the IInventoryContents interface.\n\n >>> verifyObject(interfaces.IInventoryContents, i.contents)\n True\n >>> len(i.contents.keys())\n 0\n >>> len(i.contents.values())\n 0\n >>> len(i.contents.items())\n 0\n >>> list(i.contents)\n []\n >>> i.contents.get('mydemo') # None\n >>> 'mydemo' in i\n False\n >>> i.contents['mydemo']\n Traceback (most recent call last):\n ...\n KeyError: 'mydemo'\n >>> del i.contents['mydemo']\n Traceback (most recent call last):\n ...\n KeyError: 'mydemo'\n\n(ADVANCED SIDE NOTE: feel free to ignore)\n\nThe contents object is an API convenience to wrap a relationship.\nRelationships connect a token to various pieces of information. The\ntoken for all inventory contents (the top node) is stored on the vault\nas the top_token attribute, and lower levels get unique tokens that\nrepresent a given location in a vault across inventories.\n\nContents and items (seen below) essentially get all their data from the\nrelationships and the associated manifest that holds them.\n\n >>> verifyObject(interfaces.IRelationship, i.contents.relationship)\n True\n >>> i.contents.relationship.token == i.vault.top_token\n True\n >>> verifyObject(interfaces.IRelationshipContainment,\n ... i.contents.relationship.containment)\n True\n >>> i.contents.relationship.object # None, because contents.\n\n(end ADVANCED SIDE NOTE)\n\nBecause it is often convenient to use tokens as a globally unique identifier\nof a particular object, all inventory items have a \"token\" attribute.\n\n >>> i.contents.token\n 1234567\n\nUnlike typical Zope 3 containment as defined in zope.app.container, this\ncontainment does not affect the __parent__ or __name__ of the object.\n\nAll objects stored in an inventory must be None, or be adaptable to\nzope.app.keyreference.interfaces.IKeyReference. In standard Zope 3,\nthis includes any instance of a class that extends\npersistent.Persistent.\n\nAll non-None objects must also be adaptable to\nzc.freeze.interfaces.IFreezable.\n\nHere, we create an object, add it to the application, and try to add it to\nan inventory.\n\n >>> import persistent\n >>> from zope.app.container.contained import Contained\n >>> class Demo(persistent.Persistent, Contained):\n ... def __repr__(self):\n ... return \"<%s %r>\" % (self.__class__.__name__, self.__name__)\n ...\n >>> app['d1'] = Demo()\n >>> i.contents['mydemo'] = app['d1']\n Traceback (most recent call last):\n ...\n ValueError: can only place freezable objects in vault, or None\n\nThis error occurs because committing an inventory must freeze itself\nand freeze all of its contained objects, so that looking at an\nhistorical inventory displays the objects as they were at the time of\ncommit. Here's a simple demo adapter for the Demo objects. We also\ndeclare that Demo is IFreezable, an important marker.\n\n >>> import pytz\n >>> import datetime\n >>> from zope import interface, component, event\n >>> from zc.freeze.interfaces import (\n ... IFreezing, ObjectFrozenEvent, IFreezable)\n >>> from zc.freeze import method\n >>> class DemoFreezingAdapter(object):\n ... interface.implements(IFreezing)\n ... component.adapts(Demo)\n ... def __init__(self, context):\n ... self.context = context\n ... @property\n ... def _z_frozen(self):\n ... return (getattr(self.context, '_z__freeze_timestamp', None)\n ... is not None)\n ... @property\n ... def _z_freeze_timestamp(self):\n ... return getattr(self.context, '_z__freeze_timestamp', None)\n ... @method\n ... def _z_freeze(self):\n ... self.context._z__freeze_timestamp = datetime.datetime.now(\n ... pytz.utc)\n ... event.notify(ObjectFrozenEvent(self))\n ...\n >>> component.provideAdapter(DemoFreezingAdapter)\n >>> interface.classImplements(Demo, IFreezable)\n\nAs an aside, it's worth noting that the manifest objects provide\nIFreezing natively, so they can already be queried for the freezing\nstatus and timestamp without adaptation. When a manifest is frozen,\nall \"contained\" objects should be frozen as well.\n\nIt's not frozen now--and neither is our demo instance.\n\n >>> manifest._z_frozen\n False\n >>> IFreezing(app['d1'])._z_frozen\n False\n\nNow that Demo instances are freezable we can add the object to the inventory.\nThat means adding and removing objects. Here we add one.\n\n >>> i.contents['mydemo'] = app['d1']\n >>> i.contents['mydemo']\n \n >>> i.__parent__ is app\n True\n >>> i.contents.__parent__ is i\n True\n >>> i.contents.get('mydemo')\n \n >>> list(i.contents.keys())\n ['mydemo']\n >>> i.contents.values()\n []\n >>> i.contents.items()\n [('mydemo', )]\n >>> list(i.contents)\n ['mydemo']\n >>> 'mydemo' in i.contents\n True\n\nNow our effective hierarchy simply looks like this::\n\n (top node)\n |\n 'mydemo'\n ()\n\nWe will update this hierarchy as we proceed.\n\nAdding an object fires a (special to the package!) IObjectAdded event.\nThis event is not from the standard lifecycleevents package because\nthat one has a different connotation--for instance, as noted before,\nputting an object in an inventory does not set the __parent__ or\n__name__ (unless it does not already have a location, in which case it\nis put in a possibly temporary \"held\" container, discussed below).\n\n >>> interfaces.IObjectAdded.providedBy(events[-1])\n True\n >>> isinstance(events[-1].object, int)\n True\n >>> i.manifest.get(events[-1].object).object is app['d1']\n True\n >>> events[-1].mapping is i.contents.relationship.containment\n True\n >>> events[-1].key\n 'mydemo'\n\nNow we remove the object.\n\n >>> del i.contents['mydemo']\n >>> len(i.contents.keys())\n 0\n >>> len(i.contents.values())\n 0\n >>> len(i.contents.items())\n 0\n >>> list(i.contents)\n []\n >>> i.contents.get('mydemo') # None\n >>> 'mydemo' in i.contents\n False\n >>> i.contents['mydemo']\n Traceback (most recent call last):\n ...\n KeyError: 'mydemo'\n >>> del i.contents['mydemo']\n Traceback (most recent call last):\n ...\n KeyError: 'mydemo'\n\nRemoving an object fires a special IObjectRemoved event (again, not from\nlifecycleevents).\n\n >>> interfaces.IObjectRemoved.providedBy(events[-1])\n True\n >>> isinstance(events[-1].object, int)\n True\n >>> i.manifest.get(events[-1].object).object is app['d1']\n True\n >>> events[-1].mapping is i.contents.relationship.containment\n True\n >>> events[-1].key\n 'mydemo'\n\nIn addition to a mapping API, the inventory contents support an ordered\ncontainer API very similar to the ordered container in\nzope.app.container.ordered. The ordered nature of the contents mean that\niterating is on the basis of the order in which objects were added, by default\n(earliest first); and that the inventory supports an \"updateOrder\" method.\nThe method takes an iterable of names in the container: the new order will be\nthe given order. If the set of given names differs at all with the current\nset of keys, the method will raise ValueError.\n\n >>> i.contents.updateOrder(())\n >>> i.contents.updateOrder(('foo',))\n Traceback (most recent call last):\n ...\n ValueError: Incompatible key set.\n >>> i.contents['donald'] = app['d1']\n >>> app['b1'] = Demo()\n >>> i.contents['barbara'] = app['b1']\n >>> app['c1'] = Demo()\n >>> app['a1'] = Demo()\n >>> i.contents['cathy'] = app['c1']\n >>> i.contents['abe'] = app['a1']\n >>> list(i.contents.keys())\n ['donald', 'barbara', 'cathy', 'abe']\n >>> i.contents.values()\n [, , , ]\n >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE\n [('donald', ), ('barbara', ),\n ('cathy', ), ('abe', )]\n >>> list(i.contents)\n ['donald', 'barbara', 'cathy', 'abe']\n >>> 'cathy' in i.contents\n True\n >>> i.contents.updateOrder(())\n Traceback (most recent call last):\n ...\n ValueError: Incompatible key set.\n >>> i.contents.updateOrder(('foo',))\n Traceback (most recent call last):\n ...\n ValueError: Incompatible key set.\n >>> i.contents.updateOrder(iter(('abe', 'barbara', 'cathy', 'donald')))\n >>> list(i.contents.keys())\n ['abe', 'barbara', 'cathy', 'donald']\n >>> i.contents.values()\n [, , , ]\n >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE\n [('abe', ), ('barbara', ),\n ('cathy', ), ('donald', )]\n >>> list(i.contents)\n ['abe', 'barbara', 'cathy', 'donald']\n >>> i.contents.updateOrder(('abe', 'cathy', 'donald', 'barbara', 'edward'))\n Traceback (most recent call last):\n ...\n ValueError: Incompatible key set.\n >>> list(i.contents)\n ['abe', 'barbara', 'cathy', 'donald']\n >>> del i.contents['cathy']\n >>> list(i.contents.keys())\n ['abe', 'barbara', 'donald']\n >>> i.contents.values()\n [, , ]\n >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE\n [('abe', ), ('barbara', ), ('donald', )]\n >>> list(i.contents)\n ['abe', 'barbara', 'donald']\n >>> i.contents.updateOrder(('barbara', 'abe', 'donald'))\n >>> list(i.contents.keys())\n ['barbara', 'abe', 'donald']\n >>> i.contents.values()\n [, , ]\n\nNow our _`hierarchy` looks like this::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n\nReordering a container fires an event.\n\n >>> interfaces.IOrderChanged.providedBy(events[-1])\n True\n >>> events[-1].object is i.contents.relationship.containment\n True\n >>> events[-1].old_keys\n ('abe', 'barbara', 'donald')\n\nIn some circumstances it's easier to set the new order from a set of\ntokens. In that case the \"updateOrderFromTokens\" method is useful.\n\n >>> def getToken(key):\n ... return i.contents(k).token\n\n >>> new_order = [getToken(k) for k in ('abe', 'donald', 'barbara')]\n >>> i.contents.updateOrderFromTokens(new_order)\n >>> list(i.contents.keys())\n ['abe', 'donald', 'barbara']\n\nJust like \"updateOrder\", an event is fired.\n\n >>> interfaces.IOrderChanged.providedBy(events[-1])\n True\n >>> events[-1].object is i.contents.relationship.containment\n True\n >>> events[-1].old_keys\n ('barbara', 'abe', 'donald')\n\nIt's just as easy to put them back so that the `hierarchy`_ still looks the\nsame as it did at the end of the previous example.\n\n >>> new_order = [getToken(k) for k in ('barbara', 'abe', 'donald')]\n >>> i.contents.updateOrderFromTokens(new_order)\n >>> list(i.contents.keys())\n ['barbara', 'abe', 'donald']\n\nAs noted in the introduction to this document, the versioned hierarchy\nis kept external from the objects themselves. This means that objects\nthat are not containers themselves can still be branch\nnodes--containers, of a sort--within an inventory. In fact, until a\nreasonable use case emerges for the pattern, the author discourages the\nuse of true containers within a vault as branch nodes: two dimensions\nof \"containerish\" behavior is too confusing.\n\nIn order to get an object that can act as a container for one of the objects\nin the inventory, one calls the inventory contents: \"i.contents('abe')\". This\nreturns an IInventoryItem, if the key exists. It raises a KeyError for a\nmissing key by default, but can take a default.\n\n >>> i.contents['abe']\n \n >>> item = i.contents('abe')\n >>> verifyObject(interfaces.IInventoryItem, item)\n True\n >>> i.contents('foo')\n Traceback (most recent call last):\n ...\n KeyError: 'foo'\n >>> i.contents('foo', None) # None\n\nIInventoryItems extend IInventoryContents to add an 'object' attribute, which\nis the object they represent. Like IInventoryContents, a mapping interface\nallows one to manipulate the hierarchy beneath the top level. For instance,\nhere we effectively put the 'cathy' demo object in the container space of the\n'abe' demo object.\n\n >>> item.object\n \n >>> item.name\n 'abe'\n >>> item.parent.relationship is i.contents.relationship\n True\n >>> item.__parent__ is item.inventory\n True\n >>> list(item.values())\n []\n >>> list(item.keys())\n []\n >>> list(item.items())\n []\n >>> list(item)\n []\n >>> item.get('foo') # None\n >>> item['foo']\n Traceback (most recent call last):\n ...\n KeyError: 'foo'\n >>> item('foo')\n Traceback (most recent call last):\n ...\n KeyError: 'foo'\n >>> item['catherine'] = app['c1']\n >>> item['catherine']\n \n >>> item.get('catherine')\n \n >>> list(item.keys())\n ['catherine']\n >>> list(item.values())\n []\n >>> list(item.items())\n [('catherine', )]\n >>> catherine = item('catherine')\n >>> catherine.object\n \n >>> catherine.name\n 'catherine'\n >>> catherine.parent.name\n 'abe'\n >>> catherine.parent.object\n \n >>> list(catherine.keys())\n []\n\nNow our hierarchy looks like this::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n |\n |\n 'catherine'\n \n\nIt's worthwhile noting that the same object can be in multiple places in an\ninventory. This does not duplicate the hierarchy, or keep changes in sync.\nIf desired, this policy should be performed in code that uses the vault;\nsimilarly if a vault should only contain an object in one location at a time,\nthis should be enforced in code that uses a vault.\n\n >>> i.contents('abe')('catherine')['anna'] = app['a1']\n >>> i.contents('abe')('catherine').items()\n [('anna', )]\n >>> i.contents('abe')('catherine')('anna').parent.parent.object\n \n\nNow our hierarchy looks like this::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nEven though a1 contains c1 contains a1, this does not constitute a cycle: the\nhierarchy is separate from the objects.\n\nInventoryItems and InventoryContents are currently created on the fly, and\nnot persisted. They should be compared with \"==\", not \"is\". They represent\na persistent core data object that provides zc.vault.interfaces.IRelationship.\nThe IRelationship itself is hidden from the majority of this discussion and\nonly introduced at the end of the document. But in any case...\n\n >>> i.contents('abe') is i.contents('abe')\n False\n >>> i.contents('abe') == i.contents('abe')\n True\n >>> i.contents is i.contents\n False\n >>> i.contents == i.contents\n True\n >>> i.contents == None\n False\n >>> i.contents('abe') == None\n False\n\nComparing inventories will also compare their contents:\n\n >>> i == None\n False\n >>> i == i\n True\n >>> i != i\n False\n\nAnother important characteristic of inventory items is that they continue to\nhave the right information even as objects around them are changed--for\ninstance, if an object's parent is changed from one part of the hierarchy to\nanother (see `moveTo`, below), an item generated before the move will still\nreflect the change correctly.\n\nIt's worth noting that, thanks to the wonder of the zc.shortcut code, views can\nexist for the object and also, from a proxy, have access to the InventoryItem's\ninformation: this needs to be elaborated (TODO).\n\nNow we'll try to commit.\n\n >>> v.commit(i) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ConflictError: \n\nConflicts? We don't need no stinking conflicts! We didn't even merge! Where\ndid this come from?\n\nThe default vault takes a very strict approach to keeping track of conflicts:\nfor instance, if you add something and then delete it in the same inventory,\nit will regard this as an \"orphan conflict\": a change that happened in this\ninventory that will not be committed. You must explicitly say that it is\nOK for these orphaned changes to be lost. Let's look at the orphans.\n\n >>> orphans = list(i.iterOrphanConflicts())\n >>> sorted(repr(item.object) for item in orphans)\n [\"\", \"\"]\n >>> orphans[0].parent # None\n >>> orphans[0].name # None\n\nAh yes--you can see that we deleted these objects above: we deleted \"mydemo\"\n(d1) and cathy (c1). We'll just tell the inventory that it is ok to not\ninclude them. If vault clients want to have more automation so that deletions\nautomatically resolve, then they have the tools to do so. After the\nresolution, iterOrphanConflicts will then be empty, and iterOrphanResolutions\nwill include the objects.\n\n >>> for o in orphans:\n ... o.resolveOrphanConflict()\n ...\n >>> len(list(i.iterOrphanConflicts()))\n 0\n >>> sorted(repr(item.object) for item in i.iterOrphanResolutions())\n [\"\", \"\"]\n\nNow when we commit, all objects will be versioned, and we will receive events\nfor the freezing and the committing. The events list represents recent\nevents; when this document is run as a test, it is populated by listening for\nall events and attaching them to the list.\n\n >>> v.commit(i)\n >>> interfaces.IManifestCommitted.providedBy(events[-1])\n True\n >>> events[-1].object is manifest\n True\n >>> manifest.__parent__ is v\n True\n >>> IFreezing(app['a1'])._z_frozen\n True\n >>> IFreezing(app['b1'])._z_frozen\n True\n >>> IFreezing(app['c1'])._z_frozen\n True\n >>> IFreezing(app['d1'])._z_frozen\n True\n >>> manifest._z_frozen\n True\n >>> v.manifest is manifest\n True\n >>> len(v)\n 1\n\nAfter the committing, the inventory enforces the freeze: no more changes\ncan be made.\n\n >>> i.contents['foo'] = Demo()\n Traceback (most recent call last):\n ...\n FrozenError\n >>> i.contents.updateOrder(())\n Traceback (most recent call last):\n ...\n FrozenError\n >>> i.contents('abe')('catherine')['foo'] = Demo()\n Traceback (most recent call last):\n ...\n FrozenError\n\n >>> v.manifest._z_frozen\n True\n\nEnforcing the freezing of the inventory's objects is the responsibility of\nother code or configuration, not the vault package.\n\nThe manifest now has an __name__ which is the string of its index. This is\nof very limited usefulness, but with the right traverser might still allow\nitems in the held container to be traversed to.\n\n >>> i.manifest.__name__\n u'0'\n\nAfter every commit, the vault should be able to determine the previous and\nnext versions of every relationship. Since this is the first commit, previous\nwill be None, but we'll check it now anyway, building a function that checks\nthe most recent manifest of the vault.\n\n >>> def checkManifest(m):\n ... v = m.vault\n ... for r in m:\n ... p = v.getPrevious(r)\n ... assert (p is None or\n ... r.__parent__.vault is not v or\n ... p.__parent__.vault is not v or\n ... v.getNext(p) is r)\n ...\n >>> checkManifest(v.manifest)\n\nCreating a new working inventory requires a new manifest, based on the old\nmanifest.\n\nFor better or worse, the package offers four approaches to this. We\ncan create a new working inventory by specifying a vault, from which\nthe most recent manifest will be selected, and \"mutable=True\";\n\n >>> i = Inventory(vault=v, mutable=True)\n >>> manifest = i.manifest\n >>> manifest._z_frozen\n False\n\nby specifying an inventory, from which its manifest will be\nextracted, and \"mutable=True\";\n\n >>> i = Inventory(inventory=v.inventory, mutable=True)\n >>> manifest = i.manifest\n >>> manifest._z_frozen\n False\n\nby specifying a versioned manifest and \"mutable=True\";\n\n >>> i = Inventory(v.manifest, mutable=True)\n >>> manifest = i.manifest\n >>> manifest._z_frozen\n False\n\nor by specifying a mutable manifest.\n\n >>> i = Inventory(Manifest(v.manifest))\n >>> i.manifest._z_frozen\n False\n\nThese multiple spellings should be reexamined at a later date, and may have\na deprecation period. The last spelling--an explicit pasing of a manifest to\nan inventory--is the most likely to remain stable, because it clearly allows\ninstantiation of the inventory wrapper for a working manifest or a versioned\nmanifest.\n\nNote that, as mentioned above, the inventory is just an API wrapper around the\nmanifest: therefore, changes to inventories that share a manifest will be\nshared among them.\n\n >>> i_extra = Inventory(i.manifest)\n >>> manifest._z_frozen\n False\n\nIn any case, we now have an inventory that has the same contents as the\noriginal.\n\n >>> i.contents.keys() == v.inventory.contents.keys()\n True\n >>> i.contents['barbara'] is v.inventory.contents['barbara']\n True\n >>> i.contents['abe'] is v.inventory.contents['abe']\n True\n >>> i.contents['donald'] is v.inventory.contents['donald']\n True\n >>> i.contents('abe')['catherine'] is v.inventory.contents('abe')['catherine']\n True\n >>> i.contents('abe')('catherine')['anna'] is \\\n ... v.inventory.contents('abe')('catherine')['anna']\n True\n\nWe can now manipulate the new inventory as we did the old one.\n\n >>> app['d2'] = Demo()\n >>> i.contents['donald'] = app['d2']\n >>> i.contents['donald'] is v.inventory.contents['donald']\n False\n\nNow our hierarchy looks like this::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nNow we can observe our local changes. One way to do this is to examine\nthe results of iterChangedItems.\n\n >>> len(list(i.iterChangedItems()))\n 1\n >>> iter(i.iterChangedItems()).next() == i.contents('donald')\n True\n\nAnother is to look at each inventory item. The items specify the type of\ninformation in the item: whether it is from the 'base', the 'local' changes,\nor a few other options we'll see when we examine merges.\n\n >>> i.contents('abe').type\n 'base'\n >>> i.contents('donald').type\n 'local'\n\nThis will be true whether or not the change is returned to the original value\nby hand.\n\n >>> i.contents['donald'] = app['d1']\n >>> v.inventory.contents['donald'] is i.contents['donald']\n True\n\nHowever, unchanged local copies are not included in the iterChangedItems\nresults; they are also discarded on commit, as we will see below.\n\n >>> len(list(i.iterChangedItems()))\n 0\n\nNow our hierarchy looks like this again::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nEach inventory item represents a single collection of data that stores an\nobject and its effective hierarchy. Therefore, changing either (or both) will\ngenerate a local inventory item.\n\n >>> app['e1'] = Demo()\n >>> i.contents('barbara').type\n 'base'\n >>> i.contents('barbara')['edna'] = app['e1']\n >>> i.contents('barbara').type\n 'local'\n >>> i.contents['barbara'] is v.inventory.contents['barbara']\n True\n >>> len(list(i.iterChangedItems()))\n 2\n\nThose are two changes: one new node (edna) and one changed node (barbara got a\nnew child).\n\nNow our hierarchy looks like this (\"*\" indicates a changed node)::\n\n (top node)\n / | \\\n / | \\\n 'barbara'* 'abe' 'donald'\n \n / |\n / |\n 'edna'* 'catherine'\n \n |\n |\n 'anna'\n \n\nModifying the collection of the top level contents means that we have a change\nas well: even though the inventory does not keep track of a single object at\nthe top of the hierarchy, it does keep track of containment at the top level.\n\n >>> i.contents.type\n 'base'\n >>> app['f1'] = Demo()\n >>> i.contents['fred'] = app['f1']\n >>> i.contents.type\n 'local'\n >>> len(list(i.iterChangedItems()))\n 4\n\nThat's four changes: edna, barbara, fred, and the top node.\n\nNow our hierarchy looks like this (\"*\" indicates a changed or new node)::\n\n (top node)*\n / / \\ \\\n ---- / \\ ---------\n / | | \\\n 'barbara'* 'abe' 'donald' 'fred'*\n \n / |\n / |\n 'edna'* 'catherine'\n \n |\n |\n 'anna'\n \n\nYou can actually examine the base from the changed item--and even switch back.\nThe `base_item` attribute always returns an item with the original object and\ncontainment. The `local_item` returns an item with local changes, or None if\nno changes have been made. A `select` method allows you to switch the given\nitem to look at one or the other by default. The readonly `selected`\nattribute allows introspection.\n\n >>> list(i.contents.keys())\n ['barbara', 'abe', 'donald', 'fred']\n >>> i.contents == i.contents.local_item\n True\n >>> list(i.contents('barbara').keys())\n ['edna']\n >>> i.contents('barbara') == i.contents('barbara').local_item\n True\n >>> i.contents('barbara').local_item.selected\n True\n >>> i.contents('barbara').base_item.selected\n False\n >>> len(i.contents('barbara').base_item.keys())\n 0\n >>> list(i.contents.base_item.keys())\n ['barbara', 'abe', 'donald']\n >>> i.contents('barbara').base_item.select()\n >>> len(list(i.iterChangedItems()))\n 3\n\nThat's fred, the top level, /and/ edna: edna still is a change, even though\nshe is inaccessible with the old version of barbara. If we were to commit now,\nwe would have to resolve the orphan, as shown above.\n\n >>> v.commit(i) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ConflictError: \n >>> list(item.object for item in i.iterOrphanConflicts())\n []\n\nLet's look around a little more and switch things back:\n\n >>> i.contents('barbara').local_item.selected\n False\n >>> i.contents('barbara').base_item.selected\n True\n >>> len(i.contents('barbara').keys())\n 0\n >>> i.contents('barbara') == i.contents('barbara').local_item\n False\n >>> i.contents('barbara') == i.contents('barbara').base_item\n True\n >>> i.contents('barbara').local_item.select()\n >>> len(list(i.iterChangedItems()))\n 4\n >>> i.contents('barbara').local_item.selected\n True\n >>> i.contents('barbara').base_item.selected\n False\n >>> list(i.contents('barbara').keys())\n ['edna']\n\nThe inventory has booleans to examine whether a base item or local item exists,\nas a convenience (and optimization opportunity).\n\n >>> i.contents('fred').has_local\n True\n >>> i.contents('fred').has_base\n False\n >>> i.contents('abe')('catherine').has_local\n False\n >>> i.contents('abe')('catherine').has_base\n True\n >>> i.contents('barbara').has_local\n True\n >>> i.contents('barbara').has_base\n True\n\nIt also has four other similar properties, `has_updated`, `has_suggested`,\n`has_modified`, and `has_merged`, which we will examine later.\n\nBefore we commit we are going to make one more change to the inventory. We'll\nmake a change to \"anna\". Notice how we spell this in the code: it this is the\nfirst object we have put in an inventory that does not already have a location\nin app. When an inventory is asked to version an object without an ILocation,\nit stores it in a special folder on the manifest named \"held\". Held objects\nare assigned names using the standard Zope 3 name chooser pattern and can be\nmoved out even after being versioned. In this case we will need to register a\nname chooser for our demo objects. We'll use the standard one.\n\n >>> from zope.app.container.contained import NameChooser\n >>> from zope.app.container.interfaces import IWriteContainer\n >>> component.provideAdapter(NameChooser, adapts=(IWriteContainer,))\n >>> len(i.manifest.held)\n 0\n >>> i.contents('abe')('catherine')['anna'] = Demo()\n >>> len(i.manifest.held)\n 1\n >>> i.manifest.held.values()[0] is i.contents('abe')('catherine')['anna']\n True\n\nNow our hierarchy looks like this (\"*\" indicates a changed or new node)::\n\n (top node)*\n / / \\ \\\n ---- / \\ ---------\n / | | \\\n 'barbara'* 'abe' 'donald' 'fred'*\n \n / |\n / |\n 'edna'* 'catherine'\n \n |\n |\n 'anna'*\n \n\nIn our previous inventory commit, objects were versioned in place. The vault\ncode provides a hook to generate objects for committing to vault: it tries to\nadapt objects it wants to version to zc.vault.interfaces.IVersionFactory.\nThis interface specifies any callable object. Let's provide an example.\n\nThe policy here is that if the object is in the inventories' held container,\njust return it, but otherwise \"make a copy\"--which for our demo just makes a\nnew instance and slams the old one's name on it as an attribute.\n\n >>> @interface.implementer(interfaces.IVersionFactory)\n ... @component.adapter(interfaces.IVault)\n ... def versionFactory(vault):\n ... def makeVersion(obj, manifest):\n ... if obj.__parent__ is manifest.held:\n ... return obj\n ... res = Demo()\n ... res.source_name = obj.__name__\n ... return res\n ... return makeVersion\n ...\n >>> component.provideAdapter(versionFactory)\n\nLet's commit now, to show the results. We'll discard the change to barbara.\n\n >>> len(list(i.iterChangedItems()))\n 5\n >>> i.contents('barbara')('edna').resolveOrphanConflict()\n >>> i.contents('barbara').base_item.select()\n >>> len(list(i.iterChangedItems()))\n 4\n\nEdna is included even though she is resolved.\n\nNow our hierarchy looks like this (\"*\" indicates a changed or new node)::\n\n (top node)*\n / / \\ \\\n ---- / \\ ---------\n / | | \\\n 'barbara' 'abe' 'donald' 'fred'*\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'*\n \n\n >>> changed = dict(\n ... (getattr(item, 'name', None), item)\n ... for item in i.iterChangedItems())\n >>> changed['anna'].parent.name\n 'catherine'\n >>> changed['fred'].object\n \n >>> changed['edna'].object\n \n >>> list(changed[None].keys())\n ['barbara', 'abe', 'donald', 'fred']\n >>> old_objects = dict(\n ... (k, i.object) for k, i in changed.items() if k is not None)\n >>> v.commit(i)\n >>> checkManifest(v.manifest)\n >>> len(v)\n 2\n >>> v.manifest is i.manifest\n True\n >>> v.inventory == i\n True\n\nWe committed the addition of fred, but not the addition of edna. Once an\ninventory is committed, unselected changes are discarded. Also, as mentioned\nabove, the data for local item for `donald` has been discarded, since it did\nnot include any changes.\n\n >>> i.contents.local_item == i.contents\n True\n >>> i.contents.type\n 'local'\n >>> i.contents('barbara').local_item # None\n >>> i.contents('barbara').type\n 'base'\n >>> i.contents('donald').local_item # None\n >>> i.contents('donald').type\n 'base'\n >>> IFreezing(app['e1'])._z_frozen\n False\n\nOur changes are a bit different than what we had when we began the commit,\nbecause of the version Factory. The f1 is not versioned, because we have made\na copy instead.\n\n >>> IFreezing(app['f1'])._z_frozen\n False\n >>> new_changed = dict(\n ... (getattr(item, 'name', None), item)\n ... for item in i.iterChangedItems())\n >>> new_changed['anna'].parent.name\n 'catherine'\n >>> new_changed['anna'].object is old_objects['anna']\n True\n >>> new_changed['fred'].object is old_objects['fred']\n False\n >>> new_changed['fred'].object is app['f1']\n False\n >>> new_changed['fred'].object.source_name\n u'f1'\n >>> IFreezing(new_changed['anna'].object)._z_frozen\n True\n >>> IFreezing(new_changed['fred'].object)._z_frozen\n True\n\nNow that we have two versions in the vault, we can introduce two\nadditional attributes of the inventories, contents, and items: `next` and\n`previous`. These attributes let you time travel in the vault's history.\n\nWe also look at similar attributes on the manifest, and at the vault's\n`getInventory` method.\n\nFor instance, the current inventory's `previous` attribute points to the\noriginal inventory, and vice versa.\n\n >>> i.previous == v.getInventory(0)\n True\n >>> i.manifest.previous is v[0]\n True\n >>> v.getInventory(0).next == i == v.inventory\n True\n >>> v[0].next is i.manifest is v.manifest\n True\n >>> i.next # None\n >>> manifest.next # None\n >>> v.getInventory(0).previous # None\n >>> v[0].previous # None\n\nThe same is true for inventory items.\n\n >>> list(v.inventory.contents.previous.keys())\n ['barbara', 'abe', 'donald']\n >>> list(v.getInventory(0).contents.next.keys())\n ['barbara', 'abe', 'donald', 'fred']\n >>> v.inventory.contents.previous.next == v.inventory.contents\n True\n >>> v.inventory.contents('abe')('catherine')('anna').previous.object\n \n >>> (v.inventory.contents('abe').relationship is\n ... v.inventory.contents.previous('abe').relationship)\n True\n\nOnce you step to a previous or next item, further steps from the item remain\nin the previous or next inventory.\n\n >>> v.inventory.contents('abe')('catherine')['anna'].__name__ == 'a1'\n False\n >>> v.inventory.contents.previous('abe')('catherine')['anna']\n \n\nIn addition, inventory items support `previous_version` and `next_version`.\nThe difference between these and `previous` and `next` is that the `*_version`\nvariants skip to the item that was different than the current item. For\ninstance, while the previous_version of the 'anna' is the old 'a1' object,\njust like the `previous` value, the previous_version of 'abe' is None, because\nit has no previous version.\n\n >>> v.inventory.contents(\n ... 'abe')('catherine')('anna').previous_version.object\n \n >>> v.inventory.contents('abe').previous_version # None\n\nThese leverage the `getPrevious` and `getNext` methods on the vault, which work\nwith relationships.\n\nThe previous and next tools are even more interesting when tokens move: you\ncan see positions change within the hierarchy. Inventories have a `moveTo`\nmethod that can let the inventory follow the moves to maintain history. We'll\ncreate a new inventory copy and demonstrate. As we do, notice that\ninventory items obtained before the move correctly reflect the move, as\ndescribed above.\n\n >>> manifest = Manifest(v.manifest)\n >>> del app['inventory']\n >>> i = app['inventory'] = Inventory(manifest)\n >>> item = i.contents('abe')('catherine')\n >>> item.parent.name\n 'abe'\n >>> i.contents('abe')('catherine').moveTo(i.contents('fred'))\n >>> item.parent.name\n 'fred'\n >>> len(i.contents('abe').keys())\n 0\n >>> list(i.contents('fred').keys())\n ['catherine']\n\nThe change actually only affects the source and target of the move.\n\n >>> changes = dict((getattr(item, 'name'), item)\n ... for item in i.iterChangedItems())\n >>> len(changes)\n 2\n >>> changes['fred'].values()\n []\n >>> len(changes['abe'].keys())\n 0\n\nSo now our hierarchy looks like this (\"*\" indicates a changed node)::\n\n (top node)\n / / \\ \\\n ---- / \\ ---------\n / | | \\\n 'barbara' 'abe'* 'donald' 'fred'*\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nIf you try to move parts of the hierarchy to someplace that has the same name,\nyou will receive a ValueError unless you specify a name that does not\nconflict.\n\n >>> i.contents('abe')['donald'] = app['d2']\n >>> i.contents('donald').moveTo(i.contents('abe'))\n Traceback (most recent call last):\n ...\n ValueError: Object with same name already exists in new location\n >>> i.contents('donald').moveTo(i.contents('abe'), 'old_donald')\n >>> i.contents('abe').items()\n [('donald', ), ('old_donald', )]\n\nNow our hierarchy looks like this (\"*\" indicates a changed or new node)::\n\n (top node)*\n / | \\\n ---- | ----\n / | \\\n 'barbara' 'abe'* 'fred'*\n \n / \\ |\n / \\ |\n 'donald'* 'old_donald' 'catherine'\n \n |\n |\n 'anna'\n \n\nIf you try to move part of the hierarchy to someplace within itself, you will\nalso receive a ValueError.\n\n >>> i.contents('fred').moveTo(i.contents('fred')('catherine')('anna'))\n Traceback (most recent call last):\n ...\n ValueError: May not move item to within itself\n\nIt is for this reason that the contents does not support the moveTo operation.\n\n >>> hasattr(i.contents, 'moveTo')\n False\n\nIf you move an object to the same folder it is a silent noop, unless you are\nusing the move as a rename operation and the new name conflicts.\n\n >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'))\n >>> i.contents('abe').items()\n [('donald', ), ('old_donald', )]\n >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'), 'donald')\n Traceback (most recent call last):\n ...\n ValueError: Object with same name already exists in new location\n >>> i.contents('abe').items()\n [('donald', ), ('old_donald', )]\n >>> i.contents('abe')('donald').moveTo(i.contents('abe'),\n ... 'new_donald')\n >>> i.contents('abe').items()\n [('old_donald', ), ('new_donald', )]\n\nNotice in the last part of the example above that the move within the folder\nalso changed the order.\n\nIt's also interesting to note that, with all these changes, we only have two\nadditional changed items: the addition of new_donald, and the changed\ncontainment of the contents. old_donald, for instance, is not considered to\nbe changed; only its containers were.\n\n >>> changes = dict((getattr(item, 'name', None), item)\n ... for item in i.iterChangedItems())\n >>> len(changes)\n 4\n >>> changes['fred'].items()\n [('catherine', )]\n >>> changes['abe'].items()\n [('old_donald', ), ('new_donald', )]\n >>> changes['new_donald'].object\n \n >>> list(changes[None].keys())\n ['barbara', 'abe', 'fred']\n\nNow that we have moved some objects that existed in previous inventories--\ncatherine (containing anna) was moved from abe to fred, and donald was moved\nfrom the root contents to abe and renamed to 'old_donald'--we can examine\nthe previous and previous_version pointers.\n\n >>> i.contents('abe')('old_donald').previous.parent == i.previous.contents\n True\n >>> i.contents('abe')('old_donald').previous_version # None\n\nThe previous_version is None because, as seen in the iterChangedItems example,\ndonald didn't actually change--only its containers did. previous_version does\nwork for both local changes and changes in earlier inventories, though.\n\n >>> list(i.contents('abe').keys())\n ['old_donald', 'new_donald']\n >>> list(i.contents('abe').previous.keys())\n ['catherine']\n >>> (i.contents('fred')('catherine')('anna').previous.inventory ==\n ... v.inventory)\n True\n >>> (i.contents('fred')('catherine')('anna').previous_version.inventory ==\n ... v.getInventory(0))\n True\n\nThe previous_version of anna is the first one that was committed in the\ninitial inventory--it didn't change in this version, but in the most recently\ncommitted inventory, so the previous version is the very first one committed.\n\nBy the way, notice that, while previous and previous_version point to the\ninventories from which the given item came, the historical, versioned\ninventories in the vault don't point to this working inventory in next or\nnext_version because this inventory has not been committed yet.\n\n >>> v.inventory.contents('abe').next # None\n >>> v.inventory.contents('abe').next_version # None\n\nAs mentioned above, only inventory items support `moveTo`, not the top-node\ninventory contents. Both contents and inventory items support a `copyTo`\nmethod. This is similar to moveTo but it creates new additional locations in\nthe inventory for the same objects; the new locations don't maintain any\nhistory. It is largely a short hand for doing \"location1['foo'] =\nlocation2['foo']\" for all objects in a part of the inventory. The only\ndifference is when copying between inventories, as we will see below.\n\nThe basic `copyTo` machinery is very similar to `moveTo`. We'll first copy\ncatherine and anna to within the contents.\n\n >>> i.contents('fred')('catherine').copyTo(i.contents)\n >>> list(i.contents.keys())\n ['barbara', 'abe', 'fred', 'catherine']\n >>> list(i.contents('catherine').keys())\n ['anna']\n >>> i.contents['catherine'] is i.contents('fred')['catherine']\n True\n >>> (i.contents('catherine')('anna').object is\n ... i.contents('fred')('catherine')('anna').object)\n True\n\nNow our hierarchy looks like this (\"*\" indicates a changed or new node)::\n\n (top node)*\n --------/ / \\ \\-----------\n / / \\ \\\n / / \\ \\\n 'barbara' 'abe'* 'fred'* 'catherine'*\n \n / \\ | |\n / \\ | |\n 'new_donald'* 'old_donald' 'catherine' 'anna'*\n \n |\n |\n 'anna'\n \n\nNow we have copied objects from one location to another. The copies are unlike\nthe originals because they do not have any history.\n\n >>> i.contents('fred')('catherine')('anna').previous is None\n False\n >>> i.contents('catherine')('anna').previous is None\n True\n\nHowever, they do know their copy source.\n\n >>> (i.contents('catherine')('anna').copy_source ==\n ... i.contents('fred')('catherine')('anna'))\n True\n\nAs with `moveTo`, you may not override a name, but you may explicitly provide\none.\n\n >>> i.contents['anna'] = Demo()\n >>> i.contents('catherine')('anna').copyTo(i.contents)\n Traceback (most recent call last):\n ...\n ValueError: Object with same name already exists in new location\n >>> i.contents('catherine')('anna').copyTo(i.contents, 'old_anna')\n >>> list(i.contents.keys())\n ['barbara', 'abe', 'fred', 'catherine', 'anna', 'old_anna']\n >>> del i.contents['anna']\n >>> del i.contents['old_anna']\n\nUnlike with `moveTo`, if you try to copy a part of the hierarchy on top of\nitself (same location, same name), the inventory will raise an error.\n\n >>> i.contents('catherine')('anna').copyTo(i.contents('catherine'))\n Traceback (most recent call last):\n ...\n ValueError: Object with same name already exists in new location\n\nYou can actually copyTo a location in a completely different inventory, even\nfrom a separate vault.\n\n >>> another = app['another'] = Vault()\n >>> another_i = app['another_i'] = Inventory(vault=another)\n >>> len(another_i.contents)\n 0\n >>> i.contents('abe').copyTo(another_i.contents)\n >>> another_i.contents['abe']\n \n >>> another_i.contents('abe')['new_donald']\n \n >>> another_i.contents('abe')['old_donald']\n \n\nWe haven't committed for awhile, so let's commit this third revision. We did\na lot of deletes, so let's just accept all of the orphan conflicts.\n\n >>> for item in i.iterOrphanConflicts():\n ... item.resolveOrphanConflict()\n ...\n >>> v.commit(i)\n >>> checkManifest(v.manifest)\n\nIn a future revision of the zc.vault package, it may be possible to move and\ncopy between inventories. At the time of writing, this use case is\nunnecessary, and doing so will have unspecified behavior.\n\n.. topic:: A test for a subtle bug in revision <= 78553\n\n One important case, at least for the regression testing is an\n attempt to rename an item after the vault has been frozen.\n Since we have just committed, this is the right time to try that.\n Let's create a local copy of an inventory and try to rename some\n items on it.\n\n >>> v.manifest._z_frozen\n True\n >>> l = Inventory(Manifest(v.manifest))\n >>> l.manifest._z_frozen\n False\n >>> l.contents('abe').items()\n [('old_donald', ), ('new_donald', )]\n >>> l.contents('abe')('old_donald').moveTo(l.contents('abe'), 'bob')\n >>> l.contents('abe')('new_donald').moveTo(l.contents('abe'), 'donald')\n >>> l.contents('abe').items()\n [('bob', ), ('donald', )]\n\n\nWe have now discussed the core API for the vault system for basic use. A\nnumber of other use cases are important, however:\n\n- revert to an older inventory;\n\n- merge concurrent changes;\n\n- track an object in a vault; and\n\n- traverse through a vault using URL or TALES paths.\n\nReverting to an older inventory is fairly simple: use the 'commitFrom'\nmethod to copy and commit an older version into a new copy. The same\nworks with manifests.\n\n >>> v.commitFrom(v[0])\n\nThe data is now as it was in the old version.\n\n >>> list(v.inventory.contents.keys())\n ['barbara', 'abe', 'donald']\n\nNow our hierarchy looks like this again::\n\n (top node)\n / | \\\n / | \\\n 'barbara' 'abe' 'donald'\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nThe `commitFrom` method will take any committed manifest from a vault that\nshares the same intids utility. It creates a new manifest that duplicates the\nprovided one.\n\n >>> v.inventory.contents('abe')('catherine').previous.parent.name\n 'fred'\n >>> v.manifest.previous is v[-2]\n True\n >>> v.manifest.base_source is v[-2]\n True\n >>> v.manifest.base_source is v[0]\n False\n >>> v[-2].base_source is v[-3]\n True\n\nNote that this approach will cause an error:\n\n >>> v.commit(Manifest(v[0])) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n OutOfDateError: \n\nAgain, use `commitFrom` to revert.\n\nNow we come to the most complex vault use case: concurrent changes to a vault,\nmerging inventories. The vault design supports a number of features for these\nsorts of use cases.\n\nThe basic merge story is that if one or more commits happen to a vault while\nan inventory from the vault is being worked on, so that the base of a working\ninventory is no longer the most recent committed inventory, and thus cannot\nbe committed normally...\n\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> long_running.manifest.base_source is v.manifest\n True\n >>> short_running.contents['donald'] = app['d2']\n >>> short_running.contents.items()\n [('barbara', ), ('abe', ), ('donald', )]\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> short_running.contents('barbara')['fred'] = app['f1']\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> long_running.manifest.base_source is v.manifest\n False\n >>> long_running.manifest.base_source is v.manifest.previous.previous\n True\n >>> long_running.contents['edna'] = app['e1']\n >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE\n [('barbara', ), ('abe', ),\n ('donald', ), ('edna', )]\n >>> v.commit(long_running) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n OutOfDateError: \n\n...then the inventory can be updated; and, if there are no problems with the\nupdate, then the inventory can be committed.\n\nshort_running, and the head of the vault, looks like this now (\"*\" indicates a\nchange from the previous version)::\n\n (top node)\n / | \\\n / | \\\n 'barbara'* 'abe' 'donald'*\n \n | |\n | |\n 'fred'* 'catherine'\n \n |\n |\n 'anna'\n \n\nlong_running looks like this::\n\n (top node)*\n ------/ / \\ \\----------\n / / \\ \\\n 'barbara' 'abe' 'donald' 'edna'*\n \n |\n |\n 'catherine'\n \n |\n |\n 'anna'\n \n\nThe contents node changed and 'edna' was added.\n\nBy default, an update is to the current inventory of the inventory base's vault.\n\nHere's the update. It will produce no conflicts, because the node changes do\nnot overlap (review diagrams above).\n\n >>> long_running.beginUpdate()\n >>> long_running.updating\n True\n\nPost-merge, long_running looks like this ('M' indicates a merged node)::\n\n (top node)*\n ------/ / \\ \\----------\n / / \\ \\\n 'barbara'M 'abe' 'donald'M 'edna'*\n \n | |\n | |\n 'fred'M 'catherine'\n \n |\n |\n 'anna'\n \n\n(ADVANCED)\n\nDuring an update, the local relationships may not be changed, even though they\nare not versioned.\n\n >>> long_running.contents('edna').type\n 'local'\n >>> long_running.contents('edna').relationship.object = Demo()\n Traceback (most recent call last):\n ...\n UpdateError: cannot change local relationships while updating\n >>> long_running.contents('edna').relationship.object\n \n >>> long_running.contents('edna').relationship._z_frozen\n False\n >>> long_running.manifest.getType(long_running.contents.relationship)\n 'local'\n >>> long_running.contents.relationship.containment.updateOrder(\n ... ('abe', 'barbara', 'edna', 'donald'))\n Traceback (most recent call last):\n ...\n UpdateError: cannot change local relationships while updating\n >>> long_running.contents.relationship.containment.keys()\n ('barbara', 'abe', 'donald', 'edna')\n\nWhen you change an item or contents, this is hidden by switching to a MODIFIED\nrelationship, as seen below.\n\n(end ADVANCED)\n\nNow that we have updated, our `update_source` on the inventory shows the\ninventory used to do the update.\n\n >>> long_running.manifest.base_source is v[-3]\n True\n >>> long_running.manifest.update_source is short_running.manifest\n True\n\nWhat changes should the update reflect? iterChangedItems takes an optional\nargument which can use an alternate base to calculate changes, so we can use\nthat with the long_running.base to see the effective merges.\n\n >>> changed = dict((getattr(item, 'name', None), item) for item in\n ... short_running.iterChangedItems(\n ... long_running.manifest.base_source))\n >>> changed['donald'].object.source_name\n u'd2'\n >>> changed['fred'].object.source_name\n u'f1'\n >>> list(changed['barbara'].keys())\n ['fred']\n\nOur contents show these merged results.\n\n >>> list(long_running.contents.keys())\n ['barbara', 'abe', 'donald', 'edna']\n >>> long_running.contents['donald'].source_name\n u'd2'\n >>> long_running.contents('barbara')['fred'].source_name\n u'f1'\n\nYou cannot update to another inventory until you `abortUpdate` or\n`completeUpdate`, as we discuss far below.\n\n >>> long_running.beginUpdate(v[-2])\n Traceback (most recent call last):\n ...\n UpdateError: cannot begin another update while updating\n\nWe'll show `abortUpdate`, then redo the update. A characteristic of\nabortUpdate is that it should revert all changes you made while updating. For\ninstance, we'll select another version of the contents and even add an item.\nThe changes will all go away when we abort.\n\n >>> len(list(long_running.iterChangedItems()))\n 5\n >>> long_running.contents['fred'] = app['f1']\n >>> list(long_running.contents.keys())\n ['barbara', 'abe', 'donald', 'edna', 'fred']\n >>> len(list(long_running.iterChangedItems()))\n 6\n >>> long_running.abortUpdate()\n >>> long_running.manifest.update_source # None\n >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE\n [('barbara', ), ('abe', ),\n ('donald', ), ('edna', )]\n >>> len(list(long_running.iterChangedItems()))\n 2\n >>> long_running.beginUpdate()\n >>> list(long_running.contents.keys())\n ['barbara', 'abe', 'donald', 'edna']\n >>> long_running.contents['donald'].source_name\n u'd2'\n >>> long_running.contents('barbara')['fred'].source_name\n u'f1'\n\nNow we'll look around more at the state of things. We can use\niterChangedItems to get a list of all changed and updated. As already seen in\nthe examples, `update_source` on the inventory shows the inventory used to do\nthe update.\n\n >>> updated = {}\n >>> changed = {}\n >>> for item in long_running.iterChangedItems():\n ... name = getattr(item, 'name', None)\n ... if item.type == interfaces.LOCAL:\n ... changed[name] = item\n ... else:\n ... assert item.type == interfaces.UPDATED\n ... updated[name] = item\n ...\n >>> len(updated)\n 3\n >>> updated['donald'].object.source_name\n u'd2'\n >>> updated['fred'].object.source_name\n u'f1'\n >>> list(updated['barbara'].keys())\n ['fred']\n >>> len(changed)\n 2\n >>> list(changed[None].keys())\n ['barbara', 'abe', 'donald', 'edna']\n >>> changed['edna'].object\n \n\nThe `has_updated` and `updated_item` attributes, which only come into effect\nwhen an inventory is in the middle of an update, let you examine the changes\nfrom a more local perspective.\n\n >>> long_running.contents('donald').has_local\n False\n >>> long_running.contents('donald').has_updated\n True\n >>> (long_running.contents('donald').updated_item.relationship is\n ... long_running.contents('donald').relationship)\n True\n\nThere are three kinds of problems that can prevent a post-merge commit: item\nconflicts, orphans, and parent conflicts. Item conflicts are item updates\nthat conflicted with local changes and that the system could not merge (more\non that below). Orphans are accepted item changes (local or updated) that are\nnot accessible from the top contents, and so will be lost. Parent conflicts\nare items that were moved to one location in the source and another location\nin the local changes, and so now have two parents: an illegal state because it\nmakes future merges and sane historical analysis difficult.\n\nThese three kinds of problem can be analyzed with\n`iterUpdateConflicts`, `iterOrphanConflicts`, and `iterParentConflicts`,\nrespectively. We have already seen iterOrphanConflicts. In our current merge,\nwe have none of these problems, and we can commit (or completeUpdate)\nsuccessfully.\n\n >>> list(long_running.iterUpdateConflicts())\n []\n >>> list(long_running.iterOrphanConflicts())\n []\n >>> list(long_running.iterParentConflicts())\n []\n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nWe had a lot of discussion between the most important points here, so to\nreview, all we had to do in the simple case was this::\n\n long_running.beginUpdate()\n v.commit(long_running)\n\nWe could have rejected some of the updates and local changes, which might\nhave made things more interesting; and the two steps let you analyze the update\nchanges to tweak things as desired. But the simplest case allows a simple\nspelling.\n\nNow let's explore the possible merging problems. The first, and arguably most\ncomplex, is item conflict. An item conflict is easy to provoke. We can do it\nby manipulating the containment or the object of an item. Here we'll\nmanipulate the containment order of the root.\n\n >>> list(v.inventory.contents.keys())\n ['barbara', 'abe', 'donald', 'edna']\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> short_running.contents.updateOrder(\n ... ('abe', 'barbara', 'edna', 'donald'))\n >>> long_running.contents.updateOrder(\n ... ('abe', 'barbara', 'donald', 'edna'))\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> long_running.beginUpdate()\n >>> v.commit(long_running)\n Traceback (most recent call last):\n ...\n UpdateError: cannot complete update with conflicts\n >>> conflicts = list(long_running.iterUpdateConflicts())\n >>> len(conflicts)\n 1\n >>> conflict = conflicts[0]\n >>> conflict.type\n 'local'\n >>> list(conflict.keys())\n ['abe', 'barbara', 'donald', 'edna']\n >>> conflict.is_update_conflict\n True\n >>> conflict.selected\n True\n >>> conflict.has_updated\n True\n >>> list(conflict.updated_item.keys())\n ['abe', 'barbara', 'edna', 'donald']\n\nAs you can see, we have the tools to find out the conflicts and examine them.\nTo resolve this conflict, we merely need to use the `resolveUpdateConflict`\nmethod. We can select the desired one we want, or even create a new one and\nmodify it, before or after marking it resolved.\n\nLet's create a new one. All you have to do is start changing the item, and a\nnew one is created. You are not allowed to directly modify local changes when\nyou are updating, so that the system can revert to them; but you may create\n'modified' versions (that will be discarded if the update is aborted).\n\n >>> len(list(conflict.iterModifiedItems()))\n 0\n >>> conflict.has_modified\n False\n >>> conflict.selected\n True\n >>> conflict.type\n 'local'\n >>> list(conflict.keys())\n ['abe', 'barbara', 'donald', 'edna']\n >>> conflict.updateOrder(['abe', 'donald', 'barbara', 'edna'])\n >>> len(list(conflict.iterModifiedItems()))\n 1\n >>> conflict.has_modified\n True\n >>> conflict.selected\n True\n >>> conflict.type\n 'modified'\n >>> conflict.copy_source.type\n 'local'\n >>> conflict.copy_source == conflict.local_item\n True\n >>> conflict == list(conflict.iterModifiedItems())[0]\n True\n >>> list(conflict.local_item.keys())\n ['abe', 'barbara', 'donald', 'edna']\n >>> list(conflict.keys())\n ['abe', 'donald', 'barbara', 'edna']\n >>> list(conflict.updated_item.keys())\n ['abe', 'barbara', 'edna', 'donald']\n\nNow we're going to resolve it.\n\n >>> conflict.resolveUpdateConflict()\n >>> conflict.is_update_conflict\n False\n >>> len(list(long_running.iterUpdateConflicts()))\n 0\n >>> resolved = list(long_running.iterUpdateResolutions())\n >>> len(resolved)\n 1\n >>> resolved[0] == conflict\n True\n\nNow if we called abortUpdate, the local_item would look the way it did before\nthe update, because we modified a separate object. Let's commit, though.\n\n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nOur hierarchy looks like this now::\n\n (top node)*\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald'M 'barbara'M 'edna'*\n \n | |\n | |\n 'catherine' 'fred'M\n \n |\n |\n 'anna'\n \n\nThe vault code allows for adapters to try and suggest merges. For instance, a\nsimple merge might have a policy that one version with an object change and\nanother version with a containment change can be merged simply. This uses\nsome APIs we haven't talked about yet: if there is a core.txt in this\ndirectory, you're in luck; otherwise, hope for help in interfaces.py and\nbother Gary for docs (sorry).\n\n >>> from zc.vault.core import Relationship\n >>> @component.adapter(interfaces.IVault)\n ... @interface.implementer(interfaces.IConflictResolver)\n ... def factory(vault):\n ... def resolver(manifest, local, updated, base):\n ... if local.object is not base.object:\n ... if updated.object is base.object:\n ... object = local.object\n ... else:\n ... return\n ... else:\n ... object = updated.object\n ... if local.containment != base.containment:\n ... if updated.containment != base.containment:\n ... return\n ... else:\n ... containment = local.containment\n ... else:\n ... containment = updated.containment\n ... suggested = Relationship(local.token, object, containment)\n ... manifest.addSuggested(suggested)\n ... manifest.select(suggested)\n ... manifest.resolveUpdateConflict(local.token)\n ... return resolver\n ...\n >>> component.provideAdapter(factory)\n\nNow if we merge changes that this policy can handle, we'll have smooth updates.\n\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> app['c2'] = Demo()\n >>> short_running.contents('abe')['catherine'] = app['c2']\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> long_running.contents('abe')('catherine')['fred'] = app['f1']\n >>> long_running.beginUpdate()\n >>> cath = long_running.contents('abe')('catherine')\n >>> cath.has_suggested\n True\n >>> cath.type\n 'suggested'\n >>> cath.has_updated\n True\n >>> cath.selected\n True\n >>> cath.has_local\n True\n >>> suggestedItems = list(cath.iterSuggestedItems())\n >>> len(suggestedItems)\n 1\n >>> suggestedItems[0] == cath\n True\n >>> cath.object.source_name\n u'c2'\n >>> list(cath.keys())\n ['anna', 'fred']\n >>> cath.local_item.object\n \n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nThis means we automatically merged this... ::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n | |\n | |\n 'catherine'* 'fred'\n \n |\n |\n 'anna'\n \n\n...with this (that would normally produce a conflict with the 'catherine'\nnode)... ::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n | |\n | |\n 'catherine'* 'fred'\n \n / \\\n / \\\n 'anna' 'fred'*\n \n\n...to produce this::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n | |\n | |\n 'catherine'* 'fred'\n \n / \\\n / \\\n 'anna' 'fred'*\n \n\nThis concludes our tour of item conflicts. We are left with orphans and\nparent conflicts.\n\nAs mentioned above, orphans are accepted, changed items, typically from the\nupdate or local changes, that are inaccessible from the root of the inventory.\nFor example, consider the following.\n\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> list(short_running.contents('abe').keys())\n ['catherine']\n >>> list(short_running.contents('abe')('catherine').keys())\n ['anna', 'fred']\n >>> del short_running.contents('abe')['catherine']\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> long_running.contents('abe')('catherine')['anna'] = Demo()\n >>> long_running.beginUpdate()\n >>> v.commit(long_running)\n Traceback (most recent call last):\n ...\n UpdateError: cannot complete update with conflicts\n >>> orphans =list(long_running.iterOrphanConflicts())\n >>> len(orphans)\n 1\n >>> orphan = orphans[0]\n >>> orphan.parent.name\n 'catherine'\n >>> orphan.selected\n True\n >>> orphan.type\n 'local'\n >>> orphan.parent.selected\n True\n >>> orphan.parent.type\n 'base'\n >>> orphan.parent.parent.type\n 'base'\n >>> orphan.parent.parent.selected\n False\n >>> orphan.parent.parent.selected_item.type\n 'updated'\n\nTo reiterate in a diagram, the short_running inventory deleted the\n'catherine' branch::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n |\n |\n 'fred'\n \n\nHowever, the long running branch made a change to an object that had\nbeen removed ('anna')::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n | |\n | |\n 'catherine' 'fred'\n \n / \\\n / \\\n 'anna'* 'fred'\n \n\nSo, given the orphan, you can discover the old version of the node that let the\nchange occur, and thus the change that hid the orphan.\n\nTo resolve an orphan, as seen before, you can `resolveOrphanConflict`, or\nsomehow change the tree so that the orphan is within the tree again (using\n`moveTo`). We'll just resolve it. Note that resolving keeps it selected: it\njust stops the complaining.\n\n >>> orphan.selected\n True\n >>> orphan.resolveOrphanConflict()\n >>> orphan.selected\n True\n >>> len(list(long_running.iterOrphanConflicts()))\n 0\n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nThe same happens if the change occurs because of a reversal--the long_running\ninventory performs the delete.\n\nIt also can happen if the user explicitly selects a choice that eliminates an\naccepted change, even outside of a merge, as we have seen above.\n\nParent conflicts are the last sort of conflict.\n\nOur hierarchy now looks like this::\n\n (top node)\n ----------/ / \\ \\----------\n / / \\ \\\n 'abe' 'donald' 'barbara' 'edna'\n \n |\n |\n 'fred'\n \n\nThe short_running version will be changed to look like this::\n\n (top node)\n ------/ | \\-------\n / | \\\n 'abe' 'barbara'* 'edna'\n \n / \\\n / \\\n 'fred' 'donald'\n \n\nThe long_running version will look like this. ::\n\n (top node)\n ------/ | \\-------\n / | \\\n 'abe' 'barbara' 'edna'\n \n |\n |\n 'fred'*\n \n |\n |\n 'donald'\n \n\nPost-merge the tree looks like this::\n\n (top node)\n ------/ | \\-------\n / | \\\n 'abe' 'barbara'* 'edna'\n \n / \\\n / \\\n 'fred'* 'donald'\n \n |\n |\n 'donald'\n \n\nThe problem is Donald. It is one token in two or more places: a parent\nconflict.\n\n >>> short_running = Inventory(Manifest(v.manifest))\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> short_running.contents('donald').moveTo(\n ... short_running.contents('barbara'))\n >>> v.commit(short_running)\n >>> checkManifest(v.manifest)\n >>> long_running.contents('donald').moveTo(\n ... long_running.contents('barbara')('fred'))\n >>> long_running.beginUpdate()\n >>> conflicts = list(long_running.iterParentConflicts())\n >>> v.commit(long_running)\n Traceback (most recent call last):\n ...\n UpdateError: cannot complete update with conflicts\n >>> conflicts = list(long_running.iterParentConflicts())\n >>> len(conflicts)\n 1\n >>> conflict = conflicts[0]\n >>> conflict.name\n Traceback (most recent call last):\n ...\n ParentConflictError\n >>> conflict.parent\n Traceback (most recent call last):\n ...\n ParentConflictError\n >>> selected = list(conflict.iterSelectedParents())\n >>> len(selected)\n 2\n >>> sorted((s.type, s.name) for s in selected)\n [('local', 'fred'), ('updated', 'barbara')]\n >>> all = dict((s.type, s) for s in conflict.iterParents())\n >>> len(all)\n 3\n >>> sorted(all)\n ['base', 'local', 'updated']\n\nYou can provoke these just by accepting a previous version, outside of merges.\nFor instance, we can now make a three-way parent conflict by selecting the\nroot node.\n\n >>> all['base'].select()\n >>> selected = list(conflict.iterSelectedParents())\n >>> len(selected)\n 3\n\nNow if we resolve the original problem by rejecting the local change,\nwe'll still have a problem, because of accepting the baseParent.\n\n >>> all['local'].base_item.select()\n >>> selected = list(conflict.iterSelectedParents())\n >>> len(selected)\n 2\n >>> v.commit(long_running)\n Traceback (most recent call last):\n ...\n UpdateError: cannot complete update with conflicts\n >>> all['base'].local_item.select()\n >>> len(list(long_running.iterParentConflicts()))\n 0\n\nNow our hierarchy looks like short_running again::\n\n (top node)\n ------/ | \\-------\n / | \\\n 'abe' 'barbara' 'edna'\n \n / \\\n / \\\n 'fred' 'donald'\n \n\nWe can't check this in because there are no effective changes between this\nand the last checkin.\n\n >>> v.commit(long_running) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n NoChangesError: \n\nSo actually, we'll reinstate the local change, reject the short_running\nchange (the placement within barbara), and commit.\n\n >>> all['local'].select()\n >>> all['updated'].base_item.select()\n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nNote that even though we selected the base_item, the relationship generated by\ncompleting the update is actually local because it is a change from the\nprevious updated source.\n\n >>> v.inventory.contents('barbara').type\n 'local'\n\nThere is actually a fourth kind of error: having child nodes in selected\nrelationships for which there are no selected relationships. The code tries to\ndisallow this, so it should not be encountered.\n\nNext, we will talk about using vaults to create and manage branches.\nThe simple basics of this are that you can commit an inventory based on one\nvault into a fresh vault, and you can then update across the two vaults. To\ncreate a vault that can have merged manifests, you must share the internal\n'intids' attribute. The `createBranch` method is sugar for doing that and then\n(by default) committing the most recent manifest of the current vault as the first\nrevision of the branch.\n\n >>> branch = app['branch'] = v.createBranch()\n >>> bi = Inventory(Manifest(branch.manifest))\n >>> branch_start_inventory = v.inventory\n >>> bi.contents['george'] = Demo()\n >>> branch.commit(bi)\n >>> checkManifest(branch.manifest)\n >>> i = Inventory(Manifest(v.manifest))\n >>> i.contents['barbara'] = app['b2'] = Demo()\n >>> v.commit(i)\n >>> checkManifest(v.manifest)\n >>> i.contents['barbara'].source_name\n u'b2'\n >>> bi = Inventory(Manifest(branch.manifest))\n >>> bi.contents('barbara')['henry'] = app['h1'] = Demo()\n >>> branch.commit(bi)\n >>> checkManifest(branch.manifest)\n\nNow we want to merge the mainline changes with the branch.\n\n >>> bi = Inventory(Manifest(branch.manifest))\n >>> (bi.manifest.base_source is bi.manifest.getBaseSource(branch) is\n ... branch.manifest)\n True\n >>> (bi.manifest.getBaseSource(v) is branch_start_inventory.manifest is\n ... v[-2])\n True\n >>> bi.beginUpdate(v.inventory)\n >>> bi.contents['barbara'].source_name\n u'b2'\n >>> bi.contents('barbara')['henry'].source_name\n u'h1'\n\nA smooth update. But what happens if meanwhile someone changes the branch,\nbefore this is committed? We use `completeUpdate`, and then update again on\nthe branch. `completeUpdate` moves all selected changes to be `local`,\nwhatever the source, the same way commit does (in fact, commit uses\ncompleteUpdate).\n\n >>> bi2 = Inventory(Manifest(branch.manifest))\n >>> bi2.contents['edna'] = app['e2'] = Demo()\n >>> branch.commit(bi2)\n >>> checkManifest(branch.manifest)\n >>> branch.commit(bi) # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n OutOfDateError: \n >>> bi.completeUpdate()\n >>> bi.beginUpdate()\n >>> branch.commit(bi)\n >>> checkManifest(branch.manifest)\n\nOnce we have done this, the head of the branch is based on the head of the\noriginal vault, so we can immediately check in a branch inventory in the\ntrunk inventory.\n\n >>> v.commit(Inventory(Manifest(branch.manifest)))\n >>> checkManifest(v.manifest)\n\nFinally, cherry-picking changes is possible as well, though it can\ncause normal updates to be confused. `beginCollectionUpdate` takes an\niterable of items (such as is produced by iterChangedItems) and applies\nthe update with the usual conflict and examination approaches we've\nseen above. `completeUpdate` can then accept the changes for\nadditional updates.\n\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> discarded = Inventory(Manifest(v.manifest))\n >>> discarded.contents['ignatius'] = app['i1'] = Demo()\n >>> discarded.contents['jacobus'] = app['j1'] = Demo()\n >>> long_running.beginCollectionUpdate((discarded.contents('ignatius'),))\n >>> len(list(long_running.iterOrphanConflicts()))\n 1\n >>> o = iter(long_running.iterOrphanConflicts()).next()\n >>> o.selected\n True\n >>> o.name # None\n >>> o.parent # None\n >>> o.object\n \n >>> o.moveTo(long_running.contents, 'ignatius')\n >>> len(list(long_running.iterOrphanConflicts()))\n 0\n >>> long_running.contents['ignatius']\n \n >>> long_running.contents('ignatius')['jacobus'] = app['j1']\n >>> list(long_running.contents('ignatius').keys())\n ['jacobus']\n >>> long_running.contents('ignatius')('jacobus').selected\n True\n >>> list(discarded.contents('ignatius').keys())\n []\n >>> v.commit(long_running)\n >>> checkManifest(v.manifest)\n\nThe code will stop you if you try to add a set of relationships that result in\nthe manifest having keys that don't map to values--or more precisely, child\ntokens that don't have matching selected relationships. For instance, consider\nthis.\n\n >>> long_running = Inventory(Manifest(v.manifest))\n >>> discarded = Inventory(Manifest(v.manifest))\n >>> discarded.contents['katrina'] = app['k1'] = Demo()\n >>> discarded.contents('katrina')['loyola'] = app['l1'] = Demo()\n >>> long_running.beginCollectionUpdate((discarded.contents('katrina'),))\n ... # doctest: +ELLIPSIS\n Traceback (most recent call last):\n ...\n ValueError: cannot update from a set that includes children tokens...\n\nIt is disallowed because the katrina node includes the 'loyola' node, but we\ndidn't include the matching 'loyola' item.\n\nIf you include both, the merge will proceed as usual.\n\n >>> long_running.beginCollectionUpdate(\n ... (discarded.contents('katrina'),\n ... discarded.contents('katrina')('loyola')))\n >>> long_running.updating\n True\n >>> len(list((long_running.iterOrphanConflicts())))\n 2\n >>> orphans = dict((o.name, o) for o in long_running.iterOrphanConflicts())\n >>> orphans[None].moveTo(long_running.contents, 'katrina')\n >>> long_running.contents['katrina']\n \n >>> long_running.contents('katrina')['loyola']\n \n\nThe combination of `beginCollectionUpdate` and `iterChangedItems` can provide\na powerful way to apply arbitrary changesets to a revision.\n\nStoring None\n============\n\nSometimes you want to just make an empty node for organizational purposes.\nWhile normally stored objects must be versionable and adaptable to\nIKeyReference, None is a special case. We can store None in any node. Let's\nmake a quick example.\n\n >>> v = app['v'] = Vault()\n >>> i = Inventory(vault=v)\n >>> i.contents['foo'] = None\n >>> i.contents('foo')['bar'] = None\n >>> i.contents('foo')('bar')['baz'] = app['d1']\n >>> i.contents['foo'] # None\n >>> i.contents('foo')['bar'] # None\n >>> i.contents('foo')('bar')['baz'] is app['d1']\n True\n >>> i.contents['bing'] = app['a1']\n >>> i.contents['bing'] is app['a1']\n True\n >>> v.commit(i)\n >>> i = Inventory(vault=v, mutable=True)\n >>> i.contents['bing'] = None\n >>> del i.contents('foo')['bar']\n >>> i.contents['foo'] = app['d1']\n >>> v.commit(i)\n >>> v.inventory.contents.previous['bing'] is app['a1']\n True\n >>> v.inventory.contents.previous['foo'] is None\n True\n\nSpecial \"held\" Containers\n=========================\n\nIt is sometimes useful to specify a \"held\" container for all objects stored\nin a vault, overriding the \"held\" containers for each manifest as described\nabove. Vaults can be instantiated with specifying a held container.\n\n >>> from zc.vault.core import HeldContainer\n >>> held = app['held'] = HeldContainer()\n >>> v = app['vault_held'] = Vault(held=held)\n >>> i = Inventory(vault=v)\n >>> o = i.contents['foo'] = Demo()\n >>> o.__parent__ is held\n True\n >>> held[o.__name__] is o\n True\n\nIf you create a branch, by default it will use the same held container.\n\n >>> v.commit(i)\n >>> v2 = app['vault_held2'] = v.createBranch()\n >>> i2 = Inventory(vault=v2, mutable=True)\n >>> o2 = i2.contents['bar'] = Demo()\n >>> o2.__parent__ is held\n True\n >>> held[o2.__name__] is o2\n True\n\nYou can also specify another held container when you create a branch.\n\n >>> another_held = app['another_held'] = HeldContainer()\n >>> v3 = app['vault_held3'] = v.createBranch(held=another_held)\n >>> i3 = Inventory(vault=v3, mutable=True)\n >>> o3 = i3.contents['baz'] = Demo()\n >>> o3.__parent__ is another_held\n True\n >>> another_held[o3.__name__] is o3\n True\n\nCommitting the transaction\n==========================\n\nWe'll make sure that all these changes can in fact be committed to the ZODB.\n\n >>> import transaction\n >>> transaction.commit()\n\n-----------\n\n.. Other topics.\n\n ...commit messages? Could be added to event, so object log could use.\n\n Need commit datetime stamp, users. Handled now by objectlog.\n\n Show traversal adapters that use zc.shortcut code...\n\n Talk about tokens.\n\n Then talk about use case of having a reference be updated to a given object\n within a vault...\n\n ...a vault mirror that also keeps track of hierarchy?\n\n A special reference that knows both vault and token?\n\n\n=======\nCHANGES\n=======\n\n0.11 (2011-04-08)\n=================\n\n- Use eggs instead of zope3 checkout.\n- Use Python's `doctest` module instead of the deprecated\n `zope.testing.doctest`.\n- Update tests to run with ZTK 1.0.\n\n\n0.10 (2008-03-04)\n=================\n\n- Add a dependency on `rwproperty` instead of using a copy of it.\n- Add `zc.vault.versions.Traversable`.\n- Fix a bug for rename after freeze in r <= 78553.\n\n\n0.9 (2006-12-03)\n================\n\nInitial egg release.", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://pypi.python.org/pypi/zc.vault", "keywords": "Zope Zope3 version control vault", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "zc.vault", "package_url": "https://pypi.org/project/zc.vault/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/zc.vault/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://pypi.python.org/pypi/zc.vault" }, "release_url": "https://pypi.org/project/zc.vault/0.11/", "requires_dist": null, "requires_python": null, "summary": "Low-level versioning support", "version": "0.11" }, "last_serial": 802214, "releases": { "0.10": [ { "comment_text": "", "digests": { "md5": "dfcf8d3f1f2fbe948ce692839bebcae2", "sha256": "d84556122ed9398123e68107d9562dba9a5b2215c7c2d723cb13f6b4628b8714" }, "downloads": -1, "filename": "zc.vault-0.10.tar.gz", "has_sig": false, "md5_digest": "dfcf8d3f1f2fbe948ce692839bebcae2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 148002, "upload_time": "2008-03-04T22:09:43", "url": "https://files.pythonhosted.org/packages/fe/80/ddd60e7b094d0e9ff54e6fb596e1ea9ddf477c94cb2250e74c85e81d568b/zc.vault-0.10.tar.gz" } ], "0.11": [ { "comment_text": "", "digests": { "md5": "20fc3bbba33ed2a2976735870422ff7c", "sha256": "b7b6334cee979a54b3532a93884aa33704c8989b45a7708b9059349acc7c41b2" }, "downloads": -1, "filename": "zc.vault-0.11.tar.gz", "has_sig": false, "md5_digest": "20fc3bbba33ed2a2976735870422ff7c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 101999, "upload_time": "2011-04-08T23:41:50", "url": "https://files.pythonhosted.org/packages/be/63/9719a6732cbc628381f80667a91866435d4d1c1d86965b97ece3ff668ddf/zc.vault-0.11.tar.gz" } ], "0.9": [ { "comment_text": "", "digests": { "md5": "f295aa655839fef4b4bea7954139e38c", "sha256": "1a2313bed9c0c5b7da1336642f9d4bc5eabbb66f45fd8b3533a061e81bd0c9fc" }, "downloads": -1, "filename": "zc.vault-0.9-py2.4.egg", "has_sig": false, "md5_digest": "f295aa655839fef4b4bea7954139e38c", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 154301, "upload_time": "2006-12-03T01:20:28", "url": "https://files.pythonhosted.org/packages/af/f7/1b2eeb523a22e0155b86ce1cf921c142ffde42c217aa92475e439301af0f/zc.vault-0.9-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "db7a6707010c4153c860fbeb697fb69e", "sha256": "8f82252df63627bf6270b3f6559f6586fa6fe3b675ec51865d170f18e320f8b5" }, "downloads": -1, "filename": "zc.vault-0.9.tar.gz", "has_sig": false, "md5_digest": "db7a6707010c4153c860fbeb697fb69e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 98680, "upload_time": "2006-12-03T01:20:25", "url": "https://files.pythonhosted.org/packages/6b/5f/e05cf7fb6ddd273ad5d8609968e4872e230667075d53b8e858300a2f4c38/zc.vault-0.9.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "20fc3bbba33ed2a2976735870422ff7c", "sha256": "b7b6334cee979a54b3532a93884aa33704c8989b45a7708b9059349acc7c41b2" }, "downloads": -1, "filename": "zc.vault-0.11.tar.gz", "has_sig": false, "md5_digest": "20fc3bbba33ed2a2976735870422ff7c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 101999, "upload_time": "2011-04-08T23:41:50", "url": "https://files.pythonhosted.org/packages/be/63/9719a6732cbc628381f80667a91866435d4d1c1d86965b97ece3ff668ddf/zc.vault-0.11.tar.gz" } ] }