[Checkins] SVN: zc.vault/trunk/src/zc/vault/ Initial checkin. A
subversion-like repository stored in the ZODB,
complex but powerful and useful for many low-level tasks.
Gary Poster
gary at zope.com
Tue Aug 15 16:58:29 EDT 2006
Log message for revision 69538:
Initial checkin. A subversion-like repository stored in the ZODB, complex but powerful and useful for many low-level tasks.
Changed:
A zc.vault/trunk/src/zc/vault/README.txt
A zc.vault/trunk/src/zc/vault/__init__.py
A zc.vault/trunk/src/zc/vault/catalog.py
A zc.vault/trunk/src/zc/vault/catalog.txt
A zc.vault/trunk/src/zc/vault/catalog.zcml
A zc.vault/trunk/src/zc/vault/configure.zcml
A zc.vault/trunk/src/zc/vault/core.py
A zc.vault/trunk/src/zc/vault/i18n.py
A zc.vault/trunk/src/zc/vault/interfaces.py
A zc.vault/trunk/src/zc/vault/keyref.py
A zc.vault/trunk/src/zc/vault/objectlog.py
A zc.vault/trunk/src/zc/vault/objectlog.txt
A zc.vault/trunk/src/zc/vault/objectlog.zcml
A zc.vault/trunk/src/zc/vault/tests.py
A zc.vault/trunk/src/zc/vault/traversal.py
A zc.vault/trunk/src/zc/vault/traversal.txt
A zc.vault/trunk/src/zc/vault/traversal.zcml
A zc.vault/trunk/src/zc/vault/vault.py
A zc.vault/trunk/src/zc/vault/versions.py
A zc.vault/trunk/src/zc/vault/versions.txt
A zc.vault/trunk/src/zc/vault/versions.zcml
-=-
Added: zc.vault/trunk/src/zc/vault/README.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/README.txt 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/README.txt 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,2489 @@
+=====
+Vault
+=====
+
+Vaults model versioned containers. A single revision of a vault is
+typically viewed and (if not yet frozen) manipulated as an "inventory".
+Inventories actually manipulate lower-level objects called manifests
+that are only touched on in this document. Inventories are the primary
+API.
+
+Inventories *model* containers, but are not traditional mappings:
+containment is external to the actual objects in the inventory. You
+must query the inventory to discover the hierarchy, rather than the
+objects themselves. For instance, if you put an object in an inventory
+and want to treat it as a versioned folder, you don't put children in
+the object, but in the inventory node that wraps the object. This will
+be demonstrated repeatedly and in-depth below.
+
+Vaults only contain versioned, frozen manifests, accessed as
+inventories. Working inventories can be made from any inventory in a
+vault. They can then be modified, and committed themselves in the
+vault. Committing an inventory freezes it and all objects it
+"contains".
+
+Let's look at an example. Vaults store manifests, so when you first
+create one it is empty. Vaults have a basic sequence API, so a `len`
+will return `0`.
+
+ >>> from zc.vault.vault import Vault, Inventory
+ >>> from zc.vault.core import Manifest
+ >>> from zc.vault import interfaces
+ >>> from zope.interface.verify import verifyObject
+ >>> v = Vault()
+ >>> len(v)
+ 0
+ >>> verifyObject(interfaces.IVault, v)
+ True
+
+The last inventory--the -1 index--is the current one. A shorthand to this
+inventory is the inventory attribute.
+
+ >>> v.inventory # None
+
+Vaults and inventories must have a database connection in order to store their
+data. We'll assume we have a ZODB folder named "app" in which we can store
+our information. This is set up in tests.py when this file is run as a test.
+
+ >>> app['vault'] = v
+
+Creating an initial working inventory requires us to merely instantiate it.
+Usually we pass a versioned inventory on which to base the new inventory, but
+without that we at least pass the vault.
+
+ >>> i = Inventory(vault=v)
+ >>> verifyObject(interfaces.IInventory, i)
+ True
+
+Technically, what we have done is create a manifest--the core API for managing
+the contents--and wrapped an inventory API around it.
+
+ >>> verifyObject(interfaces.IManifest, i.manifest)
+ True
+
+We could have created the manifest explicitly instead.
+
+ >>> manifest = Manifest(vault=v)
+ >>> verifyObject(interfaces.IManifest, manifest)
+ True
+ >>> i = Inventory(manifest)
+ >>> verifyObject(interfaces.IInventory, i)
+ True
+
+Inventories--or at least the manifests on which they rely--must be
+stored somewhere in the database before being committed. They provide
+zope.app.location.interfaces.ILocation so that they can be stored in
+standard Zope containers as they are being developed.
+
+ >>> app['inventory'] = i
+
+Inventories have contents that can seem to directly contain objects. They have
+a mapping API, and follow the IInventoryContents interface.
+
+ >>> verifyObject(interfaces.IInventoryContents, i.contents)
+ True
+ >>> len(i.contents.keys())
+ 0
+ >>> len(i.contents.values())
+ 0
+ >>> len(i.contents.items())
+ 0
+ >>> list(i.contents)
+ []
+ >>> i.contents.get('mydemo') # None
+ >>> 'mydemo' in i
+ False
+ >>> i.contents['mydemo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'mydemo'
+ >>> del i.contents['mydemo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'mydemo'
+
+(ADVANCED SIDE NOTE: feel free to ignore)
+
+The contents object is an API convenience to wrap a relationship.
+Relationships connect a token to various pieces of information. The
+token for all inventory contents (the top node) is stored on the vault
+as the top_token attribute, and lower levels get unique tokens that
+represent a given location in a vault across inventories.
+
+Contents and items (seen below) essentially get all their data from the
+relationships and the associated manifest that holds them.
+
+ >>> verifyObject(interfaces.IRelationship, i.contents.relationship)
+ True
+ >>> i.contents.relationship.token == i.vault.top_token
+ True
+ >>> verifyObject(interfaces.IRelationshipContainment,
+ ... i.contents.relationship.containment)
+ True
+ >>> i.contents.relationship.object # None, because contents.
+
+(end ADVANCED SIDE NOTE)
+
+Because it is often convenient to use tokens as a globally unique identifier
+of a particular object, all inventory items have a "token" attribute.
+
+ >>> i.contents.token
+ 1234567
+
+Unlike typical Zope 3 containment as defined in zope.app.container, this
+containment does not affect the __parent__ or __name__ of the object.
+
+All objects stored in an inventory must be None, or be adaptable to
+zope.app.keyreference.interfaces.IKeyReference. In standard Zope 3,
+this includes any instance of a class that extends
+persistent.Persistent.
+
+All non-None objects must also be adaptable to
+zc.copyversion.interfaces.IVersionable.
+
+Here, we create an object, add it to the application, and try to add it to
+an inventory.
+
+ >>> import persistent
+ >>> from zope.app.container.contained import Contained
+ >>> class Demo(persistent.Persistent, Contained):
+ ... def __repr__(self):
+ ... return "<%s %r>" % (self.__class__.__name__, self.__name__)
+ ...
+ >>> app['d1'] = Demo()
+ >>> i.contents['mydemo'] = app['d1']
+ Traceback (most recent call last):
+ ...
+ ValueError: can only place versionable objects in vault, or None
+
+This error occurs because committing an inventory must version itself
+and version all of its contained objects, so that looking at an
+historical inventory displays the objects as they were at the time of
+commit. Here's a simple demo adapter for the Demo objects. We also
+declare that Demo is IVersionable, an important marker.
+
+ >>> import pytz
+ >>> import datetime
+ >>> from zope import interface, component, event
+ >>> from zc.copyversion.interfaces import (
+ ... IVersioning, ObjectVersionedEvent, IVersionable)
+ >>> from zc.copyversion.versioning import method
+ >>> class DemoVersioningAdapter(object):
+ ... interface.implements(IVersioning)
+ ... component.adapts(Demo)
+ ... def __init__(self, context):
+ ... self.context = context
+ ... @property
+ ... def _z_versioned(self):
+ ... return (getattr(self.context, '_z__version_timestamp', None)
+ ... is not None)
+ ... @property
+ ... def _z_version_timestamp(self):
+ ... return getattr(self.context, '_z__version_timestamp', None)
+ ... @method
+ ... def _z_version(self):
+ ... self.context._z__version_timestamp = datetime.datetime.now(
+ ... pytz.utc)
+ ... event.notify(ObjectVersionedEvent(self))
+ ...
+ >>> component.provideAdapter(DemoVersioningAdapter)
+ >>> interface.classImplements(Demo, IVersionable)
+
+As an aside, it's worth noting that the manifest objects provide
+IVersioning natively, so they can already be queried for the versioning
+status and timestamp without adaptation. When a manifest is versioned,
+all "contained" objects should be versioned as well.
+
+It's not versioned now--and neither is our demo instance.
+
+ >>> manifest._z_versioned
+ False
+ >>> IVersioning(app['d1'])._z_versioned
+ False
+
+Now that Demo instances are versionable we can add the object to the inventory.
+That means adding and removing objects. Here we add one.
+
+ >>> i.contents['mydemo'] = app['d1']
+ >>> i.contents['mydemo']
+ <Demo u'd1'>
+ >>> i.__parent__ is app
+ True
+ >>> i.contents.__parent__ is i
+ True
+ >>> i.contents.get('mydemo')
+ <Demo u'd1'>
+ >>> list(i.contents.keys())
+ ['mydemo']
+ >>> i.contents.values()
+ [<Demo u'd1'>]
+ >>> i.contents.items()
+ [('mydemo', <Demo u'd1'>)]
+ >>> list(i.contents)
+ ['mydemo']
+ >>> 'mydemo' in i.contents
+ True
+
+Now our effective hierarchy simply looks like this::
+
+ (top node)
+ |
+ 'mydemo'
+ (<Demo u'd1'>)
+
+We will update this hierarchy as we proceed.
+
+Adding an object fires a (special to the package!) IObjectAdded event.
+This event is not from the standard lifecycleevents package because
+that one has a different connotation--for instance, as noted before,
+putting an object in an inventory does not set the __parent__ or
+__name__ (unless it does not already have a location, in which case it
+is put in a possibly temporary "held" container, discussed below).
+
+ >>> interfaces.IObjectAdded.providedBy(events[-1])
+ True
+ >>> isinstance(events[-1].object, int)
+ True
+ >>> i.manifest.get(events[-1].object).object is app['d1']
+ True
+ >>> events[-1].mapping is i.contents.relationship.containment
+ True
+ >>> events[-1].key
+ 'mydemo'
+
+Now we remove the object.
+
+ >>> del i.contents['mydemo']
+ >>> len(i.contents.keys())
+ 0
+ >>> len(i.contents.values())
+ 0
+ >>> len(i.contents.items())
+ 0
+ >>> list(i.contents)
+ []
+ >>> i.contents.get('mydemo') # None
+ >>> 'mydemo' in i.contents
+ False
+ >>> i.contents['mydemo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'mydemo'
+ >>> del i.contents['mydemo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'mydemo'
+
+Removing an object fires a special IObjectRemoved event (again, not from
+lifecycleevents).
+
+ >>> interfaces.IObjectRemoved.providedBy(events[-1])
+ True
+ >>> isinstance(events[-1].object, int)
+ True
+ >>> i.manifest.get(events[-1].object).object is app['d1']
+ True
+ >>> events[-1].mapping is i.contents.relationship.containment
+ True
+ >>> events[-1].key
+ 'mydemo'
+
+In addition to a mapping API, the inventory contents support an ordered
+container API very similar to the ordered container in
+zope.app.container.ordered. The ordered nature of the contents mean that
+iterating is on the basis of the order in which objects were added, by default
+(earliest first); and that the inventory supports an "updateOrder" method.
+The method takes an iterable of names in the container: the new order will be
+the given order. If the set of given names differs at all with the current
+set of keys, the method will raise ValueError.
+
+ >>> i.contents.updateOrder(())
+ >>> i.contents.updateOrder(('foo',))
+ Traceback (most recent call last):
+ ...
+ ValueError: Incompatible key set.
+ >>> i.contents['donald'] = app['d1']
+ >>> app['b1'] = Demo()
+ >>> i.contents['barbara'] = app['b1']
+ >>> app['c1'] = Demo()
+ >>> app['a1'] = Demo()
+ >>> i.contents['cathy'] = app['c1']
+ >>> i.contents['abe'] = app['a1']
+ >>> list(i.contents.keys())
+ ['donald', 'barbara', 'cathy', 'abe']
+ >>> i.contents.values()
+ [<Demo u'd1'>, <Demo u'b1'>, <Demo u'c1'>, <Demo u'a1'>]
+ >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+ [('donald', <Demo u'd1'>), ('barbara', <Demo u'b1'>),
+ ('cathy', <Demo u'c1'>), ('abe', <Demo u'a1'>)]
+ >>> list(i.contents)
+ ['donald', 'barbara', 'cathy', 'abe']
+ >>> 'cathy' in i.contents
+ True
+ >>> i.contents.updateOrder(())
+ Traceback (most recent call last):
+ ...
+ ValueError: Incompatible key set.
+ >>> i.contents.updateOrder(('foo',))
+ Traceback (most recent call last):
+ ...
+ ValueError: Incompatible key set.
+ >>> i.contents.updateOrder(iter(('abe', 'barbara', 'cathy', 'donald')))
+ >>> list(i.contents.keys())
+ ['abe', 'barbara', 'cathy', 'donald']
+ >>> i.contents.values()
+ [<Demo u'a1'>, <Demo u'b1'>, <Demo u'c1'>, <Demo u'd1'>]
+ >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+ [('abe', <Demo u'a1'>), ('barbara', <Demo u'b1'>),
+ ('cathy', <Demo u'c1'>), ('donald', <Demo u'd1'>)]
+ >>> list(i.contents)
+ ['abe', 'barbara', 'cathy', 'donald']
+ >>> i.contents.updateOrder(('abe', 'cathy', 'donald', 'barbara', 'edward'))
+ Traceback (most recent call last):
+ ...
+ ValueError: Incompatible key set.
+ >>> list(i.contents)
+ ['abe', 'barbara', 'cathy', 'donald']
+ >>> del i.contents['cathy']
+ >>> list(i.contents.keys())
+ ['abe', 'barbara', 'donald']
+ >>> i.contents.values()
+ [<Demo u'a1'>, <Demo u'b1'>, <Demo u'd1'>]
+ >>> i.contents.items() # doctest: +NORMALIZE_WHITESPACE
+ [('abe', <Demo u'a1'>), ('barbara', <Demo u'b1'>), ('donald', <Demo u'd1'>)]
+ >>> list(i.contents)
+ ['abe', 'barbara', 'donald']
+ >>> i.contents.updateOrder(('barbara', 'abe', 'donald'))
+ >>> list(i.contents.keys())
+ ['barbara', 'abe', 'donald']
+ >>> i.contents.values()
+ [<Demo u'b1'>, <Demo u'a1'>, <Demo u'd1'>]
+
+Now our _`hierarchy` looks like this::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+
+Reordering a container fires an event.
+
+ >>> interfaces.IOrderChanged.providedBy(events[-1])
+ True
+ >>> events[-1].object is i.contents.relationship.containment
+ True
+ >>> events[-1].old_keys
+ ('abe', 'barbara', 'donald')
+
+In some circumstances it's easier set the new order from a set of tokens. In
+that case the "updateOrderFromTokens" method is useful.
+
+ >>> def getToken(key):
+ ... return i.contents(k).token
+
+ >>> new_order = [getToken(k) for k in ('abe', 'donald', 'barbara')]
+ >>> i.contents.updateOrderFromTokens(new_order)
+ >>> list(i.contents.keys())
+ ['abe', 'donald', 'barbara']
+
+Just like "updateOrder", an event is fired.
+
+ >>> interfaces.IOrderChanged.providedBy(events[-1])
+ True
+ >>> events[-1].object is i.contents.relationship.containment
+ True
+ >>> events[-1].old_keys
+ ('barbara', 'abe', 'donald')
+
+It's just as easy to put them back so that the `hierarchy`_ still looks the
+same as it did at the end of the previous example.
+
+ >>> new_order = [getToken(k) for k in ('barbara', 'abe', 'donald')]
+ >>> i.contents.updateOrderFromTokens(new_order)
+ >>> list(i.contents.keys())
+ ['barbara', 'abe', 'donald']
+
+As noted in the introduction to this document, the versioned hierarchy
+is kept external from the objects themselves. This means that objects
+that are not containers themselves can still be branch
+nodes--containers, of a sort--within an inventory. In fact, until a
+reasonable use case emerges for the pattern, the author discourages the
+use of true containers within a vault as branch nodes: two dimensions
+of "containerish" behavior is too confusing.
+
+In order to get an object that can act as a container for one of the objects
+in the inventory, one calls the inventory contents: "i.contents('abe')". This
+returns an IInventoryItem, if the key exists. It raises a KeyError for a
+missing key by default, but can take a default.
+
+ >>> i.contents['abe']
+ <Demo u'a1'>
+ >>> item = i.contents('abe')
+ >>> verifyObject(interfaces.IInventoryItem, item)
+ True
+ >>> i.contents('foo')
+ Traceback (most recent call last):
+ ...
+ KeyError: 'foo'
+ >>> i.contents('foo', None) # None
+
+IInventoryItems extend IInventoryContents to add an 'object' attribute, which
+is the object they represent. Like IInventoryContents, a mapping interface
+allows one to manipulate the hierarchy beneath the top level. For instance,
+here we effectively put the 'cathy' demo object in the container space of the
+'abe' demo object.
+
+ >>> item.object
+ <Demo u'a1'>
+ >>> item.name
+ 'abe'
+ >>> item.parent.relationship is i.contents.relationship
+ True
+ >>> item.__parent__ is item.inventory
+ True
+ >>> list(item.values())
+ []
+ >>> list(item.keys())
+ []
+ >>> list(item.items())
+ []
+ >>> list(item)
+ []
+ >>> item.get('foo') # None
+ >>> item['foo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'foo'
+ >>> item('foo')
+ Traceback (most recent call last):
+ ...
+ KeyError: 'foo'
+ >>> item['catherine'] = app['c1']
+ >>> item['catherine']
+ <Demo u'c1'>
+ >>> item.get('catherine')
+ <Demo u'c1'>
+ >>> list(item.keys())
+ ['catherine']
+ >>> list(item.values())
+ [<Demo u'c1'>]
+ >>> list(item.items())
+ [('catherine', <Demo u'c1'>)]
+ >>> catherine = item('catherine')
+ >>> catherine.object
+ <Demo u'c1'>
+ >>> catherine.name
+ 'catherine'
+ >>> catherine.parent.name
+ 'abe'
+ >>> catherine.parent.object
+ <Demo u'a1'>
+ >>> list(catherine.keys())
+ []
+
+Now our hierarchy looks like this::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+
+It's worthwhile noting that the same object can be in multiple places in an
+inventory. This does not duplicate the hierarchy, or keep changes in sync.
+If desired, this policy should be performed in code that uses the vault;
+similarly if a vault should only contain an object in one location at a time,
+this should be enforced in code that uses a vault.
+
+ >>> i.contents('abe')('catherine')['anna'] = app['a1']
+ >>> i.contents('abe')('catherine').items()
+ [('anna', <Demo u'a1'>)]
+ >>> i.contents('abe')('catherine')('anna').parent.parent.object
+ <Demo u'a1'>
+
+Now our hierarchy looks like this::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+Even though a1 contains c1 contains a1, this does not constitute a cycle: the
+hierarchy is separate from the objects.
+
+InventoryItems and InventoryContents are currently created on the fly, and
+not persisted. They should be compared with "==", not "is". They represent
+a persistent core data object that provides zc.vault.interfaces.IRelationship.
+The IRelationship itself is hidden from the majority of this discussion and
+only introduced at the end of the document. But in any case...
+
+ >>> i.contents('abe') is i.contents('abe')
+ False
+ >>> i.contents('abe') == i.contents('abe')
+ True
+ >>> i.contents is i.contents
+ False
+ >>> i.contents == i.contents
+ True
+ >>> i.contents == None
+ False
+ >>> i.contents('abe') == None
+ False
+
+Comparing inventories will also compare their contents:
+
+ >>> i == None
+ False
+ >>> i == i
+ True
+ >>> i != i
+ False
+
+Another important characteristic of inventory items is that they continue to
+have the right information even as objects around them are changed--for
+instance, if an object's parent is changed from one part of the hierarchy to
+another (see `moveTo`, below), an item generated before the move will still
+reflect the change correctly.
+
+It's worth noting that, thanks to the wonder of the zc.shortcut code, views can
+exist for the object and also, from a proxy, have access to the InventoryItem's
+information: this needs to be elaborated (TODO).
+
+Now we'll try to commit.
+
+ >>> v.commit(i) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ ConflictError: <zc.vault.core.Manifest object at ...>
+
+Conflicts? We don't need no stinking conflicts! We didn't even merge! Where
+did this come from?
+
+The default vault takes a very strict approach to keeping track of conflicts:
+for instance, if you add something and then delete it in the same inventory,
+it will regard this as an "orphan conflict": a change that happened in this
+inventory that will not be committed. You must explicitly say that it is
+OK for these orphaned changes to be lost. Let's look at the orphans.
+
+ >>> orphans = list(i.iterOrphanConflicts())
+ >>> sorted(repr(item.object) for item in orphans)
+ ["<Demo u'c1'>", "<Demo u'd1'>"]
+ >>> orphans[0].parent # None
+ >>> orphans[0].name # None
+
+Ah yes--you can see that we deleted these objects above: we deleted "mydemo"
+(d1) and cathy (c1). We'll just tell the inventory that it is ok to not
+include them. If vault clients want to have more automation so that deletions
+automatically resolve, then they have the tools to do so. After the
+resolution, iterOrphanConflicts will then be empty, and iterOrphanResolutions
+will include the objects.
+
+ >>> for o in orphans:
+ ... o.resolveOrphanConflict()
+ ...
+ >>> len(list(i.iterOrphanConflicts()))
+ 0
+ >>> sorted(repr(item.object) for item in i.iterOrphanResolutions())
+ ["<Demo u'c1'>", "<Demo u'd1'>"]
+
+Now when we commit, all objects will be versioned, and we will receive events
+for the versioning and the committing. The events list represents recent
+events; when this document is run as a test, it is populated by listening for
+all events and attaching them to the list.
+
+ >>> v.commit(i)
+ >>> interfaces.IManifestCommitted.providedBy(events[-1])
+ True
+ >>> events[-1].object is manifest
+ True
+ >>> manifest.__parent__ is v
+ True
+ >>> IVersioning(app['a1'])._z_versioned
+ True
+ >>> IVersioning(app['b1'])._z_versioned
+ True
+ >>> IVersioning(app['c1'])._z_versioned
+ True
+ >>> IVersioning(app['d1'])._z_versioned
+ True
+ >>> manifest._z_versioned
+ True
+ >>> v.manifest is manifest
+ True
+ >>> len(v)
+ 1
+
+After the versioning, the inventory enforces the versioning: no more changes
+can be made.
+
+ >>> i.contents['foo'] = Demo()
+ Traceback (most recent call last):
+ ...
+ VersionedError
+ >>> i.contents.updateOrder(())
+ Traceback (most recent call last):
+ ...
+ VersionedError
+ >>> i.contents('abe')('catherine')['foo'] = Demo()
+ Traceback (most recent call last):
+ ...
+ VersionedError
+
+ >>> v.manifest._z_versioned
+ True
+
+Enforcing the versioning of the inventory's objects is the responsibility of
+other configuration.
+
+The manifest now has an __name__ which is the string of its index. This is
+of very limited usefulness, but with the right traverser might still allow
+items in the held container to be traversed to.
+
+ >>> i.manifest.__name__
+ u'0'
+
+After every commit, the vault should be able to determine the previous and
+next versions of every relationship. Since this is the first commit, previous
+will be None, but we'll check it now anyway, building a function that checks
+the most recent manifest of the vault.
+
+ >>> def checkManifest(m):
+ ... v = m.vault
+ ... for r in m:
+ ... p = v.getPrevious(r)
+ ... assert (p is None or
+ ... r.__parent__.vault is not v or
+ ... p.__parent__.vault is not v or
+ ... v.getNext(p) is r)
+ ...
+ >>> checkManifest(v.manifest)
+
+Creating a new working inventory requires a new manifest, based on the old
+manifest.
+
+For better or worse, the package offers four approaches to this. We
+can create a new working inventory by specifying a vault, from which
+the most recent manifest will be selected, and "mutable=True";
+
+ >>> i = Inventory(vault=v, mutable=True)
+ >>> manifest = i.manifest
+ >>> manifest._z_versioned
+ False
+
+by specifying an inventory, from which its manifest will be
+extracted, and "mutable=True";
+
+ >>> i = Inventory(inventory=v.inventory, mutable=True)
+ >>> manifest = i.manifest
+ >>> manifest._z_versioned
+ False
+
+by specifying a versioned manifest and "mutable=True";
+
+ >>> i = Inventory(v.manifest, mutable=True)
+ >>> manifest = i.manifest
+ >>> manifest._z_versioned
+ False
+
+or by specifying a mutable manifest.
+
+ >>> i = Inventory(Manifest(v.manifest))
+ >>> i.manifest._z_versioned
+ False
+
+These multiple spellings should be reexamined at a later date, and may have
+a deprecation period. The last spelling--an explicit pasing of a manifest to
+an inventory--is the most likely to remain stable, because it clearly allows
+instantiation of the inventory wrapper for a working manifest or a versioned
+manifest.
+
+Note that, as mentioned above, the inventory is just an API wrapper around the
+manifest: therefore, changes to inventories that share a manifest will be
+shared among them.
+
+ >>> i_extra = Inventory(i.manifest)
+ >>> manifest._z_versioned
+ False
+
+In any case, we now have an inventory that has the same contents as the
+original.
+
+ >>> i.contents.keys() == v.inventory.contents.keys()
+ True
+ >>> i.contents['barbara'] is v.inventory.contents['barbara']
+ True
+ >>> i.contents['abe'] is v.inventory.contents['abe']
+ True
+ >>> i.contents['donald'] is v.inventory.contents['donald']
+ True
+ >>> i.contents('abe')['catherine'] is v.inventory.contents('abe')['catherine']
+ True
+ >>> i.contents('abe')('catherine')['anna'] is \
+ ... v.inventory.contents('abe')('catherine')['anna']
+ True
+
+We can now manipulate the new inventory as we did the old one.
+
+ >>> app['d2'] = Demo()
+ >>> i.contents['donald'] = app['d2']
+ >>> i.contents['donald'] is v.inventory.contents['donald']
+ False
+
+Now our hierarchy looks like this::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd2'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+Now we can observe our local changes. One way to do this is to examine
+the results of iterChangedItems.
+
+ >>> len(list(i.iterChangedItems()))
+ 1
+ >>> iter(i.iterChangedItems()).next() == i.contents('donald')
+ True
+
+Another is to look at each inventory item. The items specify the type of
+information in the item: whether it is from the 'base', the 'local' changes,
+or a few other options we'll see when we examine merges.
+
+ >>> i.contents('abe').type
+ 'base'
+ >>> i.contents('donald').type
+ 'local'
+
+This will be true whether or not the change is returned to the original value
+by hand.
+
+ >>> i.contents['donald'] = app['d1']
+ >>> v.inventory.contents['donald'] is i.contents['donald']
+ True
+
+However, unchanged local copies are not included in the iterChangedItems
+results; they are also discarded on commit, as we will see below.
+
+ >>> len(list(i.iterChangedItems()))
+ 0
+
+Now our hierarchy looks like this again::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+Each inventory item represents a single collection of data that stores an
+object and its effective hierarchy. Therefore, changing either (or both) will
+generate a local inventory item.
+
+ >>> app['e1'] = Demo()
+ >>> i.contents('barbara').type
+ 'base'
+ >>> i.contents('barbara')['edna'] = app['e1']
+ >>> i.contents('barbara').type
+ 'local'
+ >>> i.contents['barbara'] is v.inventory.contents['barbara']
+ True
+ >>> len(list(i.iterChangedItems()))
+ 2
+
+Those are two changes: one new node (edna) and one changed node (barbara got a
+new child).
+
+Now our hierarchy looks like this ("*" indicates a changed node)::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara'* 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+ / |
+ / |
+ 'edna'* 'catherine'
+ <Demo u'e1'> <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+Modifying the collection of the top level contents means that we have a change
+as well: even though the inventory does not keep track of a single object at
+the top of the hierarchy, it does keep track of containment at the top level.
+
+ >>> i.contents.type
+ 'base'
+ >>> app['f1'] = Demo()
+ >>> i.contents['fred'] = app['f1']
+ >>> i.contents.type
+ 'local'
+ >>> len(list(i.iterChangedItems()))
+ 4
+
+That's four changes: edna, barbara, fred, and the top node.
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+ (top node)*
+ / / \ \
+ ---- / \ ---------
+ / | | \
+ 'barbara'* 'abe' 'donald' 'fred'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'f1'>
+ / |
+ / |
+ 'edna'* 'catherine'
+ <Demo u'e1'> <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+You can actually examine the base from the changed item--and even switch back.
+The `base_item` attribute always returns an item with the original object and
+containment. The `local_item` returns an item with local changes, or None if
+no changes have been made. A `select` method allows you to switch the given
+item to look at one or the other by default. The readonly `selected`
+attribute allows introspection.
+
+ >>> list(i.contents.keys())
+ ['barbara', 'abe', 'donald', 'fred']
+ >>> i.contents == i.contents.local_item
+ True
+ >>> list(i.contents('barbara').keys())
+ ['edna']
+ >>> i.contents('barbara') == i.contents('barbara').local_item
+ True
+ >>> i.contents('barbara').local_item.selected
+ True
+ >>> i.contents('barbara').base_item.selected
+ False
+ >>> len(i.contents('barbara').base_item.keys())
+ 0
+ >>> list(i.contents.base_item.keys())
+ ['barbara', 'abe', 'donald']
+ >>> i.contents('barbara').base_item.select()
+ >>> len(list(i.iterChangedItems()))
+ 3
+
+That's fred, the top level, /and/ edna: edna still is a change, even though
+she is inaccessible with the old version of barbara. If we were to commit now,
+we would have to resolve the orphan, as shown above.
+
+ >>> v.commit(i) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ ConflictError: <zc.vault.core.Manifest object at ...>
+ >>> list(item.object for item in i.iterOrphanConflicts())
+ [<Demo u'e1'>]
+
+Let's look around a little more and switch things back:
+
+ >>> i.contents('barbara').local_item.selected
+ False
+ >>> i.contents('barbara').base_item.selected
+ True
+ >>> len(i.contents('barbara').keys())
+ 0
+ >>> i.contents('barbara') == i.contents('barbara').local_item
+ False
+ >>> i.contents('barbara') == i.contents('barbara').base_item
+ True
+ >>> i.contents('barbara').local_item.select()
+ >>> len(list(i.iterChangedItems()))
+ 4
+ >>> i.contents('barbara').local_item.selected
+ True
+ >>> i.contents('barbara').base_item.selected
+ False
+ >>> list(i.contents('barbara').keys())
+ ['edna']
+
+The inventory has booleans to examine whether a base item or local item exists,
+as a convenience (and optimization opportunity).
+
+ >>> i.contents('fred').has_local
+ True
+ >>> i.contents('fred').has_base
+ False
+ >>> i.contents('abe')('catherine').has_local
+ False
+ >>> i.contents('abe')('catherine').has_base
+ True
+ >>> i.contents('barbara').has_local
+ True
+ >>> i.contents('barbara').has_base
+ True
+
+It also has four other similar properties, `has_updated`, `has_suggested`,
+`has_modified`, and `has_merged`, which we will examine later.
+
+Before we commit we are going to make one more change to the inventory. We'll
+make a change to "anna". Notice how we spell this in the code: it this is the
+first object we have put in an inventory that does not already have a location
+in app. When an inventory is asked to version an object without an ILocation,
+it stores it in a special folder on the manifest named "held". Held objects
+are assigned names using the standard Zope 3 name chooser pattern and can be
+moved out even after being versioned. In this case we will need to register a
+name chooser for our demo objects. We'll use the standard one.
+
+ >>> from zope.app.container.contained import NameChooser
+ >>> from zope.app.container.interfaces import IWriteContainer
+ >>> component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+ >>> len(i.manifest.held)
+ 0
+ >>> i.contents('abe')('catherine')['anna'] = Demo()
+ >>> len(i.manifest.held)
+ 1
+ >>> i.manifest.held.values()[0] is i.contents('abe')('catherine')['anna']
+ True
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+ (top node)*
+ / / \ \
+ ---- / \ ---------
+ / | | \
+ 'barbara'* 'abe' 'donald' 'fred'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'f1'>
+ / |
+ / |
+ 'edna'* 'catherine'
+ <Demo u'e1'> <Demo u'c1'>
+ |
+ |
+ 'anna'*
+ <Demo ...>
+
+In our previous inventory commit, objects were versioned in place. The vault
+code provides a hook to generate objects for committing to vault: it tries to
+adapt objects it wants to version to zc.vault.interfaces.IVersionFactory.
+This interface specifies any callable object. Let's provide an example.
+
+The policy here is that if the object is in the inventories' held container,
+just return it, but otherwise "make a copy"--which for our demo just makes a
+new instance and slams the old one's name on it as an attribute.
+
+ >>> @interface.implementer(interfaces.IVersionFactory)
+ ... @component.adapter(interfaces.IVault)
+ ... def versionFactory(vault):
+ ... def makeVersion(obj, manifest):
+ ... if obj.__parent__ is manifest.held:
+ ... return obj
+ ... res = Demo()
+ ... res.source_name = obj.__name__
+ ... return res
+ ... return makeVersion
+ ...
+ >>> component.provideAdapter(versionFactory)
+
+Let's commit now, to show the results. We'll discard the change to barbara.
+
+ >>> len(list(i.iterChangedItems()))
+ 5
+ >>> i.contents('barbara')('edna').resolveOrphanConflict()
+ >>> i.contents('barbara').base_item.select()
+ >>> len(list(i.iterChangedItems()))
+ 4
+
+Edna is included even though she is resolved.
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+ (top node)*
+ / / \ \
+ ---- / \ ---------
+ / | | \
+ 'barbara' 'abe' 'donald' 'fred'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'f1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'*
+ <Demo ...>
+
+ >>> changed = dict(
+ ... (getattr(item, 'name', None), item)
+ ... for item in i.iterChangedItems())
+ >>> changed['anna'].parent.name
+ 'catherine'
+ >>> changed['fred'].object
+ <Demo u'f1'>
+ >>> changed['edna'].object
+ <Demo u'e1'>
+ >>> list(changed[None].keys())
+ ['barbara', 'abe', 'donald', 'fred']
+ >>> old_objects = dict(
+ ... (k, i.object) for k, i in changed.items() if k is not None)
+ >>> v.commit(i)
+ >>> checkManifest(v.manifest)
+ >>> len(v)
+ 2
+ >>> v.manifest is i.manifest
+ True
+ >>> v.inventory == i
+ True
+
+We committed the addition of fred, but not the addition of edna. Once an
+inventory is committed, unselected changes are discarded. Also, as mentioned
+above, the data for local item for `donald` has been discarded, since it did
+not include any changes.
+
+ >>> i.contents.local_item == i.contents
+ True
+ >>> i.contents.type
+ 'local'
+ >>> i.contents('barbara').local_item # None
+ >>> i.contents('barbara').type
+ 'base'
+ >>> i.contents('donald').local_item # None
+ >>> i.contents('donald').type
+ 'base'
+ >>> IVersioning(app['e1'])._z_versioned
+ False
+
+Our changes are a bit different than what we had when we began the commit,
+because of the version Factory. The f1 is not versioned, because we have made
+a copy instead.
+
+ >>> IVersioning(app['f1'])._z_versioned
+ False
+ >>> new_changed = dict(
+ ... (getattr(item, 'name', None), item)
+ ... for item in i.iterChangedItems())
+ >>> new_changed['anna'].parent.name
+ 'catherine'
+ >>> new_changed['anna'].object is old_objects['anna']
+ True
+ >>> new_changed['fred'].object is old_objects['fred']
+ False
+ >>> new_changed['fred'].object is app['f1']
+ False
+ >>> new_changed['fred'].object.source_name
+ u'f1'
+ >>> IVersioning(new_changed['anna'].object)._z_versioned
+ True
+ >>> IVersioning(new_changed['fred'].object)._z_versioned
+ True
+
+Now that we have two versions in the vault, we can introduce two
+additional attributes of the inventories, contents, and items: `next` and
+`previous`. These attributes let you time travel in the vault's history.
+
+We also look at similar attributes on the manifest, and at the vault's
+`getInventory` method.
+
+For instance, the current inventory's `previous` attribute points to the
+original inventory, and vice versa.
+
+ >>> i.previous == v.getInventory(0)
+ True
+ >>> i.manifest.previous is v[0]
+ True
+ >>> v.getInventory(0).next == i == v.inventory
+ True
+ >>> v[0].next is i.manifest is v.manifest
+ True
+ >>> i.next # None
+ >>> manifest.next # None
+ >>> v.getInventory(0).previous # None
+ >>> v[0].previous # None
+
+The same is true for inventory items.
+
+ >>> list(v.inventory.contents.previous.keys())
+ ['barbara', 'abe', 'donald']
+ >>> list(v.getInventory(0).contents.next.keys())
+ ['barbara', 'abe', 'donald', 'fred']
+ >>> v.inventory.contents.previous.next == v.inventory.contents
+ True
+ >>> v.inventory.contents('abe')('catherine')('anna').previous.object
+ <Demo u'a1'>
+ >>> (v.inventory.contents('abe').relationship is
+ ... v.inventory.contents.previous('abe').relationship)
+ True
+
+Once you step to a previous or next item, further steps from the item remain
+in the previous or next inventory.
+
+ >>> v.inventory.contents('abe')('catherine')['anna'].__name__ == 'a1'
+ False
+ >>> v.inventory.contents.previous('abe')('catherine')['anna']
+ <Demo u'a1'>
+
+In addition, inventory items support `previous_version` and `next_version`.
+The difference between these and `previous` and `next` is that the `*_version`
+variants skip to the item that was different than the current item. For
+instance, while the previous_version of the 'anna' is the old 'a1' object,
+just like the `previous` value, the previous_version of 'abe' is None, because
+it has no previous version.
+
+ >>> v.inventory.contents(
+ ... 'abe')('catherine')('anna').previous_version.object
+ <Demo u'a1'>
+ >>> v.inventory.contents('abe').previous_version # None
+
+These leverage the `getPrevious` and `getNext` methods on the vault, which work
+with relationships.
+
+The previous and next tools are even more interesting when tokens move: you
+can see positions change within the hierarchy. Inventories have a `moveTo`
+method that can let the inventory follow the moves to maintain history. We'll
+create a new inventory copy and demonstrate. As we do, notice that
+inventory items obtained before the move correctly reflect the move, as
+described above.
+
+ >>> manifest = Manifest(v.manifest)
+ >>> del app['inventory']
+ >>> i = app['inventory'] = Inventory(manifest)
+ >>> item = i.contents('abe')('catherine')
+ >>> item.parent.name
+ 'abe'
+ >>> i.contents('abe')('catherine').moveTo(i.contents('fred'))
+ >>> item.parent.name
+ 'fred'
+ >>> len(i.contents('abe').keys())
+ 0
+ >>> list(i.contents('fred').keys())
+ ['catherine']
+
+The change actually only affects the source and target of the move.
+
+ >>> changes = dict((getattr(item, 'name'), item)
+ ... for item in i.iterChangedItems())
+ >>> len(changes)
+ 2
+ >>> changes['fred'].values()
+ [<Demo u'c1'>]
+ >>> len(changes['abe'].keys())
+ 0
+
+So now our hierarchy looks like this ("*" indicates a changed node)::
+
+ (top node)
+ / / \ \
+ ---- / \ ---------
+ / | | \
+ 'barbara' 'abe'* 'donald' 'fred'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'f1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo ...>
+
+If you try to move parts of the hierarchy to someplace that has the same name,
+you will receive a ValueError unless you specify a name that does not
+conflict.
+
+ >>> i.contents('abe')['donald'] = app['d2']
+ >>> i.contents('donald').moveTo(i.contents('abe'))
+ Traceback (most recent call last):
+ ...
+ ValueError: Object with same name already exists in new location
+ >>> i.contents('donald').moveTo(i.contents('abe'), 'old_donald')
+ >>> i.contents('abe').items()
+ [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+ (top node)*
+ / | \
+ ---- | ----
+ / | \
+ 'barbara' 'abe'* 'fred'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'f1'>
+ / \ |
+ / \ |
+ 'donald'* 'old_donald' 'catherine'
+ <Demo u'd2'> <Demo u'd1'> <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo ...>
+
+If you try to move part of the hierarchy to someplace within itself, you will
+also receive a ValueError.
+
+ >>> i.contents('fred').moveTo(i.contents('fred')('catherine')('anna'))
+ Traceback (most recent call last):
+ ...
+ ValueError: May not move item to within itself
+
+It is for this reason that the contents does not support the moveTo operation.
+
+ >>> hasattr(i.contents, 'moveTo')
+ False
+
+If you move an object to the same folder it is a silent noop, unless you are
+using the move as a rename operation and the new name conflicts.
+
+ >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'))
+ >>> i.contents('abe').items()
+ [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+ >>> i.contents('abe')('old_donald').moveTo(i.contents('abe'), 'donald')
+ Traceback (most recent call last):
+ ...
+ ValueError: Object with same name already exists in new location
+ >>> i.contents('abe').items()
+ [('donald', <Demo u'd2'>), ('old_donald', <Demo u'd1'>)]
+ >>> i.contents('abe')('donald').moveTo(i.contents('abe'),
+ ... 'new_donald')
+ >>> i.contents('abe').items()
+ [('old_donald', <Demo u'd1'>), ('new_donald', <Demo u'd2'>)]
+
+Notice in the last part of the example above that the move within the folder
+also changed the order.
+
+It's also interesting to note that, with all these changes, we only have two
+additional changed items: the addition of new_donald, and the changed
+containment of the contents. old_donald, for instance, is not considered to
+be changed; only its containers were.
+
+ >>> changes = dict((getattr(item, 'name', None), item)
+ ... for item in i.iterChangedItems())
+ >>> len(changes)
+ 4
+ >>> changes['fred'].items()
+ [('catherine', <Demo u'c1'>)]
+ >>> changes['abe'].items()
+ [('old_donald', <Demo u'd1'>), ('new_donald', <Demo u'd2'>)]
+ >>> changes['new_donald'].object
+ <Demo u'd2'>
+ >>> list(changes[None].keys())
+ ['barbara', 'abe', 'fred']
+
+Now that we have moved some objects that existed in previous inventories--
+catherine (containing anna) was moved from abe to fred, and donald was moved
+from the root contents to abe and renamed to 'old_donald'--we can examine
+the previous and previous_version pointers.
+
+ >>> i.contents('abe')('old_donald').previous.parent == i.previous.contents
+ True
+ >>> i.contents('abe')('old_donald').previous_version # None
+
+The previous_version is None because, as seen in the iterChangedItems example,
+donald didn't actually change--only its containers did. previous_version does
+work for both local changes and changes in earlier inventories, though.
+
+ >>> list(i.contents('abe').keys())
+ ['old_donald', 'new_donald']
+ >>> list(i.contents('abe').previous.keys())
+ ['catherine']
+ >>> (i.contents('fred')('catherine')('anna').previous.inventory ==
+ ... v.inventory)
+ True
+ >>> (i.contents('fred')('catherine')('anna').previous_version.inventory ==
+ ... v.getInventory(0))
+ True
+
+The previous_version of anna is the first one that was committed in the
+initial inventory--it didn't change in this version, but in the most recently
+committed inventory, so the previous version is the very first one committed.
+
+By the way, notice that, while previous and previous_version point to the
+inventories from which the given item came, the historical, versioned
+inventories in the vault don't point to this working inventory in next or
+next_version because this inventory has not been committed yet.
+
+ >>> v.inventory.contents('abe').next # None
+ >>> v.inventory.contents('abe').next_version # None
+
+As mentioned above, only inventory items support `moveTo`, not the top-node
+inventory contents. Both contents and inventory items support a `copyTo`
+method. This is similar to moveTo but it creates new additional locations in
+the inventory for the same objects; the new locations don't maintain any
+history. It is largely a short hand for doing "location1['foo'] =
+location2['foo']" for all objects in a part of the inventory. The only
+difference is when copying between inventories, as we will see below.
+
+The basic `copyTo` machinery is very similar to `moveTo`. We'll first copy
+catherine and anna to within the contents.
+
+ >>> i.contents('fred')('catherine').copyTo(i.contents)
+ >>> list(i.contents.keys())
+ ['barbara', 'abe', 'fred', 'catherine']
+ >>> list(i.contents('catherine').keys())
+ ['anna']
+ >>> i.contents['catherine'] is i.contents('fred')['catherine']
+ True
+ >>> (i.contents('catherine')('anna').object is
+ ... i.contents('fred')('catherine')('anna').object)
+ True
+
+Now our hierarchy looks like this ("*" indicates a changed or new node)::
+
+ (top node)*
+ --------/ / \ \-----------
+ / / \ \
+ / / \ \
+ 'barbara' 'abe'* 'fred'* 'catherine'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'f1'> <Demo u'c1'>
+ / \ | |
+ / \ | |
+ 'new_donald'* 'old_donald' 'catherine' 'anna'*
+ <Demo u'd2'> <Demo u'd1'> <Demo u'c1'> <Demo ...>
+ |
+ |
+ 'anna'
+ <Demo ...>
+
+Now we have copied objects from one location to another. The copies are unlike
+the originals because they do not have any history.
+
+ >>> i.contents('fred')('catherine')('anna').previous is None
+ False
+ >>> i.contents('catherine')('anna').previous is None
+ True
+
+However, they do know their copy source.
+
+ >>> (i.contents('catherine')('anna').copy_source ==
+ ... i.contents('fred')('catherine')('anna'))
+ True
+
+As with `moveTo`, you may not override a name, but you may explicitly provide
+one.
+
+ >>> i.contents['anna'] = Demo()
+ >>> i.contents('catherine')('anna').copyTo(i.contents)
+ Traceback (most recent call last):
+ ...
+ ValueError: Object with same name already exists in new location
+ >>> i.contents('catherine')('anna').copyTo(i.contents, 'old_anna')
+ >>> list(i.contents.keys())
+ ['barbara', 'abe', 'fred', 'catherine', 'anna', 'old_anna']
+ >>> del i.contents['anna']
+ >>> del i.contents['old_anna']
+
+Unlike with `moveTo`, if you try to copy a part of the hierarchy on top of
+itself (same location, same name), the inventory will raise an error.
+
+ >>> i.contents('catherine')('anna').copyTo(i.contents('catherine'))
+ Traceback (most recent call last):
+ ...
+ ValueError: Object with same name already exists in new location
+
+You can actually copyTo a location in a completely different inventory, even
+from a separate vault.
+
+ >>> another = app['another'] = Vault()
+ >>> another_i = app['another_i'] = Inventory(vault=another)
+ >>> len(another_i.contents)
+ 0
+ >>> i.contents('abe').copyTo(another_i.contents)
+ >>> another_i.contents['abe']
+ <Demo u'a1'>
+ >>> another_i.contents('abe')['new_donald']
+ <Demo u'd2'>
+ >>> another_i.contents('abe')['old_donald']
+ <Demo u'd1'>
+
+We haven't committed for awhile, so let's commit this third revision. We did
+a lot of deletes, so let's just accept all of the orphan conflicts.
+
+ >>> for item in i.iterOrphanConflicts():
+ ... item.resolveOrphanConflict()
+ ...
+ >>> v.commit(i)
+ >>> checkManifest(v.manifest)
+
+In a future revision of the zc.vault package, it may be possible to move and
+copy between inventories. At the time of writing, this use case is
+unnecessary, and doing so will have unspecified behavior.
+
+We have now discussed the core API for the vault system for basic use. A
+number of other use cases are important, however:
+
+- revert to an older inventory;
+
+- merge concurrent changes;
+
+- track an object in a vault; and
+
+- traverse through a vault using URL or TALES paths.
+
+Reverting to an older inventory is fairly simple: use the 'commitFrom'
+method to copy and commit an older version into a new copy. The same
+works with manifests.
+
+ >>> v.commitFrom(v[0])
+
+The data is now as it was in the old version.
+
+ >>> list(v.inventory.contents.keys())
+ ['barbara', 'abe', 'donald']
+
+Now our hierarchy looks like this again::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara' 'abe' 'donald'
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+The `commitFrom` method will take any committed manifest from a vault that
+shares the same intids utility. It creates a new manifest that duplicates the
+provided one.
+
+ >>> v.inventory.contents('abe')('catherine').previous.parent.name
+ 'fred'
+ >>> v.manifest.previous is v[-2]
+ True
+ >>> v.manifest.base_source is v[-2]
+ True
+ >>> v.manifest.base_source is v[0]
+ False
+ >>> v[-2].base_source is v[-3]
+ True
+
+Note that this approach will cause an error:
+
+ >>> v.commit(Manifest(v[0])) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ OutOfDateError: <zc.vault.core.Manifest object at ...>
+
+Again, use `commitFrom` to revert.
+
+Now we come to the most complex vault use case: concurrent changes to a vault,
+merging inventories. The vault design supports a number of features for these
+sorts of use cases.
+
+The basic merge story is that if one or more commits happen to a vault while
+an inventory from the vault is being worked on, so that the base of a working
+inventory is no longer the most recent committed inventory, and thus cannot
+be committed normally...
+
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> long_running.manifest.base_source is v.manifest
+ True
+ >>> short_running.contents['donald'] = app['d2']
+ >>> short_running.contents.items()
+ [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>), ('donald', <Demo u'd2'>)]
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> short_running.contents('barbara')['fred'] = app['f1']
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> long_running.manifest.base_source is v.manifest
+ False
+ >>> long_running.manifest.base_source is v.manifest.previous.previous
+ True
+ >>> long_running.contents['edna'] = app['e1']
+ >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE
+ [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>),
+ ('donald', <Demo u'd1'>), ('edna', <Demo u'e1'>)]
+ >>> v.commit(long_running) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ OutOfDateError: <zc.vault.core.Manifest object at ...>
+
+...then the inventory can be updated; and, if there are no problems with the
+update, then the inventory can be committed.
+
+short_running, and the head of the vault, looks like this now ("*" indicates a
+change from the previous version)::
+
+ (top node)
+ / | \
+ / | \
+ 'barbara'* 'abe' 'donald'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd2'>
+ | |
+ | |
+ 'fred'* 'catherine'
+ <Demo u'f1'> <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+long_running looks like this::
+
+ (top node)*
+ ------/ / \ \----------
+ / / \ \
+ 'barbara' 'abe' 'donald' 'edna'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'e1'>
+ |
+ |
+ 'catherine'
+ <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+The contents node changed and 'edna' was added.
+
+By default, an update is to the current inventory of the inventory base's vault.
+
+Here's the update. It will produce no conflicts, because the node changes do
+not overlap (review diagrams above).
+
+ >>> long_running.beginUpdate()
+ >>> long_running.updating
+ True
+
+Post-merge, long_running looks like this ('M' indicates a merged node)::
+
+ (top node)*
+ ------/ / \ \----------
+ / / \ \
+ 'barbara'M 'abe' 'donald'M 'edna'*
+ <Demo u'b1'> <Demo u'a1'> <Demo u'd2'> <Demo u'e1'>
+ | |
+ | |
+ 'fred'M 'catherine'
+ <Demo u'f1'> <Demo u'c1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+(ADVANCED)
+
+During an update, the local relationships may not be changed, even though they
+are not versioned.
+
+ >>> long_running.contents('edna').type
+ 'local'
+ >>> long_running.contents('edna').relationship.object = Demo()
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot change local relationships while updating
+ >>> long_running.contents('edna').relationship.object
+ <Demo u'e1'>
+ >>> long_running.contents('edna').relationship._z_versioned
+ False
+ >>> long_running.manifest.getType(long_running.contents.relationship)
+ 'local'
+ >>> long_running.contents.relationship.containment.updateOrder(
+ ... ('abe', 'barbara', 'edna', 'donald'))
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot change local relationships while updating
+ >>> long_running.contents.relationship.containment.keys()
+ ('barbara', 'abe', 'donald', 'edna')
+
+When you change an item or contents, this is hidden by switching to a MODIFIED
+relationship, as seen below.
+
+(end ADVANCED)
+
+Now that we have updated, our `update_source` on the inventory shows the
+inventory used to do the update.
+
+ >>> long_running.manifest.base_source is v[-3]
+ True
+ >>> long_running.manifest.update_source is short_running.manifest
+ True
+
+What changes should the update reflect? iterChangedItems takes an optional
+argument which can use an alternate base to calculate changes, so we can use
+that with the long_running.base to see the effective merges.
+
+ >>> changed = dict((getattr(item, 'name', None), item) for item in
+ ... short_running.iterChangedItems(
+ ... long_running.manifest.base_source))
+ >>> changed['donald'].object.source_name
+ u'd2'
+ >>> changed['fred'].object.source_name
+ u'f1'
+ >>> list(changed['barbara'].keys())
+ ['fred']
+
+Our contents show these merged results.
+
+ >>> list(long_running.contents.keys())
+ ['barbara', 'abe', 'donald', 'edna']
+ >>> long_running.contents['donald'].source_name
+ u'd2'
+ >>> long_running.contents('barbara')['fred'].source_name
+ u'f1'
+
+You cannot update to another inventory until you `abortUpdate` or
+`completeUpdate`, as we discuss far below.
+
+ >>> long_running.beginUpdate(v[-2])
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot begin another update while updating
+
+We'll show `abortUpdate`, then redo the update. A characteristic of
+abortUpdate is that it should revert all changes you made while updating. For
+instance, we'll select another version of the contents and even add an item.
+The changes will all go away when we abort.
+
+ >>> len(list(long_running.iterChangedItems()))
+ 5
+ >>> long_running.contents['fred'] = app['f1']
+ >>> list(long_running.contents.keys())
+ ['barbara', 'abe', 'donald', 'edna', 'fred']
+ >>> len(list(long_running.iterChangedItems()))
+ 6
+ >>> long_running.abortUpdate()
+ >>> long_running.manifest.update_source # None
+ >>> long_running.contents.items() # doctest: +NORMALIZE_WHITESPACE
+ [('barbara', <Demo u'b1'>), ('abe', <Demo u'a1'>),
+ ('donald', <Demo u'd1'>), ('edna', <Demo u'e1'>)]
+ >>> len(list(long_running.iterChangedItems()))
+ 2
+ >>> long_running.beginUpdate()
+ >>> list(long_running.contents.keys())
+ ['barbara', 'abe', 'donald', 'edna']
+ >>> long_running.contents['donald'].source_name
+ u'd2'
+ >>> long_running.contents('barbara')['fred'].source_name
+ u'f1'
+
+Now we'll look around more at the state of things. We can use
+iterChangedItems to get a list of all changed and updated. As already seen in
+the examples, `update_source` on the inventory shows the inventory used to do
+the update.
+
+ >>> updated = {}
+ >>> changed = {}
+ >>> for item in long_running.iterChangedItems():
+ ... name = getattr(item, 'name', None)
+ ... if item.type == interfaces.LOCAL:
+ ... changed[name] = item
+ ... else:
+ ... assert item.type == interfaces.UPDATED
+ ... updated[name] = item
+ ...
+ >>> len(updated)
+ 3
+ >>> updated['donald'].object.source_name
+ u'd2'
+ >>> updated['fred'].object.source_name
+ u'f1'
+ >>> list(updated['barbara'].keys())
+ ['fred']
+ >>> len(changed)
+ 2
+ >>> list(changed[None].keys())
+ ['barbara', 'abe', 'donald', 'edna']
+ >>> changed['edna'].object
+ <Demo u'e1'>
+
+The `has_updated` and `updated_item` attributes, which only come into effect
+when an inventory is in the middle of an update, let you examine the changes
+from a more local perspective.
+
+ >>> long_running.contents('donald').has_local
+ False
+ >>> long_running.contents('donald').has_updated
+ True
+ >>> (long_running.contents('donald').updated_item.relationship is
+ ... long_running.contents('donald').relationship)
+ True
+
+There are three kinds of problems that can prevent a post-merge commit: item
+conflicts, orphans, and parent conflicts. Item conflicts are item updates
+that conflicted with local changes and that the system could not merge (more
+on that below). Orphans are accepted item changes (local or updated) that are
+not accessible from the top contents, and so will be lost. Parent conflicts
+are items that were moved to one location in the source and another location
+in the local changes, and so now have two parents: an illegal state because it
+makes future merges and sane historical analysis difficult.
+
+These three kinds of problem can be analyzed with
+`iterUpdateConflicts`, `iterOrphanConflicts`, and `iterParentConflicts`,
+respectively. We have already seen iterOrphanConflicts. In our current merge,
+we have none of these problems, and we can commit (or completeUpdate)
+successfully.
+
+ >>> list(long_running.iterUpdateConflicts())
+ []
+ >>> list(long_running.iterOrphanConflicts())
+ []
+ >>> list(long_running.iterParentConflicts())
+ []
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+We had a lot of discussion between the most important points here, so to
+review, all we had to do in the simple case was this::
+
+ long_running.beginUpdate()
+ v.commit(long_running)
+
+We could have rejected some of the updates and local changes, which might
+have made things more interesting; and the two steps let you analyze the update
+changes to tweak things as desired. But the simplest case allows a simple
+spelling.
+
+Now let's explore the possible merging problems. The first, and arguably most
+complex, is item conflict. An item conflict is easy to provoke. We can do it
+by manipulating the containment or the object of an item. Here we'll
+manipulate the containment order of the root.
+
+ >>> list(v.inventory.contents.keys())
+ ['barbara', 'abe', 'donald', 'edna']
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> short_running.contents.updateOrder(
+ ... ('abe', 'barbara', 'edna', 'donald'))
+ >>> long_running.contents.updateOrder(
+ ... ('abe', 'barbara', 'donald', 'edna'))
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> long_running.beginUpdate()
+ >>> v.commit(long_running)
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot complete update with conflicts
+ >>> conflicts = list(long_running.iterUpdateConflicts())
+ >>> len(conflicts)
+ 1
+ >>> conflict = conflicts[0]
+ >>> conflict.type
+ 'local'
+ >>> list(conflict.keys())
+ ['abe', 'barbara', 'donald', 'edna']
+ >>> conflict.is_update_conflict
+ True
+ >>> conflict.selected
+ True
+ >>> conflict.has_updated
+ True
+ >>> list(conflict.updated_item.keys())
+ ['abe', 'barbara', 'edna', 'donald']
+
+As you can see, we have the tools to find out the conflicts and examine them.
+To resolve this conflict, we merely need to use the `resolveUpdateConflict`
+method. We can select the desired one we want, or even create a new one and
+modify it, before or after marking it resolved.
+
+Let's create a new one. All you have to do is start changing the item, and a
+new one is created. You are not allowed to directly modify local changes when
+you are updating, so that the system can revert to them; but you may create
+'modified' versions (that will be discarded if the update is aborted).
+
+ >>> len(list(conflict.iterModifiedItems()))
+ 0
+ >>> conflict.has_modified
+ False
+ >>> conflict.selected
+ True
+ >>> conflict.type
+ 'local'
+ >>> list(conflict.keys())
+ ['abe', 'barbara', 'donald', 'edna']
+ >>> conflict.updateOrder(['abe', 'donald', 'barbara', 'edna'])
+ >>> len(list(conflict.iterModifiedItems()))
+ 1
+ >>> conflict.has_modified
+ True
+ >>> conflict.selected
+ True
+ >>> conflict.type
+ 'modified'
+ >>> conflict.copy_source.type
+ 'local'
+ >>> conflict.copy_source == conflict.local_item
+ True
+ >>> conflict == list(conflict.iterModifiedItems())[0]
+ True
+ >>> list(conflict.local_item.keys())
+ ['abe', 'barbara', 'donald', 'edna']
+ >>> list(conflict.keys())
+ ['abe', 'donald', 'barbara', 'edna']
+ >>> list(conflict.updated_item.keys())
+ ['abe', 'barbara', 'edna', 'donald']
+
+Now we're going to resolve it.
+
+ >>> conflict.resolveUpdateConflict()
+ >>> conflict.is_update_conflict
+ False
+ >>> len(list(long_running.iterUpdateConflicts()))
+ 0
+ >>> resolved = list(long_running.iterUpdateResolutions())
+ >>> len(resolved)
+ 1
+ >>> resolved[0] == conflict
+ True
+
+Now if we called abortUpdate, the local_item would look the way it did before
+the update, because we modified a separate object. Let's commit, though.
+
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+Our hierarchy looks like this now::
+
+ (top node)*
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald'M 'barbara'M 'edna'*
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ | |
+ | |
+ 'catherine' 'fred'M
+ <Demo u'c1'> <Demo u'f1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+The vault code allows for adapters to try and suggest merges. For instance, a
+simple merge might have a policy that one version with an object change and
+another version with a containment change can be merged simply. This uses
+some APIs we haven't talked about yet: if there is a core.txt in this
+directory, you're in luck; otherwise, hope for help in interfaces.py and
+bother Gary for docs (sorry).
+
+ >>> from zc.vault.core import Relationship
+ >>> @component.adapter(interfaces.IVault)
+ ... @interface.implementer(interfaces.IConflictResolver)
+ ... def factory(vault):
+ ... def resolver(manifest, local, updated, base):
+ ... if local.object is not base.object:
+ ... if updated.object is base.object:
+ ... object = local.object
+ ... else:
+ ... return
+ ... else:
+ ... object = updated.object
+ ... if local.containment != base.containment:
+ ... if updated.containment != base.containment:
+ ... return
+ ... else:
+ ... containment = local.containment
+ ... else:
+ ... containment = updated.containment
+ ... suggested = Relationship(local.token, object, containment)
+ ... manifest.addSuggested(suggested)
+ ... manifest.select(suggested)
+ ... manifest.resolveUpdateConflict(local.token)
+ ... return resolver
+ ...
+ >>> component.provideAdapter(factory)
+
+Now if we merge changes that this policy can handle, we'll have smooth updates.
+
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> app['c2'] = Demo()
+ >>> short_running.contents('abe')['catherine'] = app['c2']
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> long_running.contents('abe')('catherine')['fred'] = app['f1']
+ >>> long_running.beginUpdate()
+ >>> cath = long_running.contents('abe')('catherine')
+ >>> cath.has_suggested
+ True
+ >>> cath.type
+ 'suggested'
+ >>> cath.has_updated
+ True
+ >>> cath.selected
+ True
+ >>> cath.has_local
+ True
+ >>> suggestedItems = list(cath.iterSuggestedItems())
+ >>> len(suggestedItems)
+ 1
+ >>> suggestedItems[0] == cath
+ True
+ >>> cath.object.source_name
+ u'c2'
+ >>> list(cath.keys())
+ ['anna', 'fred']
+ >>> cath.local_item.object
+ <Demo u'c1'>
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+This means we automatically merged this... ::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ | |
+ | |
+ 'catherine'* 'fred'
+ <Demo u'c2'> <Demo u'f1'>
+ |
+ |
+ 'anna'
+ <Demo u'a1'>
+
+...with this (that would normally produce a conflict with the 'catherine'
+node)... ::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ | |
+ | |
+ 'catherine'* 'fred'
+ <Demo u'c1'> <Demo u'f1'>
+ / \
+ / \
+ 'anna' 'fred'*
+ <Demo u'a1'> <Demo u'f1'>
+
+...to produce this::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ | |
+ | |
+ 'catherine'* 'fred'
+ <Demo u'c2'> <Demo u'f1'>
+ / \
+ / \
+ 'anna' 'fred'*
+ <Demo u'a1'> <Demo u'f1'>
+
+This concludes our tour of item conflicts. We are left with orphans and
+parent conflicts.
+
+As mentioned above, orphans are accepted, changed items, typically from the
+update or local changes, that are inaccessible from the root of the inventory.
+For example, consider the following.
+
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> list(short_running.contents('abe').keys())
+ ['catherine']
+ >>> list(short_running.contents('abe')('catherine').keys())
+ ['anna', 'fred']
+ >>> del short_running.contents('abe')['catherine']
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> long_running.contents('abe')('catherine')['anna'] = Demo()
+ >>> long_running.beginUpdate()
+ >>> v.commit(long_running)
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot complete update with conflicts
+ >>> orphans =list(long_running.iterOrphanConflicts())
+ >>> len(orphans)
+ 1
+ >>> orphan = orphans[0]
+ >>> orphan.parent.name
+ 'catherine'
+ >>> orphan.selected
+ True
+ >>> orphan.type
+ 'local'
+ >>> orphan.parent.selected
+ True
+ >>> orphan.parent.type
+ 'base'
+ >>> orphan.parent.parent.type
+ 'base'
+ >>> orphan.parent.parent.selected
+ False
+ >>> orphan.parent.parent.selected_item.type
+ 'updated'
+
+To reiterate in a diagram, the short_running inventory deleted the
+'catherine' branch::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ |
+ |
+ 'fred'
+ <Demo u'f1'>
+
+However, the long running branch made a change to an object that had
+been removed ('anna')::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ | |
+ | |
+ 'catherine' 'fred'
+ <Demo u'c2'> <Demo u'f1'>
+ / \
+ / \
+ 'anna'* 'fred'
+ <Demo ...> <Demo u'f1'>
+
+So, given the orphan, you can discover the old version of the node that let the
+change occur, and thus the change that hid the orphan.
+
+To resolve an orphan, as seen before, you can `resolveOrphanConflict`, or
+somehow change the tree so that the orphan is within the tree again (using
+`moveTo`). We'll just resolve it. Note that resolving keeps it selected: it
+just stops the complaining.
+
+ >>> orphan.selected
+ True
+ >>> orphan.resolveOrphanConflict()
+ >>> orphan.selected
+ True
+ >>> len(list(long_running.iterOrphanConflicts()))
+ 0
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+The same happens if the change occurs because of a reversal--the long_running
+inventory performs the delete.
+
+It also can happen if the user explicitly selects a choice that eliminates an
+accepted change, even outside of a merge, as we have seen above.
+
+Parent conflicts are the last sort of conflict.
+
+Our hierarchy now looks like this::
+
+ (top node)
+ ----------/ / \ \----------
+ / / \ \
+ 'abe' 'donald' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'>
+ |
+ |
+ 'fred'
+ <Demo u'f1'>
+
+The short_running version will be changed to look like this::
+
+ (top node)
+ ------/ | \-------
+ / | \
+ 'abe' 'barbara'* 'edna'
+ <Demo u'a1'> <Demo u'b1'> <Demo u'e1'>
+ / \
+ / \
+ 'fred' 'donald'
+ <Demo u'f1'> <Demo u'd2'>
+
+The long_running version will look like this. ::
+
+ (top node)
+ ------/ | \-------
+ / | \
+ 'abe' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'b1'> <Demo u'e1'>
+ |
+ |
+ 'fred'*
+ <Demo u'f1'>
+ |
+ |
+ 'donald'
+ <Demo u'd2'>
+
+Post-merge the tree looks like this::
+
+ (top node)
+ ------/ | \-------
+ / | \
+ 'abe' 'barbara'* 'edna'
+ <Demo u'a1'> <Demo u'b1'> <Demo u'e1'>
+ / \
+ / \
+ 'fred'* 'donald'
+ <Demo u'f1'> <Demo u'd2'>
+ |
+ |
+ 'donald'
+ <Demo u'd2'>
+
+The problem is Donald. It is one token in two or more places: a parent
+conflict.
+
+ >>> short_running = Inventory(Manifest(v.manifest))
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> short_running.contents('donald').moveTo(
+ ... short_running.contents('barbara'))
+ >>> v.commit(short_running)
+ >>> checkManifest(v.manifest)
+ >>> long_running.contents('donald').moveTo(
+ ... long_running.contents('barbara')('fred'))
+ >>> long_running.beginUpdate()
+ >>> conflicts = list(long_running.iterParentConflicts())
+ >>> v.commit(long_running)
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot complete update with conflicts
+ >>> conflicts = list(long_running.iterParentConflicts())
+ >>> len(conflicts)
+ 1
+ >>> conflict = conflicts[0]
+ >>> conflict.name
+ Traceback (most recent call last):
+ ...
+ ParentConflictError
+ >>> conflict.parent
+ Traceback (most recent call last):
+ ...
+ ParentConflictError
+ >>> selected = list(conflict.iterSelectedParents())
+ >>> len(selected)
+ 2
+ >>> sorted((s.type, s.name) for s in selected)
+ [('local', 'fred'), ('updated', 'barbara')]
+ >>> all = dict((s.type, s) for s in conflict.iterParents())
+ >>> len(all)
+ 3
+ >>> sorted(all)
+ ['base', 'local', 'updated']
+
+You can provoke these just by accepting a previous version, outside of merges.
+For instance, we can now make a three-way parent conflict by selecting the
+root node.
+
+ >>> all['base'].select()
+ >>> selected = list(conflict.iterSelectedParents())
+ >>> len(selected)
+ 3
+
+Now if we resolve the original problem by rejecting the local change,
+we'll still have a problem, because of accepting the baseParent.
+
+ >>> all['local'].base_item.select()
+ >>> selected = list(conflict.iterSelectedParents())
+ >>> len(selected)
+ 2
+ >>> v.commit(long_running)
+ Traceback (most recent call last):
+ ...
+ UpdateError: cannot complete update with conflicts
+ >>> all['base'].local_item.select()
+ >>> len(list(long_running.iterParentConflicts()))
+ 0
+
+Now our hierarchy looks like short_running again::
+
+ (top node)
+ ------/ | \-------
+ / | \
+ 'abe' 'barbara' 'edna'
+ <Demo u'a1'> <Demo u'b1'> <Demo u'e1'>
+ / \
+ / \
+ 'fred' 'donald'
+ <Demo u'f1'> <Demo u'd2'>
+
+We can't check this in because there are no effective changes between this
+and the last checkin.
+
+ >>> v.commit(long_running) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ NoChangesError: <zc.vault.core.Manifest object at ...>
+
+So actually, we'll reinstate the local change, reject the short_running
+change (the placement within barbara), and commit.
+
+ >>> all['local'].select()
+ >>> all['updated'].base_item.select()
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+Note that even though we selected the base_item, the relationship generated by
+completing the update is actually local because it is a change from the
+previous updated source.
+
+ >>> v.inventory.contents('barbara').type
+ 'local'
+
+There is actually a fourth kind of error: having child nodes in selected
+relationships for which there are no selected relationships. The code tries to
+disallow this, so it should not be encountered.
+
+Next, we will talk about using vaults to create and manage branches.
+The simple basics of this are that you can commit an inventory based on one
+vault into a fresh vault, and you can then update across the two vaults. To
+create a vault that can have merged manifests, you must share the internal
+'intids' attribute. The `createBranch` method is sugar for doing that and then
+(by default) committing the most recent manifest of the current vault as the first
+revision of the branch.
+
+ >>> branch = app['branch'] = v.createBranch()
+ >>> bi = Inventory(Manifest(branch.manifest))
+ >>> branch_start_inventory = v.inventory
+ >>> bi.contents['george'] = Demo()
+ >>> branch.commit(bi)
+ >>> checkManifest(branch.manifest)
+ >>> i = Inventory(Manifest(v.manifest))
+ >>> i.contents['barbara'] = app['b2'] = Demo()
+ >>> v.commit(i)
+ >>> checkManifest(v.manifest)
+ >>> i.contents['barbara'].source_name
+ u'b2'
+ >>> bi = Inventory(Manifest(branch.manifest))
+ >>> bi.contents('barbara')['henry'] = app['h1'] = Demo()
+ >>> branch.commit(bi)
+ >>> checkManifest(branch.manifest)
+
+Now we want to merge the mainline changes with the branch.
+
+ >>> bi = Inventory(Manifest(branch.manifest))
+ >>> (bi.manifest.base_source is bi.manifest.getBaseSource(branch) is
+ ... branch.manifest)
+ True
+ >>> (bi.manifest.getBaseSource(v) is branch_start_inventory.manifest is
+ ... v[-2])
+ True
+ >>> bi.beginUpdate(v.inventory)
+ >>> bi.contents['barbara'].source_name
+ u'b2'
+ >>> bi.contents('barbara')['henry'].source_name
+ u'h1'
+
+A smooth update. But what happens if meanwhile someone changes the branch,
+before this is committed? We use `completeUpdate`, and then update again on
+the branch. `completeUpdate` moves all selected changes to be `local`,
+whatever the source, the same way commit does (in fact, commit uses
+completeUpdate).
+
+ >>> bi2 = Inventory(Manifest(branch.manifest))
+ >>> bi2.contents['edna'] = app['e2'] = Demo()
+ >>> branch.commit(bi2)
+ >>> checkManifest(branch.manifest)
+ >>> branch.commit(bi) # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ OutOfDateError: <zc.vault.core.Manifest object at ...>
+ >>> bi.completeUpdate()
+ >>> bi.beginUpdate()
+ >>> branch.commit(bi)
+ >>> checkManifest(branch.manifest)
+
+Once we have done this, the head of the branch is based on the head of the
+original vault, so we can immediately check in a branch inventory in the
+trunk inventory.
+
+ >>> v.commit(Inventory(Manifest(branch.manifest)))
+ >>> checkManifest(v.manifest)
+
+Finally, cherry-picking changes is possible as well, though it can
+cause normal updates to be confused. `beginCollectionUpdate` takes an
+iterable of items (such as is produced by iterChangedItems) and applies
+the update with the usual conflict and examination approaches we've
+seen above. `completeUpdate` can then accept the changes for
+additional updates.
+
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> discarded = Inventory(Manifest(v.manifest))
+ >>> discarded.contents['ignatius'] = app['i1'] = Demo()
+ >>> discarded.contents['jacobus'] = app['j1'] = Demo()
+ >>> long_running.beginCollectionUpdate((discarded.contents('ignatius'),))
+ >>> len(list(long_running.iterOrphanConflicts()))
+ 1
+ >>> o = iter(long_running.iterOrphanConflicts()).next()
+ >>> o.selected
+ True
+ >>> o.name # None
+ >>> o.parent # None
+ >>> o.object
+ <Demo u'i1'>
+ >>> o.moveTo(long_running.contents, 'ignatius')
+ >>> len(list(long_running.iterOrphanConflicts()))
+ 0
+ >>> long_running.contents['ignatius']
+ <Demo u'i1'>
+ >>> long_running.contents('ignatius')['jacobus'] = app['j1']
+ >>> list(long_running.contents('ignatius').keys())
+ ['jacobus']
+ >>> long_running.contents('ignatius')('jacobus').selected
+ True
+ >>> list(discarded.contents('ignatius').keys())
+ []
+ >>> v.commit(long_running)
+ >>> checkManifest(v.manifest)
+
+The code will stop you if you try to add a set of relationships that result in
+the manifest having keys that don't map to values--or more precisely, child
+tokens that don't have matching selected relationships. For instance, consider
+this.
+
+ >>> long_running = Inventory(Manifest(v.manifest))
+ >>> discarded = Inventory(Manifest(v.manifest))
+ >>> discarded.contents['katrina'] = app['k1'] = Demo()
+ >>> discarded.contents('katrina')['loyola'] = app['l1'] = Demo()
+ >>> long_running.beginCollectionUpdate((discarded.contents('katrina'),))
+ ... # doctest: +ELLIPSIS
+ Traceback (most recent call last):
+ ...
+ ValueError: cannot update from a set that includes children tokens...
+
+It is disallowed because the katrina node includes the 'loyola' node, but we
+didn't include the matching 'loyola' item.
+
+If you include both, the merge will proceed as usual.
+
+ >>> long_running.beginCollectionUpdate(
+ ... (discarded.contents('katrina'),
+ ... discarded.contents('katrina')('loyola')))
+ >>> long_running.updating
+ True
+ >>> len(list((long_running.iterOrphanConflicts())))
+ 2
+ >>> orphans = dict((o.name, o) for o in long_running.iterOrphanConflicts())
+ >>> orphans[None].moveTo(long_running.contents, 'katrina')
+ >>> long_running.contents['katrina']
+ <Demo u'k1'>
+ >>> long_running.contents('katrina')['loyola']
+ <Demo u'l1'>
+
+The combination of `beginCollectionUpdate` and `iterChangedItems` can provide
+a powerful way to apply arbitrary changesets to a revision.
+
+Storing None
+============
+
+Sometimes you want to just make an empty node for organizational purposes.
+While normally stored objects must be versionable and adaptable to
+IKeyReference, None is a special case. We can store None in any node. Let's
+make a quick example.
+
+ >>> v = app['v'] = Vault()
+ >>> i = Inventory(vault=v)
+ >>> i.contents['foo'] = None
+ >>> i.contents('foo')['bar'] = None
+ >>> i.contents('foo')('bar')['baz'] = app['d1']
+ >>> i.contents['foo'] # None
+ >>> i.contents('foo')['bar'] # None
+ >>> i.contents('foo')('bar')['baz'] is app['d1']
+ True
+ >>> i.contents['bing'] = app['a1']
+ >>> i.contents['bing'] is app['a1']
+ True
+ >>> v.commit(i)
+ >>> i = Inventory(vault=v, mutable=True)
+ >>> i.contents['bing'] = None
+ >>> del i.contents('foo')['bar']
+ >>> i.contents['foo'] = app['d1']
+ >>> v.commit(i)
+ >>> v.inventory.contents.previous['bing'] is app['a1']
+ True
+ >>> v.inventory.contents.previous['foo'] is None
+ True
+
+Special "held" Containers
+=========================
+
+It is sometimes useful to specify a "held" container for all objects stored
+in a vault, overriding the "held" containers for each manifest as described
+above. Vaults can be instantiated with specifying a held container.
+
+ >>> from zc.vault.core import HeldContainer
+ >>> held = app['held'] = HeldContainer()
+ >>> v = app['vault_held'] = Vault(held=held)
+ >>> i = Inventory(vault=v)
+ >>> o = i.contents['foo'] = Demo()
+ >>> o.__parent__ is held
+ True
+ >>> held[o.__name__] is o
+ True
+
+If you create a branch, by default it will use the same held container.
+
+ >>> v.commit(i)
+ >>> v2 = app['vault_held2'] = v.createBranch()
+ >>> i2 = Inventory(vault=v2, mutable=True)
+ >>> o2 = i2.contents['bar'] = Demo()
+ >>> o2.__parent__ is held
+ True
+ >>> held[o2.__name__] is o2
+ True
+
+You can also specify another held container when you create a branch.
+
+ >>> another_held = app['another_held'] = HeldContainer()
+ >>> v3 = app['vault_held3'] = v.createBranch(held=another_held)
+ >>> i3 = Inventory(vault=v3, mutable=True)
+ >>> o3 = i3.contents['baz'] = Demo()
+ >>> o3.__parent__ is another_held
+ True
+ >>> another_held[o3.__name__] is o3
+ True
+
+Committing the transaction
+==========================
+
+We'll make sure that all these changes can in fact be committed to the ZODB.
+
+ >>> import transaction
+ >>> transaction.commit()
+
+-----------
+
+...commit messages? Could be added to event, so object log could use.
+
+Need commit datetime stamp, users. Handled now by objectlog.
+
+Show traversal adapters that use zc.shortcut code...
+
+Talk about tokens.
+
+Then talk about use case of having a reference be updated to a given object
+within a vault...
+
+...a vault mirror that also keeps track of hierarchy?
+
+A special reference that knows both vault and token?
+
Property changes on: zc.vault/trunk/src/zc/vault/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.vault/trunk/src/zc/vault/__init__.py
===================================================================
--- zc.vault/trunk/src/zc/vault/__init__.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/__init__.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1 @@
+#
Added: zc.vault/trunk/src/zc/vault/catalog.py
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,221 @@
+from BTrees import IOBTree, IFBTree
+from zc.vault import interfaces, keyref
+from zope import component, interface
+import persistent
+import zope.app.container.interfaces
+import zope.app.intid.interfaces
+import zope.component.interfaces
+import zope.event
+import zope.lifecycleevent
+import zope.lifecycleevent.interfaces
+import zope.location
+
+# from a site, to get to the default package, the incantation is
+# site.getSiteManager()['default']
+
+def addLocalUtility(package, utility, interface=None,
+ name='', name_in_container='', comment=u''):
+ chooser = zope.app.container.interfaces.INameChooser(package)
+ name_in_container = chooser.chooseName(name_in_container, utility)
+ zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(utility))
+ package[name_in_container] = utility
+ # really want IComponentRegistry, but that is not set up in Zope 3 ATM
+ registry = zope.component.interfaces.IComponentLookup(package)
+ registry.registerUtility(utility, interface, name, comment)
+
+HISTORICAL = 'zc.vault.historical'
+CURRENT = 'zc.vault.current'
+WORKING = 'zc.vault.working'
+# the following constant is not API
+REVERSE = 'zc.vault.working_reverse'
+
+class IRevisionReferences(interface.Interface):
+ historical = interface.Attribute(
+ '''an object with a standard mapping get method: stores
+ object intid -> set of historical manifest intids''')
+ current = interface.Attribute(
+ '''an object with a standard mapping get method: stores
+ object intid -> set of historical manifest intids''')
+ working = interface.Attribute(
+ '''an object with a standard mapping get method: stores
+ object intid -> set of historical manifest intids''')
+
+class RevisionReferencesMappings(persistent.Persistent):
+
+ def __init__(self):
+ self.references = IOBTree.IOBTree()
+
+ def _getrefs(self, key):
+ refs = self.references.get(key)
+ if refs is None:
+ refs = self.references[key] = IFBTree.IFTreeSet()
+ return refs
+
+ def add(self, key, value):
+ self._getrefs(key).insert(value)
+
+ def update(self, key, values):
+ self._getrefs(key).update(values)
+
+ def remove(self, key, value):
+ refs = self.references.get(key)
+ if refs is not None:
+ refs.remove(value) # raises KeyError when we desire
+ if not refs:
+ del self.references[key]
+ else:
+ raise KeyError("key and value pair does not exist")
+
+ def discard(self, key, value):
+ try:
+ self.remove(key, value)
+ except KeyError:
+ pass
+
+ def contains(self, key, value):
+ refs = self.references.get(key)
+ if refs is not None:
+ return value in refs
+ return False
+
+ def set(self, key, values):
+ refs = self.references.get(key)
+ vals = tuple(values)
+ if not vals:
+ if refs is not None:
+ # del
+ del self.references[key]
+ else:
+ if refs is None:
+ refs = self.references[key] = IFBTree.IFTreeSet()
+ else:
+ refs.clear()
+ refs.update(vals)
+
+ def get(self, key):
+ return self.references.get(key, ())
+
+class RevisionReferences(persistent.Persistent, zope.location.Location):
+
+ interface.implements(IRevisionReferences)
+
+ __parent__ = __name__ = None
+
+ def __init__(self):
+ self.historical = RevisionReferencesMappings()
+ self.current = RevisionReferencesMappings()
+ self.working = RevisionReferencesMappings()
+ self.reverse = RevisionReferencesMappings()
+
+def createRevisionReferences(package):
+ utility = RevisionReferences()
+ chooser = zope.app.container.interfaces.INameChooser(package)
+ name = chooser.chooseName('zc_vault_revision_references', utility)
+ zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(utility))
+ package[name] = utility
+ # really want IComponentRegistry, but that is not set up in Zope 3 ATM
+ registry = zope.component.interfaces.IComponentLookup(package)
+ registry.registerUtility(utility, IRevisionReferences, '', '')
+
+ at component.adapter(interfaces.IManifestCommitted)
+def makeReferences(ev):
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man = ev.object
+ man_id = intids.register(man)
+ prev = man.previous
+ if prev is not None:
+ prev_id = intids.register(prev)
+ for rel in prev:
+ if rel.token is not man.vault.top_token:
+ o_id = intids.register(rel.object)
+ refs.current.discard(o_id, prev_id)
+ refs.historical.add(o_id, prev_id)
+ for o_id in refs.reverse.get(man_id):
+ refs.working.remove(o_id, man_id)
+ refs.reverse.set(man_id, ())
+ for rel in man:
+ if rel.token is not man.vault.top_token:
+ refs.current.add(intids.register(rel.object), man_id)
+
+ at component.adapter(interfaces.IUpdateCompleted)
+def updateCompleted(ev):
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man = ev.object
+ man_id = intids.register(man)
+ for o_id in refs.reverse.get(man_id):
+ refs.working.remove(o_id, man_id)
+ refs.reverse.set(man_id, ())
+ for rel in man.iterSelections():
+ if rel.token is not man.vault.top_token:
+ o_id = intids.register(rel.object)
+ refs.working.add(o_id, man_id)
+ refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IUpdateAborted)
+def updateAborted(ev):
+ updateCompleted(ev)
+
+ at component.adapter(
+ interfaces.IManifest, zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def manifestCreated(man, ev):
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man = ev.object
+ man_id = intids.register(man)
+ for rel in man:
+ if rel.token is not man.vault.top_token:
+ o_id = intids.register(rel.object)
+ refs.working.add(o_id, man_id)
+ refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IRelationshipSelected)
+def relationshipSelected(ev):
+ rel = ev.object
+ if rel.token is not rel.__parent__.vault.top_token:
+ man = ev.manifest
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man_id = intids.register(man)
+ o_id = intids.register(rel.object)
+ refs.working.add(o_id, man_id)
+ refs.reverse.add(man_id, o_id)
+
+ at component.adapter(interfaces.IRelationshipDeselected)
+def relationshipDeselected(ev):
+ rel = ev.object
+ if rel.token is not rel.__parent__.vault.top_token:
+ man = ev.manifest
+ for other in man:
+ if other.object is rel.object:
+ break
+ else:
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man_id = intids.register(man)
+ o_id = intids.register(rel.object)
+ refs.working.remove(o_id, man_id)
+ refs.reverse.remove(man_id, o_id)
+
+ at component.adapter(interfaces.IObjectChanged)
+def objectChanged(ev):
+ rel = ev.object
+ if rel.token is not rel.__parent__.vault.top_token:
+ man = rel.__parent__
+ if man is not None and man.isSelected(rel):
+ refs = component.getUtility(IRevisionReferences)
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ man_id = intids.register(man)
+ o_id = intids.register(rel.object)
+ previous = ev.previous
+ if previous is not None:
+ for other in man:
+ if other.object is previous:
+ break
+ else:
+ p_id = intids.register(previous)
+ refs.working.remove(p_id, man_id)
+ refs.reverse.remove(man_id, p_id)
+ refs.working.add(o_id, man_id)
+ refs.reverse.add(man_id, o_id)
Added: zc.vault/trunk/src/zc/vault/catalog.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.txt 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.txt 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,196 @@
+When wanting to index objects in a vault, we have identified at least three
+scenarios.
+
+- You may want to find unique objects irrespective of their effective locations
+ within a vault.
+
+- You may want to index the objects within a vault, tracking the most recent
+ manifest revision, treating identical objects in different vault locations as
+ separate searchable entities.
+
+- As a variation of the second story, you may want to search through all past
+ vault revisions, not just the most recent one.
+
+The way the catalog.py module approaches this is to provide three
+extrinsicreferences-like mapping objects to historical revisions, current
+revisions, and uncommitted versions, respectively, that use them (a fourth
+similar data structure is included for bookkeeping, but is not part of the
+advertised API). Then it provides a number of subscribers to keep these
+data structures up-to-date.
+
+Let's run the code through its paces. First we want to run the code that adds
+the references. It is a function that expects a package.
+
+ >>> from zc.vault import catalog
+ >>> sm = app.getSiteManager()
+ >>> package = sm['default']
+ >>> catalog.createRevisionReferences(package)
+
+Now we should be able to get the utilities. Note that the
+catalog module provides constants for the utility names.
+
+ >>> from zope import component
+ >>> refs = component.getUtility(catalog.IRevisionReferences)
+
+Now we can actually create a vault and watch the references change.
+
+ >>> from zc.vault.vault import Vault, Inventory
+ >>> v = Vault()
+ >>> app['vault'] = v
+ >>> i = Inventory(vault=v)
+ >>> import persistent
+ >>> from zope.app.container.contained import Contained
+ >>> from zc.copyversion.versioning import Versioning
+ >>> class Demo(persistent.Persistent, Contained, Versioning):
+ ... def __repr__(self):
+ ... return "<%s %r>" % (self.__class__.__name__, self.__name__)
+ ...
+ >>> i.contents[u'able'] = app['a1'] = Demo()
+
+Initially, an object is added to a working inventory (or manifest).
+The refs put a relationship in the working category.
+
+ >>> from zope.app.intid.interfaces import IIntIds
+ >>> intids = component.getUtility(IIntIds)
+ >>> working = refs.working.get(intids.getId(app['a1']))
+ >>> len(working)
+ 1
+ >>> list(working) == [intids.getId(i.manifest)]
+ True
+ >>> len(refs.current.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.historical.get(intids.getId(app['a1'])))
+ 0
+
+Once we commit the inventory, the relationship moves to the current category.
+
+ >>> v.commit(i)
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.historical.get(intids.getId(app['a1'])))
+ 0
+ >>> current = refs.current.get(intids.getId(app['a1']))
+ >>> len(current)
+ 1
+ >>> list(current) == [intids.getId(i.manifest)]
+ True
+
+If we create a new inventory, the object is in both working and current.
+
+ >>> i = Inventory(vault=v, mutable=True)
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 1
+ >>> working = refs.working.get(intids.getId(app['a1']))
+ >>> len(working)
+ 1
+ >>> list(working) == [intids.getId(i.manifest)]
+ True
+ >>> current = refs.current.get(intids.getId(app['a1']))
+ >>> len(current)
+ 1
+ >>> list(current) == [intids.getId(v.manifest)]
+ True
+ >>> len(refs.historical.get(intids.getId(app['a1'])))
+ 0
+
+Commit that one, and we have one in historical and one in current.
+
+ >>> i.contents[u'disiri'] = app['d1'] = Demo() # so there's a change
+ >>> v.commit(i)
+ >>> historical = refs.historical.get(intids.getId(app['a1']))
+ >>> len(historical)
+ 1
+ >>> list(historical) == [intids.getId(v[0])]
+ True
+ >>> current = refs.current.get(intids.getId(app['a1']))
+ >>> len(current)
+ 1
+ >>> list(current) == [intids.getId(v.manifest)]
+ True
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+
+We'll make another two inventories.
+
+ >>> i = Inventory(vault=v, mutable=True)
+ >>> i2 = Inventory(vault=v, mutable=True)
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 2
+
+Now one of them will replace 'a1' ('able') with another object.
+
+ >>> i.contents['able'] = app['a2'] = Demo()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 1
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 1
+
+Now we'll commit the first inventory.
+
+ >>> v.commit(i)
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 1
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 0
+ >>> len(refs.current.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.current.get(intids.getId(app['a2'])))
+ 1
+ >>> len(refs.historical.get(intids.getId(app['a1'])))
+ 2
+
+If we update i2 (do a merge with the checked-in version) then the references
+correctly change.
+
+ >>> i2.beginUpdate()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 1
+
+If we select a different relationship then the references also correctly
+change.
+
+ >>> i2.contents('able').base_item.select()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 1
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 0
+ >>> i2.contents('able').updated_item.select()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 1
+
+If we abort then the references correctly change.
+
+ >>> i2.abortUpdate()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 1
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 0
+ >>> i2.beginUpdate()
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 1
+
+Now we'll commit i2 and once again check the references.
+
+ >>> i2.contents[u'toug'] = app['t1'] = Demo() # so there's a change
+ >>> v.commit(i2)
+ >>> len(refs.working.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.working.get(intids.getId(app['a2'])))
+ 0
+ >>> len(refs.current.get(intids.getId(app['a1'])))
+ 0
+ >>> len(refs.current.get(intids.getId(app['a2'])))
+ 1
+ >>> len(refs.historical.get(intids.getId(app['a1'])))
+ 2
+ >>> len(refs.historical.get(intids.getId(app['a2'])))
+ 1
+
+The end. Further tests would make sure that selected orphans were
+maintained in working copies.
Property changes on: zc.vault/trunk/src/zc/vault/catalog.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.vault/trunk/src/zc/vault/catalog.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/catalog.zcml 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/catalog.zcml 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zc.vault">
+
+ <subscriber handler=".catalog.makeReferences" />
+
+ <subscriber handler=".catalog.updateCompleted" />
+
+ <subscriber handler=".catalog.updateAborted" />
+
+ <subscriber handler=".catalog.manifestCreated" />
+
+ <subscriber handler=".catalog.relationshipSelected" />
+
+ <subscriber handler=".catalog.relationshipDeselected" />
+
+ <subscriber handler=".catalog.objectChanged" />
+
+</configure>
Added: zc.vault/trunk/src/zc/vault/configure.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/configure.zcml 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/configure.zcml 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zc.task">
+
+ <class class=".core.Mapping">
+ <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+ <require permission="zope.View"
+ interface="zope.interface.common.mapping.IEnumerableMapping" />
+ <require permission="zope.View"
+ attributes="getKey __eq__ __ne__" />
+ <require permission="zope.ManageContent"
+ interface="zope.interface.common.mapping.IWriteMapping" />
+ <require permission="zope.ManageContent"
+ attributes="updateOrder updateOrderFromTokens" />
+ </class>
+
+ <class class=".keyref.Token">
+ <allow interface="zope.app.keyreference.interfaces.IKeyReference"
+ attributes="identifiers" />
+ </class>
+
+ <class class=".keyref._top_token_">
+ <require like_class=".keyref.Token" />
+ </class>
+
+ <class class=".core.Relationship">
+ <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+ <require like_class=".keyref.Token" />
+ <require permission="zope.View"
+ attributes="token object containment children copy_source" />
+ <require permission="zope.ManageContent"
+ set_attributes="object" />
+ </class>
+
+ <class class=".core.HeldContainer">
+ <require permission="zope.View"
+ interface="zope.app.container.interfaces.IReadContainer" />
+ <require permission="zope.ManageContent"
+ interface="zope.app.container.interfaces.IWriteContainer" />
+ </class>
+
+ <class class=".core.Manifest">
+ <require like_class="zc.copyversion.versioning.VersioningAdapter" />
+ <require permission="zope.View"
+ attributes="held vault_index getBaseSources getBaseSource base_source
+ vault merged_sources update_source update_base updating
+ iterChanges get getType isSelected getBase getLocal
+ getUpdated iterSuggested iterModified iterMerged
+ iterSelectedParents iterParents isLinked
+ iterUpdateConflicts iterUpdateResolutions isUpdateConflict
+ iterOrphanConflicts iterOrphanResolutions isOrphan
+ isOrphanConflict iterParentConflicts iterAll __iter__
+ iterUnchangedOrphans previous next isOption" />
+ <require permission="zope.ManageContent"
+ attributes="addLocal addModified beginUpdate beginCollectionUpdate
+ completeUpdate approveRelationshipChange abortUpdate select
+ resolveUpdateConflict resolveOrphanConflict
+ undoOrphanConflictResolution" />
+ <!-- addSuggested and reindex should be forbidden or another permission;
+ currently forbidden -->
+ <require permission="zope.ManageContent"
+ set_attributes="vault_index vault" />
+ </class>
+
+ <class class=".core.Vault">
+ <require permission="zope.View"
+ attributes="intids getPrevious getNext __len__ __getitem__ __iter__
+ manifest" />
+ <require permission="zope.ManageContent"
+ attributes="commit commitOverride" />
+ </class>
+
+ <class class=".vault.InventoryContents">
+ <require permission="zope.View"
+ interface="zope.interface.common.mapping.IEnumerableMapping" />
+ <require permission="zope.View"
+ attributes="getKey __eq__ __ne__ __call__ previous next
+ previous_version next_version type selected
+ is_update_conflict has_base has_local has_updated
+ has_suggested has_modified has_merged base_item local_item
+ updated_item iterSuggestedItems iterModifiedItems
+ iterMergedItems copy_source selected_item
+ relationship inventory" />
+ <require permission="zope.ManageContent"
+ interface="zope.interface.common.mapping.IWriteMapping" />
+ <require permission="zope.ManageContent"
+ attributes="updateOrder updateOrderFromTokens makeMutable select
+ resolveUpdateConflict" />
+ </class>
+
+ <class class=".vault.InventoryItem">
+ <require like_class=".vault.InventoryContents" />
+ <require permission="zope.View"
+ attributes="is_orphan is_orphan_conflict is_parent_conflict parent
+ name iterSelectedParents iterParents object" />
+ <require permission="zope.ManageContent"
+ attributes="resolveOrphanConflict moveTo copyTo"
+ set_attributes="object" />
+ </class>
+
+ <class class=".vault.Inventory">
+ <require permission="zope.View"
+ attributes="__eq__ __ne__ vault contents manifest iterUpdateConflicts
+ iterUpdateResolutions iterOrphanConflicts
+ iterOrphanResolutions iterUnchangedOrphans
+ iterParentConflicts __iter__ updating merged_sources
+ update_source iterChangedItems getItemFromToken previous
+ next" />
+ <require permission="zope.ManageContent"
+ attributes="beginUpdate completeUpdate abortUpdate
+ beginCollectionUpdate"
+ set_attributes="vault" />
+ </class>
+
+ <class class=".vault.Vault">
+ <require like_class=".core.Vault" />
+ <require permission="zope.View"
+ attributes="getInventory inventory" />
+ </class>
+
+</configure>
Added: zc.vault/trunk/src/zc/vault/core.py
===================================================================
--- zc.vault/trunk/src/zc/vault/core.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/core.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,1318 @@
+
+from ZODB.interfaces import IConnection
+from BTrees import IOBTree, OIBTree, OOBTree, IFBTree, Length
+import persistent
+from zope import interface, component, event
+import zope.lifecycleevent
+import zope.location
+import zope.location.interfaces
+import zope.app.container.contained
+from zope.app.container.interfaces import INameChooser
+import zope.app.intid
+import zope.app.intid.interfaces
+from zc.relationship import index
+from zc.copyversion import versioning
+import zc.copyversion.interfaces
+import zope.app.keyreference.interfaces
+# import rwproperty
+from zc.copyversion import rwproperty
+
+from zc.vault import interfaces, keyref
+
+# vault -> relationships -> relationship -> mapping
+# -> held
+
+# XXX Missing relationships (child token without matching selected
+# relationship) is handled but not very gracefully. Especially a problem with
+# beginCollectionUpdate. Other situations are addressed in reindex.
+
+class approvalmethod(object):
+
+ def __init__(self, reindex=False):
+ self.reindex = reindex
+
+ def __call__(self, func):
+ self.func = func
+ return self.wrapper
+
+ def wrapper(self, wself, *args, **kwargs):
+ manifest = None
+ rel = wself.__parent__
+ if rel is not None:
+ manifest = rel.__parent__
+ if manifest is not None:
+ if rel.token is None:
+ raise ValueError(
+ 'cannot change containment without token on '
+ 'relationship')
+ manifest.checkRelationshipChange(rel)
+ self.func(wself, *args, **kwargs)
+ if self.reindex and manifest is not None:
+ manifest.reindex(rel)
+
+class Mapping(persistent.Persistent, versioning.Versioning):
+
+ interface.implements(interfaces.IRelationshipContainment)
+
+ __parent__ = None
+
+ def __init__(self, data=None):
+ self._forward = OIBTree.OIBTree()
+ self._reverse = IOBTree.IOBTree()
+ self._length = Length.Length()
+ self._order = ()
+ if data is not None:
+ self.update(data)
+
+ def __len__(self):
+ return self._length.value
+
+ def __getitem__(self, key):
+ return self._forward[key]
+
+ def get(self, key, default=None):
+ return self._forward.get(key, default)
+
+ def items(self):
+ return [(k, self._forward[k]) for k in self._order]
+
+ def keys(self):
+ return self._order
+
+ def values(self):
+ return [self._forward[k] for k in self._order]
+
+ @property
+ def valuesBTree(self): # for optimization
+ return self._reverse
+
+ @property
+ def keysBTree(self): # for symmetry :-)
+ return self._forward
+
+ def __iter__(self):
+ return iter(self._order)
+
+ def __contains__(self, key):
+ return key in self._forward
+
+ has_key = __contains__
+
+ def getKey(self, value, default=None):
+ if not isinstance(value, int):
+ return default
+ return self._reverse.get(value, default)
+
+ @versioning.method
+ @approvalmethod(reindex=True)
+ def __delitem__(self, key):
+ self._delitem(key)
+
+ def _delitem(self, key):
+ old = self._forward.pop(key)
+ order = list(self._order)
+ order.remove(key)
+ self._order = tuple(order)
+ self._reverse.pop(old)
+ self._length.change(-1)
+ event.notify(interfaces.ObjectRemoved(old, self, key))
+
+ @versioning.method
+ @approvalmethod(reindex=True)
+ def __setitem__(self, key, value):
+ self._setitem(key, value)
+
+ def _setitem(self, key, value):
+ bad = False
+ if isinstance(key, basestring):
+ try:
+ unicode(key)
+ except UnicodeError:
+ bad = True
+ else:
+ bad = True
+ if bad:
+ raise TypeError("'%s' is invalid, the key must be an "
+ "ascii or unicode string" % key)
+ if len(key) == 0:
+ raise ValueError("The key cannot be an empty string")
+ if not isinstance(value, int):
+ raise ValueError("The value must be an integer")
+ old_key = self._reverse.get(value)
+ if old_key is not None:
+ if old_key != key:
+ raise ValueError(
+ 'name mapping can only contain unique values')
+ # else noop
+ else:
+ old_value = self._forward.get(key)
+ if old_value is None:
+ self._order += (key,)
+ self._length.change(1)
+ else:
+ old_old_key = self._reverse.pop(old_value)
+ assert old_old_key == key
+ self._forward[key] = value
+ self._reverse[value] = key
+ if old_value is not None:
+ event.notify(interfaces.ObjectRemoved(old_value, self, key))
+ event.notify(interfaces.ObjectAdded(value, self, key))
+
+ @versioning.method
+ @approvalmethod(reindex=True)
+ def update(self, data):
+ if getattr(data, 'keys', None) is not None:
+ data = [(k, data[k]) for k in data.keys()]
+ if len(self):
+ # since duplication of values is disallowed, we need to remove
+ # any current overlapped values so we don't get spurious errors.
+ keys = set()
+ probs = []
+ for k, v in data:
+ keys.add(k)
+ old_k = self._reverse.get(v)
+ if old_k is not None and old_k != k:
+ probs.append((old_k, v))
+ for k, v in probs:
+ if k not in keys:
+ raise ValueError(
+ 'name mapping can only contain unique values', v)
+ for k, v in probs:
+ self._delitem[k]
+ for k, v in data:
+ self._setitem(k, v)
+
+ @versioning.method
+ @approvalmethod(reindex=False)
+ def updateOrder(self, order):
+ order = tuple(order)
+ if self._order != order:
+ if len(order) != len(self):
+ raise ValueError('Incompatible key set.')
+ for k in order:
+ if k not in self._forward:
+ raise ValueError('Incompatible key set.')
+ old_order = self._order
+ self._order = order
+ event.notify(interfaces.OrderChanged(self, old_order))
+
+ def __eq__(self, other):
+ return self is other or (
+ (interfaces.IBidirectionalNameMapping.providedBy(other) and
+ self.keys() == other.keys() and self.values() == other.values()))
+
+ def __ne__(self, other):
+ return not (self == other)
+
+class Relationship(keyref.Token, versioning.Versioning):
+ interface.implements(interfaces.IRelationship)
+
+ _token = _copy_source = None
+
+ def __init__(self, token=None, object=None, containment=None,
+ relationship=None, source_manifest=None):
+ if source_manifest is not None:
+ if relationship is None:
+ raise ValueError(
+ 'source_inventory must be accompanied with relationship')
+ if relationship is not None:
+ if (source_manifest is None and
+ relationship.__parent__ is not None):
+ tmp = getattr(
+ relationship.__parent__, '__parent__', None)
+ if interfaces.IManifest.providedBy(tmp):
+ source_manifest = tmp
+ if source_manifest is not None:
+ self._copy_source = (relationship, source_manifest)
+ if object is not None or containment is not None:
+ raise ValueError(
+ 'cannot specify relationship with object or containment')
+ object = relationship.object
+ containment = relationship.containment
+ if token is not None:
+ if not isinstance(token, int):
+ raise ValueError('token must be int')
+ self._token = token
+ self._object = object
+ self._containment = Mapping(containment)
+ self._containment.__parent__ = self
+
+ @property
+ def copy_source(self): # None or tuple of (relationship, inventory)
+ return self._copy_source
+
+ @versioning.method
+ def _z_version(self):
+ if self.token is None:
+ raise zc.copyversion.interfaces.VersioningError(
+ 'Cannot version without a token')
+ vault = self.__parent__.vault
+ if not self.containment._z_versioned:
+ prev = vault.getPrevious(self)
+ if prev is not None:
+ if prev.containment == self.containment:
+ assert prev.containment._z_versioned
+ self._containment = prev.containment
+ if not self._containment._z_versioned:
+ self.containment._z_version()
+ if self._object is not None:
+ obj_v = zc.copyversion.interfaces.IVersioning(self.object)
+ if not obj_v._z_versioned:
+ factory = interfaces.IVersionFactory(vault, None)
+ if factory is not None:
+ res = factory(self.object, self.__parent__)
+ if res is not self.object:
+ self.object = res
+ obj_v = zc.copyversion.interfaces.IVersioning(res)
+ if not obj_v._z_versioned:
+ obj_v._z_version()
+ super(Relationship, self)._z_version()
+
+ @property
+ def token(self):
+ return self._token
+ @rwproperty.setproperty
+ def token(self, value):
+ if self._token is None:
+ self._token = value
+ elif not isinstance(value, int):
+ raise ValueError('token must be int')
+ else:
+ self._token = value
+
+ @property
+ def object(self):
+ return self._object
+ @versioning.setproperty
+ def object(self, value):
+ if self.token is None and self.__parent__ is not None:
+ raise ValueError('cannot change object without token')
+ if self.token == self.__parent__.vault.top_token:
+ raise ValueError('cannot set object of top token')
+ if (value is not None and
+ not zc.copyversion.interfaces.IVersionable.providedBy(value)):
+ raise ValueError(
+ 'can only place versionable objects in vault, or None')
+ if self.__parent__ is not None:
+ self.__parent__.checkRelationshipChange(self)
+ if value is not self._object:
+ old = self._object
+ self._object = value
+ if (self.__parent__ is not None and
+ self.__parent__.getType(self) is not None):
+ self.__parent__.reindex(self)
+ event.notify(interfaces.ObjectChanged(self, old))
+
+ @property
+ def containment(self):
+ return self._containment
+
+ @property
+ def children(self):
+ return self.containment.valuesBTree
+
+def localDump(obj, index, cache):
+ # NOTE: a reference to this function is persisted!
+ return index.__parent__.vault.intids.register(obj)
+
+def localLoad(token, index, cache):
+ # NOTE: a reference to this function is persisted!
+ return index.__parent__.vault.intids.getObject(token)
+
+class Manifest(persistent.Persistent, versioning.Versioning,
+ zope.app.container.contained.Contained):
+
+ interface.implements(interfaces.IManifest)
+
+ _updateSource = _updateBase = None
+
+ def __init__(self, base=None, vault=None):
+ if vault is None:
+ if base is None:
+ raise ValueError('must provide base or vault')
+ vault = base.vault
+ elif base is not None:
+ if base.vault is not vault and base.getBaseSource(vault) is None:
+ raise ValueError(
+ "explicitly passed vault must have a base in base_source.")
+ else: # vault but not base
+ base = vault.manifest
+ if base is not None and not base._z_versioned:
+ raise ValueError('base must be versioned')
+ self.__parent__ = self._vault = vault
+ self._index = index.Index(
+ ({'element':interfaces.IRelationship['token'],
+ 'dump': None, 'load': None, 'btree': IOBTree},
+ interfaces.IRelationship['object'],
+ {'element':interfaces.IRelationship['children'],
+ 'multiple': True, 'dump': None, 'load': None,
+ 'name': 'child', 'btree': IOBTree}),
+ index.TransposingTransitiveQueriesFactory('token', 'child'),
+ localDump, localLoad)
+ self._index.__parent__ = self
+ self._selections = IFBTree.IFTreeSet()
+ self._oldSelections = IFBTree.IFTreeSet()
+ self._conflicts = IFBTree.IFTreeSet()
+ self._resolutions = IFBTree.IFTreeSet()
+ self._orphanResolutions = IFBTree.IFTreeSet()
+ self._oldOrphanResolutions = IFBTree.IFTreeSet()
+ self._updated = IFBTree.IFTreeSet()
+ self._local = IFBTree.IFTreeSet()
+ self._suggested = IFBTree.IFTreeSet()
+ self._modified = IFBTree.IFTreeSet()
+ self._bases = IOBTree.IOBTree()
+ if base:
+ self._indexBases(base.getBaseSources(), base, True)
+ if vault.held is None:
+ self._held = HeldContainer()
+ zope.location.locate(self._held, self, "held")
+ else:
+ self._held = vault.held
+
+ @property
+ def held(self):
+ return self._held
+
+ def _indexBases(self, bases, base=None, select=False):
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ bases = dict((intids.register(b.vault), b) for b in bases)
+ if base is not None:
+ bid = intids.register(base.vault)
+ bases[bid] = base
+ else:
+ bid = None
+ assert not self._bases, (
+ 'programmer error: _indexBases should not be called with '
+ 'populated _bases')
+ for iid, b in bases.items():
+ select_this = select and iid==bid
+ base_set = IFBTree.IFTreeSet()
+ data = (base_set, b)
+ register = self.vault.intids.register
+ for rel in b:
+ rid = register(rel)
+ base_set.insert(rid)
+ if select_this:
+ self._selections.insert(rid)
+ event.notify(interfaces.RelationshipSelected(rel, self))
+ self._index.index_doc(rid, rel)
+ self._bases[iid] = data
+
+ versioning.makeProperty('vault_index')
+
+ def getBaseSources(self):
+ return tuple(data[1] for data in self._bases.values())
+
+ def getBaseSource(self, vault):
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ iid = intids.queryId(vault)
+ if iid is not None:
+ data = self._bases.get(iid)
+ if data is not None:
+ return data[1]
+
+ @property
+ def base_source(self):
+ return self.getBaseSource(self.vault)
+
+ _vault = None
+ @property
+ def vault(self):
+ return self._vault
+ @versioning.setproperty
+ def vault(self, value):
+ if self.updating:
+ raise interfaces.UpdateError('Cannot change vault while updating')
+ if value is not self._vault:
+ old = self._vault
+ s = set(old.intids.getObject(t) for t in self._selections)
+ bases = tuple(self.getBaseSources())
+ self._selections.clear()
+ l = set(old.intids.getObject(t) for t in self._local)
+ self._local.clear()
+ self._index.clear()
+ self._bases.clear()
+ self._vault = value
+ self._indexBases(bases)
+ for r in l:
+ self._add(r, self._local, True)
+ self._selections.update(value.intids.register(r) for r in s)
+ event.notify(interfaces.VaultChanged(self, old))
+
+ @property
+ def merged_sources(self):
+ v = self.vault
+ return tuple(b for b in self.getBaseSources() if b.vault is not v)
+
+ @property
+ def update_source(self):
+ return self._updateSource
+
+ @property
+ def update_base(self):
+ return self._updateBase
+
+ @property
+ def updating(self):
+ return self._updateSource is not None
+
+ @versioning.method
+ def _z_version(self):
+ if self.updating:
+ raise zc.copyversion.interfaces.VersioningError(
+ 'cannot version during update')
+ if (list(self.iterParentConflicts()) or
+ list(self.iterOrphanConflicts())):
+ raise zc.copyversion.interfaces.VersioningError(
+ 'cannot version with conflicts')
+ selections = set(self._iterLinked())
+ b = base = self.base_source
+ for r in list(self._local):
+ if r not in selections:
+ self._local.remove(r)
+ self._index.unindex_doc(r)
+ else:
+ rel = self.vault.intids.getObject(r)
+ if base is not None:
+ b = base.get(rel.token)
+ if (b is None or
+ b.object is not rel.object or
+ b.containment != rel.containment):
+ if not rel._z_versioned:
+ rel._z_version()
+ else:
+ selections.remove(r)
+ self._local.remove(r)
+ selections.add(self.vault.intids.getId(b))
+ self._index.unindex_doc(r)
+ self._selections.clear()
+ self._selections.update(selections)
+ super(Manifest, self)._z_version()
+
+ def _locateObject(self, relationship, force=False):
+ if not force:
+ for child in relationship.children:
+ if self.get(child) is None:
+ raise ValueError(
+ 'child tokens must have selected relationships')
+ if relationship.token == self.vault.top_token:
+ assert relationship.object is None
+ return
+ obj = relationship.object
+ if obj is not None and getattr(obj, '__parent__', None) is None:
+ if zope.location.interfaces.ILocation.providedBy(obj):
+ dest = self.held
+ dest[INameChooser(dest).chooseName('', obj)] = obj
+ else:
+ obj.__parent__ = self.vault
+
+ def _add(self, relationship, set, force=False):
+ self._locateObject(relationship, force)
+ if relationship.__parent__ is not self:
+ if relationship.__parent__ is None:
+ relationship.__parent__ = self
+ else:
+ raise ValueError(
+ 'cannot add relationship already in another set')
+ iid = self.vault.intids.register(relationship)
+ set.insert(iid)
+ self._index.index_doc(iid, relationship)
+
+ @versioning.method
+ def addLocal(self, relationship):
+ if self.updating:
+ raise interfaces.UpdateError(
+ 'cannot add local relationships during update')
+ if self.getLocal(relationship.token) is not None:
+ raise ValueError(
+ 'cannot add a second local relationship for the same token')
+ self._add(relationship, self._local)
+ event.notify(interfaces.LocalRelationshipAdded(relationship))
+ if len(self._index.findRelationshipTokenSet(
+ self._index.tokenizeQuery({'token': relationship.token}))) == 1:
+ self.select(relationship)
+
+ @versioning.method
+ def addModified(self, relationship):
+ if not self.updating:
+ raise interfaces.UpdateError(
+ 'can only add modified relationships during update')
+ self._add(relationship, self._modified)
+ event.notify(interfaces.ModifiedRelationshipAdded(relationship))
+ if len(self._index.findRelationshipTokenSet(
+ self._index.tokenizeQuery({'token': relationship.token}))) == 1:
+ self.select(relationship)
+
+ @versioning.method
+ def addSuggested(self, relationship):
+ if not self.updating:
+ raise interfaces.UpdateError(
+ 'can only add suggested relationships during update')
+ if len(self._index.findRelationshipTokenSet(
+ {'token': relationship.token})) == 0:
+ raise ValueError('cannot add suggested relationship for new token')
+ self._add(relationship, self._suggested)
+ event.notify(interfaces.SuggestedRelationshipAdded(relationship))
+
+ @versioning.method
+ def beginUpdate(self, source=None, base=None):
+ if self.updating:
+ raise interfaces.UpdateError(
+ 'cannot begin another update while updating')
+ if source is None:
+ source = self.vault.manifest
+ if not interfaces.IManifest.providedBy(source):
+ raise ValueError('source must be manifest')
+ if source.vault.intids is not self.vault.intids:
+ raise ValueError('source must share intids')
+ if base is None:
+ if self.base_source is None or source.vault != self.vault:
+ myBase = self.getBaseSource(source.vault)
+ otherBase = source.getBaseSource(self.vault)
+ if myBase is None:
+ if otherBase is None:
+ # argh. Need to walk over all bases and find any
+ # shared ones. Then pick the most recent one.
+ for b in self.getBaseSources():
+ if b.vault == self.vault:
+ continue
+ o = source.getBaseSource(b.vault)
+ if o is not None:
+ # we found one!
+ if (o._z_version_timestamp >
+ b._z_version_timestamp):
+ b = o
+ if base is None or (
+ b._z_version_timestamp >
+ base._z_version_timestamp):
+ base = b
+ if base is None:
+ raise ValueError('no shared previous manifest')
+ else:
+ base = otherBase
+ elif (otherBase is None or
+ otherBase._z_version_timestamp <=
+ myBase._z_version_timestamp):
+ base = myBase
+ else:
+ base = otherBase
+ else:
+ base = self.base_source
+
+ if base is source:
+ raise ValueError('base is source')
+ elif base._z_version_timestamp > source._z_version_timestamp:
+ raise NotImplementedError(
+ "don't know how to merge to older source")
+ if not interfaces.IManifest.providedBy(base):
+ raise ValueError('base must be manifest')
+ if not source._z_versioned or not base._z_versioned:
+ raise ValueError('manifests must be versioned')
+ intids = self.vault.intids
+ self._oldSelections.update(self._selections)
+ self._oldOrphanResolutions.update(self._orphanResolutions)
+ self._updateSource = source
+ self._updateBase = base
+ to_be_resolved = []
+ for s in source:
+ b = base.get(s.token)
+ source_changed = (b is None or s.object is not b.object or
+ s.containment != b.containment)
+ l = self.get(s.token)
+ if l is None: # even if base is non-None, that change is elsewhere
+ local_changed = False
+ elif b is None:
+ local_changed = True
+ else:
+ local_changed = l is not b and (
+ l.object is not b.object or l.containment != b.containment)
+ if source_changed:
+ iid = intids.register(s)
+ self._updated.insert(iid)
+ self._index.index_doc(iid, s)
+ if local_changed:
+ self._conflicts.insert(s.token)
+ if l is not s and (l.object is not s.object or
+ l.containment != s.containment):
+ # material difference. Give resolver a chance.
+ to_be_resolved.append((l, s, b))
+ else:
+ # we'll use the merged version by default
+ self.select(s)
+ self._resolutions.insert(s.token)
+ else:
+ self.select(s)
+ if to_be_resolved:
+ resolver = interfaces.IConflictResolver(self.vault, None)
+ if resolver is not None:
+ for l, s, b in to_be_resolved:
+ resolver(self, l, s, b)
+ event.notify(interfaces.UpdateBegun(self, source, base))
+
+ @versioning.method
+ def beginCollectionUpdate(self, source):
+ if self.updating:
+ raise interfaces.UpdateError(
+ 'cannot begin another update while updating')
+ source = set(source)
+ token_to_source = dict((r.token, r) for r in source)
+ if len(token_to_source) < len(source):
+ raise ValueError(
+ 'cannot provide more than one update relationship for the '
+ 'same source')
+ for rel in source:
+ if rel.__parent__.vault.intids is not self.vault.intids:
+ raise ValueError('sources must share intids')
+ for child in rel.children:
+ if (token_to_source.get(child) is None and
+ self.get(child) is None):
+ raise ValueError(
+ 'cannot update from a set that includes children '
+ 'tokens without matching relationships in which the '
+ 'child is the token')
+ intids = self.vault.intids
+ self._oldSelections.update(self._selections)
+ self._oldOrphanResolutions.update(self._orphanResolutions)
+ tmp_source = set()
+ for rel in source:
+ if not rel._z_versioned:
+ if rel.__parent__ is not None and rel.__parent__ is not self:
+ rel = Relationship(rel.token, relationship=rel)
+ rel.__parent__ = self
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ self._add(rel, self._updated, force=True)
+ else:
+ iid = intids.register(rel)
+ self._updated.insert(iid)
+ self._locateObject(rel, force=True)
+ self._index.index_doc(iid, rel)
+ tmp_source.add(rel)
+ local = self.getLocal(rel.token)
+ if local is not None:
+ self._conflicts.insert(rel.token)
+ if (local.object is rel.object and
+ local.containment == rel.containment):
+ self._resolutions.insert(rel.token)
+ else:
+ resolver = component.queryMultiAdapter(
+ (local, rel, None), interfaces.IConflictResolver)
+ if resolver is not None:
+ resolver(self)
+ else:
+ self.select(rel)
+ self._updateSource = frozenset(tmp_source)
+ assert not self._getChildErrors()
+ event.notify(interfaces.UpdateBegun(self, source, None))
+
+ def _selectionsFilter(self, relchain, query, index, cache):
+ return relchain[-1] in self._selections
+
+ def _iterLinked(self):
+ for p in self._index.findRelationshipTokenChains(
+ {'token': self.vault.top_token}, filter=self._selectionsFilter):
+ yield p[-1]
+
+ def completeUpdate(self):
+ source = self._updateSource
+ if source is None:
+ raise interfaces.UpdateError('not updating')
+ if (list(self.iterUpdateConflicts()) or
+ list(self.iterParentConflicts()) or
+ list(self.iterOrphanConflicts())):
+ raise interfaces.UpdateError(
+ 'cannot complete update with conflicts')
+ assert not self._getChildErrors(), 'children without relationships!'
+ manifest = interfaces.IManifest.providedBy(source)
+ # assemble the contents of what will be the new bases
+ intids = self.vault.intids
+ selected = set(self._iterLinked())
+ base = self._updateBase
+ self._updateSource = self._updateBase = None
+ self._selections.clear()
+ self._selections.update(selected)
+ self._local.clear()
+ self._index.clear()
+ self._updated.clear()
+ self._modified.clear()
+ self._suggested.clear()
+ self._conflicts.clear()
+ self._resolutions.clear()
+ self._orphanResolutions.clear()
+ self._oldOrphanResolutions.clear()
+ self._oldSelections.clear()
+ bases = self.getBaseSources()
+ self._bases.clear()
+ if manifest:
+ global_intids = component.getUtility(
+ zope.app.intid.interfaces.IIntIds)
+ bases = dict((global_intids.register(b.vault), b) for b in bases)
+ for b in source.getBaseSources():
+ iid = global_intids.register(b.vault)
+ o = bases.get(iid)
+ if o is None or o._z_version_timestamp < b._z_version_timestamp:
+ bases[iid] = b
+ self._indexBases(bases.values(), source)
+ existing = IFBTree.multiunion(
+ [data[0] for data in self._bases.values()])
+ for i in selected:
+ orig = rel = intids.getObject(i)
+ if rel._z_versioned:
+ create_local = False
+ source_rel = source.get(rel.token)
+ if source_rel is rel:
+ create_local = True
+ elif source_rel is not None:
+ base_rel = base.get(rel.token)
+ if (base_rel is None or
+ source_rel._z_version_timestamp >
+ base_rel._z_version_timestamp):
+ create_local = True
+ if create_local:
+ rel = Relationship(
+ rel.token, relationship=rel, source_manifest=self)
+ rel.__parent__ = self
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ else:
+ continue
+ self._add(rel, self._local, True)
+ event.notify(interfaces.LocalRelationshipAdded(rel))
+ if orig is not rel:
+ self._selections.remove(i)
+ self.select(rel)
+ else:
+ self._indexBases(bases)
+ existing = IFBTree.multiunion(
+ [data[0] for data in self._bases.values()])
+ for i in selected:
+ if i not in existing:
+ rel = intids.getObject(i)
+ if rel._z_versioned:
+ rel = Relationship(
+ rel.token, relationship=rel, source_manifest=self)
+ rel.__parent__ = self
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ self._add(rel, self._local, True)
+ event.notify(interfaces.LocalRelationshipAdded(rel))
+ assert not (list(self.iterUpdateConflicts()) or
+ list(self.iterParentConflicts()) or
+ list(self.iterOrphanConflicts()) or
+ self._getChildErrors())
+ event.notify(interfaces.UpdateCompleted(self, source, base))
+
+ def checkRelationshipChange(self, relationship):
+ reltype = self.getType(relationship)
+ if self.updating and reltype == interfaces.LOCAL:
+ raise interfaces.UpdateError(
+ 'cannot change local relationships while updating')
+ if reltype in (interfaces.SUGGESTED, interfaces.UPDATED):
+ assert self.updating, (
+ 'internal state error: the system should not allow suggested '
+ 'or updated relationships when not updating')
+ raise TypeError(
+ 'cannot change relationships when used as suggested or '
+ 'updated values')
+
+ def abortUpdate(self):
+ if self._updateSource is None:
+ raise interfaces.UpdateError('not updating')
+ source = self._updateSource
+ base = self._updateBase
+ self._updateSource = self._updateBase = None
+ for s in (self._updated, self._modified, self._suggested):
+ for t in s:
+ self._index.unindex_doc(t)
+ s.clear()
+ self._conflicts.clear()
+ self._resolutions.clear()
+ self._orphanResolutions.clear()
+ self._orphanResolutions.update(self._oldOrphanResolutions)
+ self._oldOrphanResolutions.clear()
+ self._selections.clear()
+ self._selections.update(self._oldSelections)
+ self._oldSelections.clear()
+ event.notify(interfaces.UpdateAborted(self, source, base))
+
+ def iterChanges(self, base=None):
+ get = self.vault.intids.getObject
+ if base is None:
+ base = self.base_source
+ for t in self._selections:
+ rel = get(t)
+ if base is None:
+ yield rel
+ else:
+ b = base.get(rel.token)
+ if (b is None or
+ b.object is not rel.object or
+ b.containment != rel.containment):
+ yield rel
+
+ @versioning.method
+ def reindex(self, relationship):
+ t = self.vault.intids.queryId(relationship)
+ if t is not None and (t in self._local or t in self._suggested or
+ t in self._modified or t in self._updated):
+ self._locateObject(relationship)
+ self._index.index_doc(t, relationship)
+
+ def _getFromSet(self, token, set, default):
+ res = list(self._yieldFromSet(token, set))
+ if not res:
+ return default
+ assert len(res) == 1, 'internal error: too many in the same category'
+ return res[0]
+
+ def _yieldFromSet(self, token, set):
+ get = self.vault.intids.getObject
+ for t in self._index.findRelationshipTokenSet({'token': token}):
+ if t in set:
+ yield get(t)
+
+ def get(self, token, default=None):
+ # return the selected relationship
+ return self._getFromSet(token, self._selections, default)
+
+ def getType(self, relationship):
+ t = self.vault.intids.queryId(relationship)
+ if t is not None:
+ if t in self._local:
+ return interfaces.LOCAL
+ elif t in self._updated:
+ return interfaces.UPDATED
+ elif t in self._suggested:
+ return interfaces.SUGGESTED
+ elif t in self._modified:
+ return interfaces.MODIFIED
+ else:
+ intids = component.getUtility(
+ zope.app.intid.interfaces.IIntIds)
+ iid = intids.queryId(relationship.__parent__.vault)
+ if iid is not None and iid in self._bases:
+ iiset, rel_set = self._bases[iid]
+ if t in iiset:
+ return interfaces.BASE
+ for bid, (iiset, rel_set) in self._bases.items():
+ if bid == iid:
+ continue
+ if t in iiset:
+ return interfaces.MERGED
+ return None
+
+ def isSelected(self, relationship):
+ t = self.vault.intids.queryId(relationship)
+ return t is not None and t in self._selections
+
+ @versioning.method
+ def select(self, relationship):
+ t = self.vault.intids.queryId(relationship)
+ if t is None or self.getType(relationship) is None:
+ raise ValueError('unknown relationship')
+ if t in self._selections:
+ return
+ rel_tokens = self._index.findRelationshipTokenSet(
+ self._index.tokenizeQuery({'token': relationship.token}))
+ for rel_t in rel_tokens:
+ if rel_t in self._selections:
+ self._selections.remove(rel_t)
+ event.notify(interfaces.RelationshipDeselected(
+ self.vault.intids.getObject(rel_t), self))
+ break
+ self._selections.insert(t)
+ event.notify(interfaces.RelationshipSelected(relationship, self))
+
+ def getBase(self, token, default=None):
+ vault = self.base_source
+ for iiset, rel_set in self._bases.values():
+ if rel_set is vault:
+ return self._getFromSet(token, iiset, default)
+
+ def getLocal(self, token, default=None):
+ return self._getFromSet(token, self._local, default)
+
+ def getUpdated(self, token, default=None):
+ return self._getFromSet(token, self._updated, default)
+
+ def iterSuggested(self, token):
+ return self._yieldFromSet(token, self._suggested)
+
+ def iterModified(self, token):
+ return self._yieldFromSet(token, self._modified)
+
+ def iterMerged(self, token):
+ vault = self.vault
+ seen = set()
+ for iiset, rel_set in self._bases.values():
+ if rel_set is not vault:
+ for r in self._yieldFromSet(token, iiset):
+ if r not in seen:
+ yield r
+ seen.add(r)
+
+ def _parents(self, token):
+ return self._index.findRelationshipTokenSet({'child': token})
+
+ def iterSelectedParents(self, token):
+ get = self.vault.intids.getObject
+ for iid in self._parents(token):
+ if iid in self._selections:
+ yield get(iid)
+
+ def iterParents(self, token):
+ get = self.vault.intids.getObject
+ return (get(iid) for iid in self._parents(token))
+
+ def getParent(self, token):
+ good = set()
+ orphaned = set()
+ unselected = set()
+ orphaned_unselected = set()
+ for iid in self._parents(token):
+ is_orphaned = self.isOrphan(iid)
+ if iid in self._selections:
+ if is_orphaned:
+ orphaned.add(iid)
+ else:
+ good.add(iid)
+ else:
+ if is_orphaned:
+ orphaned_unselected.add(iid)
+ else:
+ unselected.add(iid)
+ for s in (good, orphaned, unselected, orphaned_unselected):
+ if s:
+ if len(s) > 1:
+ raise interfaces.ParentConflictError
+ return self.vault.intids.getObject(s.pop())
+
+ def isLinked(self, token, child):
+ return self._index.isLinked(
+ self._index.tokenizeQuery({'token': token}),
+ filter=self._selectionsFilter,
+ targetQuery=self._index.tokenizeQuery({'child': child}))
+
+ def iterUpdateConflicts(self):
+ # any proposed (not accepted) relationship that has both update and
+ # local for its token
+ if self._updateSource is None:
+ raise StopIteration
+ get = self.vault.intids.getObject
+ for t in self._conflicts:
+ if t not in self._resolutions:
+ rs = list(self._index.findRelationshipTokenSet({'token': t}))
+ for r in rs:
+ if r in self._selections:
+ yield get(r)
+ break
+ else:
+ assert 0, (
+ 'programmer error: no selected relationship found for '
+ 'conflicted token')
+
+ def iterUpdateResolutions(self):
+ if self._updateSource is None:
+ raise StopIteration
+ get = self.vault.intids.getObject
+ for t in self._resolutions:
+ assert t in self._conflicts
+ rs = list(self._index.findRelationshipTokenSet({'token': t}))
+ for r in rs:
+ if r in self._selections:
+ yield get(r)
+ break
+ else:
+ assert 0, (
+ 'programmer error: no selected relationship found for '
+ 'resolved token')
+
+ def isUpdateConflict(self, token):
+ return (token in self._conflicts and
+ token not in self._resolutions)
+
+ @versioning.method
+ def resolveUpdateConflict(self, token):
+ if not self.updating:
+ raise interfaces.UpdateError(
+ 'can only resolve merge conflicts during update')
+ if token not in self._conflicts:
+ raise ValueError('token does not have merge conflict')
+ self._resolutions.insert(token)
+
+ def _iterOrphans(self, condition):
+ get = self.vault.intids.getObject
+ res = set(self._selections)
+ res.difference_update(self._iterLinked())
+ bases = IFBTree.multiunion([d[0] for d in self._bases.values()])
+ res.difference_update(bases)
+ for t in res:
+ tids = self._index.findValueTokenSet(t, 'token')
+ assert len(tids) == 1
+ tid = iter(tids).next()
+ if not condition(tid):
+ continue
+ yield get(t)
+
+ def iterOrphanConflicts(self):
+ return self._iterOrphans(lambda t: t not in self._orphanResolutions)
+
+ def iterOrphanResolutions(self):
+ return self._iterOrphans(lambda t: t in self._orphanResolutions)
+
+ def isOrphan(self, token):
+ return not (token == self.vault.top_token or
+ self.isLinked(self.vault.top_token, token))
+
+ def isOrphanConflict(self, token):
+ return (self.isOrphan(token) and
+ self.getType(token) not in (interfaces.BASE, interfaces.MERGED)
+ and token not in self._orphanResolutions)
+
+ @versioning.method
+ def resolveOrphanConflict(self, token):
+ self._orphanResolutions.insert(token)
+
+ @versioning.method
+ def undoOrphanConflictResolution(self, token):
+ self._orphanResolutions.remove(token)
+
+ def iterParentConflicts(self):
+ get = self.vault.intids.getObject
+ seen = set()
+ for r in self._iterLinked():
+ if r in seen:
+ continue
+ seen.add(r)
+ ts = self._index.findValueTokenSet(r, 'token')
+ assert len(ts) == 1
+ t = iter(ts).next()
+ paths = list(self._index.findRelationshipTokenChains(
+ {'child': t}, filter=self._selectionsFilter,
+ targetQuery={'token': self.vault.top_token}))
+ if len(paths) > 1:
+ yield get(r)
+
+ def _getChildErrors(self):
+ parents = set()
+ children = set()
+ for rid in self._iterLinked():
+ parents.update(self._index.findValueTokenSet(rid, 'token'))
+ children.update(self._index.findValueTokenSet(rid, 'child'))
+ children.difference_update(parents)
+ return children # these are token ids
+
+ def iterAll(self): # XXX __iter__?
+ get = self.vault.intids.getObject
+ seen = set()
+ for s in (self._local, self._updated, self._suggested,
+ self._modified):
+ for t in s:
+ if t not in seen:
+ yield get(t)
+ seen.add(t)
+ for iidset, rel_set in self._bases.values():
+ for t in iidset:
+ if t not in seen:
+ yield get(t)
+ seen.add(t)
+
+ def iterSelections(self): # XXX __iter__?
+ get = self.vault.intids.getObject
+ return (get(t) for t in self._selections)
+
+
+ def __iter__(self): # XXX iterLinked?
+ get = self.vault.intids.getObject
+ return (get(t) for t in self._iterLinked())
+
+ def iterUnchangedOrphans(self):
+ get = self.vault.intids.getObject
+ res = set(self._selections)
+ res.difference_update(self._iterLinked())
+ bases = IFBTree.multiunion([d[0] for d in self._bases.values()])
+ res.intersection_update(bases)
+ return (get(t) for t in res)
+
+ @property
+ def previous(self):
+ i = self.vault_index
+ if i is not None and len(self.vault)-1 >= i and self.vault[i] is self:
+ if i > 0:
+ return self.vault[i-1]
+ return None
+ return self.base_source
+
+ @property
+ def next(self):
+ i = self.vault_index
+ if i is not None and len(self.vault) > i+1 and self.vault[i] is self:
+ return self.vault[i+1]
+ return None
+
+ def isOption(self, relationship): # XXX __contains__?
+ for rel in self._index.findRelationships(
+ self._index.tokenizeQuery(
+ {'token': relationship.token, 'object': relationship.object})):
+ if rel is relationship:
+ return True
+ return False
+
+class HeldContainer(zope.app.container.btree.BTreeContainer):
+ pass
+
+class Vault(persistent.Persistent, zope.app.container.contained.Contained):
+ interface.implements(interfaces.IVault)
+ def __init__(self, intids=None, held=None):
+ self._data = IOBTree.IOBTree()
+ if intids is None:
+ intids = zope.app.intid.IntIds()
+ self.intids = intids
+ if intids.__parent__ is None:
+ zope.location.locate(intids, self, 'intids')
+ self._next = OOBTree.OOBTree()
+ self.top_token = intids.register(keyref.top_token)
+ self._held = held
+
+ @property
+ def held(self):
+ return self._held
+
+ def createBranch(self, ix=-1, held=None):
+ # ...shugah, shugah...
+ # uh, that means that this is sugar for the "make sure you share the
+ # intids when you make a branch" issue.
+ if not isinstance(ix, int):
+ raise ValueError('ix must be int')
+ if held is None:
+ held = self.held
+ res = self.__class__(self.intids, held)
+ res.commit(Manifest(self[ix]))
+ return res
+
+ def getPrevious(self, relationship):
+ manifest = relationship.__parent__ # first one with this relationship
+ ix = manifest.vault_index
+ if ix > 0:
+ return manifest.vault[ix-1].get(relationship.token)
+ return None
+
+ def getNext(self, relationship):
+ return self._next.get(self.intids.getId(relationship))
+
+ def __len__(self):
+ if self._data:
+ return self._data.maxKey() + 1
+ else:
+ return 0
+
+ def __getitem__(self, key):
+ if isinstance(key, slice):
+ start, stop, stride = key.indices(len(self))
+ if stride==1:
+ return self._data.values(start, stop, excludemax=True)
+ else:
+ pos = start
+ res = []
+ while pos < stop:
+ res.append(self._data[pos])
+ pos += stride
+ return res
+ elif key < 0:
+ effective_key = len(self) + key
+ if effective_key < 0:
+ raise IndexError(key)
+ return self._data[effective_key]
+ elif key >= len(self):
+ raise IndexError(key)
+ else:
+ return self._data[key]
+
+ def __iter__(self):
+ return iter(self._data.values())
+
+ @property
+ def manifest(self):
+ if self._data:
+ return self._data[self._data.maxKey()]
+ return None
+
+ def commit(self, manifest):
+ if not interfaces.IManifest.providedBy(manifest):
+ raise ValueError('may only commit an IManifest')
+ current = self.manifest
+ if current is not None:
+ current_vaults = set(id(b.vault) for b in current.getBaseSources())
+ current_vaults.add(id(current.vault))
+ new_vaults = set(id(b.vault) for b in manifest.getBaseSources())
+ new_vaults.add(id(manifest.vault))
+ if not current_vaults & new_vaults:
+ raise ValueError(
+ 'may only commit a manifest with at least one shared base')
+ elif manifest.getBaseSource(self) is not current and (
+ not manifest.updating or (
+ manifest.updating and
+ manifest.update_source is not current)):
+ raise interfaces.OutOfDateError(manifest)
+ self._commit(manifest)
+
+ def _commit(self, manifest):
+ if manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError(manifest)
+ if manifest.get(self.top_token) is None:
+ raise ValueError(
+ 'cannot commit a manifest without a top_token relationship')
+ if (self.manifest is not None and
+ not len(tuple(manifest.iterChanges(self.manifest)))):
+ raise interfaces.NoChangesError(manifest)
+ if manifest.updating:
+ manifest.completeUpdate()
+ elif (list(manifest.iterUpdateConflicts()) or
+ list(manifest.iterOrphanConflicts()) or
+ list(manifest.iterParentConflicts())):
+ raise interfaces.ConflictError(manifest)
+ manifest.vault = self
+ ix = len(self)
+ self._data[ix] = manifest
+ manifest.vault_index = ix
+ manifest._z_version()
+ for r in manifest:
+ if manifest.getLocal(r.token) is r:
+ p = self.getPrevious(r)
+ if p is not None and p.__parent__.vault is self:
+ pid = self.intids.getId(p)
+ self._next[pid] = r
+ if (manifest.__parent__ is None or
+ manifest.__name__ is None):
+ # so absoluteURL on objects in held "works" (needs traversers to
+ # really work)
+ zope.location.locate(manifest, self, unicode(manifest.vault_index))
+ event.notify(interfaces.ManifestCommitted(manifest))
+
+ def commitFrom(self, source):
+ if not interfaces.IManifest.providedBy(source):
+ raise ValueError('source must be manifest')
+ if source.vault.intids is not self.intids:
+ raise ValueError('source must share intids')
+ if not source._z_versioned:
+ raise ValueError('source must already be versioned')
+ res = Manifest(self.manifest)
+ base_rels = dict((r.token, r) for r in res.base_source)
+ for rel in source:
+ base_rel = base_rels.pop(rel.token, None)
+ if base_rel is not None and base_rel == rel:
+ res.select(rel)
+ else:
+ rel = Relationship(
+ rel.token, relationship=rel, source_manifest=res)
+ rel.__parent__ = res
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ res._add(rel, res._local, True)
+ res.select(rel)
+ event.notify(interfaces.LocalRelationshipAdded(rel))
+ for rel in base_rels.values():
+ res.select(rel) # to make sure that any hidden local changes are
+ # ignored. We don't need to resolve the orphans because base
+ # orphans are not regarded as conflicts
+ self._commit(res)
Added: zc.vault/trunk/src/zc/vault/i18n.py
===================================================================
--- zc.vault/trunk/src/zc/vault/i18n.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/i18n.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,33 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL). A copy of the ZVSL should accompany this
+# distribution.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""I18N support.
+
+This defines a `MessageFactory` for the I18N domain for the zc.vault
+package. This is normally used with this import::
+
+ from i18n import MessageFactory as _
+
+The factory is then used normally. Two examples::
+
+ text = _('some internationalized text')
+ text = _('helpful-descriptive-message-id', 'default text')
+"""
+__docformat__ = "reStructuredText"
+
+
+from zope import i18nmessageid
+
+MessageFactory = _ = i18nmessageid.MessageFactory("zc.vault")
+
Added: zc.vault/trunk/src/zc/vault/interfaces.py
===================================================================
--- zc.vault/trunk/src/zc/vault/interfaces.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/interfaces.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,770 @@
+from zope import interface
+import zope.interface.common.mapping
+import zope.interface.common.sequence
+import zope.app.container.interfaces
+import zope.component.interfaces
+
+#### CONSTANTS ####
+
+# see IManifest.getType for definitions
+LOCAL = 'local'
+BASE = 'base'
+UPDATED = 'updated'
+SUGGESTED = 'suggested'
+MODIFIED = 'modified'
+MERGED = 'merged'
+
+#### EXCEPTIONS ####
+
+class OutOfDateError(ValueError):
+ """Manifest to be committed is not based on the currently committed version
+ """
+
+class NoChangesError(ValueError):
+ """Manifest to be committed has no changes from the currently committed
+ version"""
+
+class ConflictError(ValueError):
+ """Manifest to be committed has unresolved conflicts"""
+
+#### EVENTS ####
+
+class IMappingEvent(zope.component.interfaces.IObjectEvent): # abstract
+
+ mapping = interface.Attribute(
+ 'the affected mapping; __parent__ is relationship')
+
+ key = interface.Attribute('the key affected')
+
+class IObjectRemoved(IMappingEvent):
+ """Object was removed from mapping"""
+
+class IObjectAdded(IMappingEvent):
+ """Object was added to mapping"""
+
+class IOrderChanged(zope.component.interfaces.IObjectEvent):
+ """Object order changed; object is mapping"""
+
+ old_keys = interface.Attribute('A tuple of the old key order')
+
+class IManifestCommitted(zope.component.interfaces.IObjectEvent):
+ """Object is manifest."""
+
+class ILocalRelationshipAdded(zope.component.interfaces.IObjectEvent):
+ """Relationship added to manifest as a local version.
+ Relationship.__parent__ must be manifest."""
+
+class IModifiedRelationshipAdded(zope.component.interfaces.IObjectEvent):
+ """Relationship added to manifest as a modified version.
+ Relationship.__parent__ must be manifest."""
+
+class ISuggestedRelationshipAdded(zope.component.interfaces.IObjectEvent):
+ """Relationship added to manifest as a suggested version.
+ Relationship.__parent__ must be manifest."""
+
+class IUpdateEvent(zope.component.interfaces.IObjectEvent):
+
+ source = interface.Attribute(
+ '''source manifest (from beginUpdate) or collection
+ (from beginCollectionUpdate)''')
+
+ base = interface.Attribute(
+ '''the base manifest from which the update proceeds, or None.''')
+
+class IUpdateBegun(IUpdateEvent):
+ '''fired from beginUpdate or beginCollectionUpdate'''
+
+class IUpdateCompleted(IUpdateEvent):
+ ''
+
+class IUpdateAborted(IUpdateEvent):
+ ''
+
+class IObjectChanged(zope.component.interfaces.IObjectEvent):
+ previous = interface.Attribute('the previous object')
+
+class IVaultChanged(zope.component.interfaces.IObjectEvent):
+ previous = interface.Attribute('the previous vault')
+
+class IRelationshipSelected(zope.component.interfaces.IObjectEvent):
+ """relationship was selected"""
+ manifest = interface.Attribute(
+ 'the manifest in which this relationship was selected')
+
+class IRelationshipDeselected(zope.component.interfaces.IObjectEvent):
+ """relationship was deselected"""
+ manifest = interface.Attribute(
+ 'the manifest in which this relationship was deselected')
+
+class ObjectRemoved(zope.component.interfaces.ObjectEvent):
+ interface.implements(IObjectRemoved)
+ def __init__(self, obj, mapping, key):
+ super(ObjectRemoved, self).__init__(obj)
+ self.mapping = mapping
+ self.key = key
+
+class ObjectAdded(zope.component.interfaces.ObjectEvent):
+ interface.implements(IObjectAdded)
+ def __init__(self, obj, mapping, key):
+ super(ObjectAdded, self).__init__(obj)
+ self.mapping = mapping
+ self.key = key
+
+class OrderChanged(zope.component.interfaces.ObjectEvent):
+ interface.implements(IOrderChanged)
+ def __init__(self, obj, old_keys):
+ super(OrderChanged, self).__init__(obj)
+ self.old_keys = old_keys
+
+class ManifestCommitted(zope.component.interfaces.ObjectEvent):
+ interface.implements(IManifestCommitted)
+
+class LocalRelationshipAdded(zope.component.interfaces.ObjectEvent):
+ interface.implements(ILocalRelationshipAdded)
+
+class ModifiedRelationshipAdded(zope.component.interfaces.ObjectEvent):
+ interface.implements(IModifiedRelationshipAdded)
+
+class SuggestedRelationshipAdded(zope.component.interfaces.ObjectEvent):
+ interface.implements(ISuggestedRelationshipAdded)
+
+class AbstractUpdateEvent(zope.component.interfaces.ObjectEvent):
+ def __init__(self, obj, source, base):
+ super(AbstractUpdateEvent, self).__init__(obj)
+ self.source = source
+ self.base = base
+
+class UpdateBegun(AbstractUpdateEvent):
+ interface.implements(IUpdateBegun)
+
+class UpdateCompleted(AbstractUpdateEvent):
+ interface.implements(IUpdateCompleted)
+
+class UpdateAborted(AbstractUpdateEvent):
+ interface.implements(IUpdateAborted)
+
+class VaultChanged(zope.component.interfaces.ObjectEvent):
+ interface.implements(IVaultChanged)
+ def __init__(self, obj, previous):
+ super(VaultChanged, self).__init__(obj)
+ self.previous = previous
+
+class ObjectChanged(zope.component.interfaces.ObjectEvent):
+ interface.implements(IObjectChanged)
+ def __init__(self, obj, previous):
+ super(ObjectChanged, self).__init__(obj)
+ self.previous = previous
+
+class RelationshipSelected(zope.component.interfaces.ObjectEvent):
+ interface.implements(IRelationshipSelected)
+ def __init__(self, obj, manifest):
+ super(RelationshipSelected, self).__init__(obj)
+ self.manifest = manifest
+
+class RelationshipDeselected(zope.component.interfaces.ObjectEvent):
+ interface.implements(IRelationshipDeselected)
+ def __init__(self, obj, manifest):
+ super(RelationshipDeselected, self).__init__(obj)
+ self.manifest = manifest
+
+#### EXCEPTIONS ####
+
+class ParentConflictError(StandardError):
+ """the item has more than one selected parent"""
+
+class UpdateError(StandardError):
+ """Update-related operation cannot proceed"""
+
+#### BASIC INTERFACES ####
+
+class IVersionFactory(interface.Interface):
+ """looked up as a adapter of vault"""
+ def __call__(object, manifest):
+ """return the object to be stored"""
+
+class IConflictResolver(interface.Interface):
+ """looked up as a adapter of vault."""
+ def __call__(manifest, local, update, base):
+ """React to conflict between local and update as desired."""
+
+class IUniqueReference(interface.Interface):
+
+ identifiers = interface.Attribute(
+ """An iterable of identifiers for this object.
+ From most general to most specific. Combination uniquely identifies
+ the object.""")
+
+ def __hash__():
+ """return a hash of the full set of identifiers."""
+
+ def __cmp__(other):
+ """Compare against other objects that provide IUniqueReference,
+ using the identifiers.""" # note that btrees do not support rich comps
+
+class IToken(IUniqueReference):
+ """An object used as a token for a manifest relationship"""
+
+class IBidirectionalNameMapping(zope.interface.common.mapping.IMapping):
+ """all keys are unicode, all values are adaptable to IKeyReference.
+
+ all values must be unique (no two keys may have the same value).
+
+ items, values, keys, and __iter__ returns in the specified order."""
+
+ def getKey(value, default=None):
+ """return key for value, or None"""
+
+ def updateOrder(order):
+ """Revise the order of keys, replacing the current ordering.
+
+ order is an iterable containing the set of existing keys in the new
+ order. `order` must contain ``len(keys())`` items and cannot contain
+ duplicate keys.
+
+ Raises ``TypeError`` if order is not iterable or any key is not
+ hashable.
+
+ Raises ``ValueError`` if order contains an invalid set of keys.
+ """
+
+class IRelationshipContainment(IBidirectionalNameMapping):
+ """If __parent__.__parent__ (manifest) exists, must call
+ manifest.approveRelationshipChange before making any changes, and should
+ call manifest.reindex after all changes except order changes.
+ """
+
+ __parent__ = interface.Attribute(
+ """The IRelationship of this containment before versioning (may be
+ reused for other relationships after versioning).""")
+
+class IRelationship(IUniqueReference):
+ """The relationship for mapping a token to its contents and its object.
+ Not mutable if can_modify is False."""
+
+ token = interface.Attribute(
+ """The token that this relationship maps""")
+
+ __parent__ = interface.Attribute(
+ """The manifest of this relationship before versioning (may be
+ reused for other manifests after being versioned)""")
+
+ object = interface.Attribute(
+ """the object that the token represents for this relationship.
+ if __parent__ exists (manifest), should call
+ manifest.approveRelationshipChange before making any changes, and
+ call manifest.reindex after change.""")
+
+ containment = interface.Attribute(
+ """The IRelationshipContainment, mapping names to contained tokens,
+ for this relationship.""")
+
+ children = interface.Attribute(
+ """readonly: the containment.values(). modify with the containment."""
+ )
+
+class IContained(IBidirectionalNameMapping):
+ """Abstract interface."""
+
+ previous = interface.Attribute(
+ """The IContained in the vault's previous inventory, or None if
+ it has no previous version. May be equal to (have the same
+ relationship as) this IContained.""")
+
+ next = interface.Attribute(
+ """The IContained in the vault's next inventory, or None if
+ it has no next version. May be equal to (have the same
+ relationship as) this IContained.""")
+
+ previous_version = interface.Attribute(
+ """The previous version of the IContained in the vault, or None if
+ it has no previous version. Will never be equal to (have the same
+ relationship as) this IContained.""")
+
+ next_version = interface.Attribute(
+ """The next version of the IContained in the vault, or None if
+ it has no next version. Will never be equal to (have the same
+ relationship as) this IContained.""")
+
+ __parent__ = interface.Attribute(
+ """the inventory to which this IContained belongs; same as inventory.
+ """)
+
+ inventory = interface.Attribute(
+ """the inventory to which this IContained belongs; same as __parent__.
+ """)
+
+ relationship = interface.Attribute(
+ """The relationship that models the containment and object information
+ for the token.""")
+
+ token = interface.Attribute(
+ """The token assigned to this IContained's relationship.
+
+ Synonym for .relationship.token""")
+
+ def __call__(name):
+ """return an IContained for the name, or raise Key Error"""
+
+ def makeMutable():
+ """convert this item to a mutable version if possible. XXX"""
+
+ type = interface.Attribute(
+ '''readonly; one of LOCAL, BASE, MERGED, UPDATED, SUGGESTED, MODIFIED.
+ see IManifest.getType (below) for definitions.''')
+
+ selected = interface.Attribute(
+ '''readonly boolean; whether this item is selected.
+ Only one item (relationship) may be selected at a time for a given
+ token in a given inventory''')
+
+ selected_item = interface.Attribute(
+ '''the selected version of this item''')
+
+ def select():
+ '''select this item, deselecting any other items for the same token'''
+
+ is_update_conflict = interface.Attribute(
+ '''whether this is an unresolved update conflict''')
+
+ def resolveUpdateConflict():
+ '''select this item and mark the update conflict as resolved.'''
+
+ has_base = interface.Attribute("whether item has a base version")
+
+ has_local = interface.Attribute("whether item has a local version")
+
+ has_updated = interface.Attribute("whether item has an updated version")
+
+ has_suggested = interface.Attribute(
+ "whether item has any suggested versions")
+
+ has_modified = interface.Attribute(
+ "whether item has any modified versions")
+
+ has_merged = interface.Attribute(
+ "whether item has any merged versions")
+
+ base_item = interface.Attribute('''the base item, or None''')
+
+ local_item = interface.Attribute('''the local item, or None''')
+
+ updated_item = interface.Attribute('''the updated item, or None''')
+
+ def iterSuggestedItems():
+ """iterate over suggested items"""
+
+ def iterModifiedItems():
+ """iterate over modified items"""
+
+ def iterMergedItems():
+ """iterate over merged items"""
+
+ def updateOrderFromTokens(order):
+ """Revise the order of keys, replacing the current ordering.
+
+ order is an iterable containing the set of tokens in the new order.
+ `order` must contain ``len(keys())`` items and cannot contain duplicate
+ values.
+
+ XXX what exceptions does this raise?
+ """
+
+class IInventoryContents(IContained):
+ """The top node of an inventory's hierarchy"""
+
+class IInventoryItem(IContained):
+
+ is_orphan = interface.Attribute(
+ '''whether this item cannot be reached from the top of the inventory's
+ hierarchy via selected relationships/items''')
+
+ is_orphan_conflict = interface.Attribute(
+ '''whether this is an orphan (see is_orphan) that is not BASE or
+ MERGED and not resolved.''')
+
+ def resolveOrphanConflict():
+ '''resolve the orphan conflict so that it no longer stops committing
+ or completing an update'''
+
+ is_parent_conflict = interface.Attribute(
+ '''whether this object has more than one selected parent''')
+
+ parent = interface.Attribute(
+ """The effective parent of the IContained.
+ Always another IContained, or None (for an IInventoryContents).
+ Will raise ParentConflictError if multiple selected parents.""")
+
+ name = interface.Attribute(
+ """The effective name of the IContained.
+ Always another IContained, or None (for an IInventoryContents).
+ Will raise ParentConflictError if multiple selected parents.""")
+
+ def iterSelectedParents():
+ '''iterate over all selected parents'''
+
+ def iterParents():
+ '''iterate over all parents'''
+
+ object = interface.Attribute(
+ """the object to which this IContained's token maps. The
+ vault_contents for the vault's top_token""")
+
+ def copyTo(location, name=None):
+ """make a clone of this node and below in location. Does not copy
+ actual object(s): just puts the same object(s) in an additional
+ location.
+
+ Location must be an IContained. Copying to another inventory is
+ currently undefined.
+ """
+
+ def moveTo(location=None, name=None):
+ """move this object's tree to location.
+
+ Location must be an IMutableContained from the same vault_contents.
+ Not specifying location indicates the current location (merely a
+ rename)."""
+
+ copy_source = interface.Attribute(
+ '''the item representing the relationship and inventory from which this
+ item's relationship was created.''')
+
+class IInventory(interface.Interface):
+ """IMPORTANT: the top token in an IInventory (and IManifest) is always
+ zc.vault.keyref.top_token."""
+
+ manifest = interface.Attribute(
+ """The IManifest used by this inventory""")
+
+ def iterUpdateConflicts():
+ '''iterate over the unresolved items that have update conflicts'''
+
+ def iterUpdateResolutions():
+ '''iterate over the resolved items that have update conflicts'''
+
+ def iterOrphanConflicts():
+ '''iterate over the current unresolved orphan conflicts'''
+
+ def iterOrphanResolutions():
+ '''iterate over the current resolved orphan conflicts'''
+
+ def iterUnchangedOrphans():
+ '''iterate over the orphans that do not cause conflicts--the ones that
+ were not changed, so are either in the base or a merged inventory.'''
+
+ def iterParentConflicts():
+ '''iterate over the items that have multiple parents.
+ The only way to resolve these is by deleting the item in one of the
+ parents.'''
+
+ def __iter__():
+ '''iterates over all selected items, whether or not they are orphans.
+ '''
+
+ updating = interface.Attribute(
+ '''readonly boolean: whether inventory is updating''')
+
+ merged_sources = interface.Attribute(
+ '''a tuple of the merged sources for this item.''')
+
+ update_source = interface.Attribute(
+ '''the source currently used for an update, or None if not updating''')
+
+ def beginUpdate(inventory=None, previous=None):
+ """begin update. Fails if already updating. if inventory is None,
+ uses the current vault's most recent checkin. if previous is None,
+ calculates most recent shared base.
+ """
+
+ def completeUpdate():
+ """complete update, moving update to merged. Fails if any update,
+ orphan, or parent conflicts."""
+
+ def abortUpdate():
+ """returns to state before update, discarding all changes made during
+ the update."""
+
+ def beginCollectionUpdate(items):
+ """start an update based on just a collection of items"""
+
+ def iterChangedItems(source=None):
+ '''iterate over items that have changed from source.
+ if source is None, use previous.'''
+
+ def getItemFromToken(token, default=None):
+ """get an item for the token in this inventory, or return default."""
+
+ previous = interface.Attribute('the previous inventory in the vault')
+
+ next = interface.Attribute('the next inventory in the vault')
+
+class IVault(zope.app.container.interfaces.IContained,
+ zope.interface.common.sequence.IFiniteSequence):
+ """A read sequence of historical manifests. The oldest is 0, and the
+ most recent is -1."""
+
+ manifest = interface.Attribute(
+ """The most recent committed manifest (self[-1])""")
+
+ def getPrevious(relationship):
+ '''return the previous version of the relationship (based on token)
+ or None'''
+
+ def getNext(relationship):
+ '''return the next version of the relationship (based on token)
+ or None'''
+
+ def commit(manifest):
+ """XXX"""
+
+ def commitFrom(manifest):
+ """XXX"""
+
+class IInventoryVault(IVault):
+ """commit and commitFrom also take inventories"""
+
+ inventory = interface.Attribute(
+ """The most recent committed inventory (self.getInventory(-1))""")
+
+ def getInventory(self, ix):
+ """return IInventory for relationship set at index ix"""
+
+class IManifest(interface.Interface):
+ """IMPORTANT: the top token in an IManifest (and IInventory) is always
+ zc.vault.keyref.top_token."""
+ # should manifests know all of the manifests
+ # they have generated (working copies)? Maybe worth keeping track of?
+
+ vault_index = interface.Attribute(
+ 'the index of the manifest in its vault')
+
+ held = interface.Attribute(
+ """Container of any related objects held because they were nowhere
+ else.
+ """)
+
+ def getBaseSources():
+ '''iterate over all bases (per vault)'''
+
+ def getBaseSource(vault):
+ '''get the base for the vault'''
+
+ base_source = interface.Attribute('''the base of the set''')
+
+ vault = interface.Attribute('the vault for this relationship set')
+
+ vault_index = interface.Attribute('the index of this set in the vault')
+
+ merged_sources = interface.Attribute(
+ '''a tuple of the non-base merged sources, as found in getBase.''')
+
+ update_source = interface.Attribute(
+ '''the manifest used for the upate''')
+
+ update_base = interface.Attribute(
+ '''the manifest for the shared ancestor that the two manifests share'''
+ )
+
+ updating = interface.Attribute(
+ '''boolean. whether in middle of update.''')
+
+ base_source = interface.Attribute(
+ """the manifest used as a base for this one.""")
+
+ def addLocal(relationship):
+ '''add local copy except during update. If no other relationship
+ exists for the token, select it. If no relationship already exists
+ for the child tokens, disallow, raising ValueError.'''
+
+ def addModified(relationship):
+ '''add modified copies during update If no other relationship
+ exists for the token, select it. If no relationship already exists
+ for the child tokens, disallow, raising ValueError.'''
+
+ def addSuggested(relationship):
+ '''add suggested copies during update. Another relationship must
+ already exist in the manifest for the relationship's token.'''
+
+ def checkRelationshipChange(relationship):
+ """raise errors if the relationship may not be changed.
+ UpdateError if the manifest is updating and the relationship is LOCAL;
+ TypeError (XXX?) if the relationship is SUGGESTED or UPDATED"""
+
+ def beginUpdate(source=None, base=None):
+ '''begin an update. Calculates update conflicts, tries to resolve.
+ If source is None, uses vault's most recent manifest. If base is None,
+ uses the most recent shared base between this manifest and the source,
+ if any.
+
+ if already updating, raise UpdateError.
+
+ update conflicts (different changes from the base both locally and in
+ the source) are given to an interfaces.IConflictResolver, if an
+ adapter to this interface is provided by the vault, to do with as it
+ will (typically including making suggestions and resolving).'''
+
+ def beginCollectionUpdate(source):
+ '''cherry-pick update interface: CURRENTLY IN FLUX'''
+
+ def completeUpdate():
+ '''moves update source to bases; turns off updating; discards unused
+ suggested, modified, and local relationships.
+
+ Newer versions of the bases of the update source will replace the
+ bases of this manifest. if a BASE or MERGED relationship (see getType
+ for definitions) is selected and its source is no longer a part of the
+ bases after the bases are replaced, a new (mutable) copy is created as
+ a local relationship.'''
+
+ def abortUpdate():
+ '''return manifest to state before beginUpdate.'''
+
+ def iterChanges(base=None):
+ ''''iterate over all selected relationships that differ from the base.
+ if base is not given, uses self.base_source'''
+
+ def reindex(relationship):
+ '''reindex the relationship after a change: used by relationships.'''
+
+ def get(token, default=None):
+ '''return the currently selected relationship for the token'''
+
+ def getType(relationship):
+ '''return type of relationship: one of BASE, LOCAL, UPDATED,
+ SUGGESTED, MODIFIED, MERGED (see constants in this file, above).
+
+ BASE relationships are those in the base_source (which is the manifest
+ from the current vault on which this manifest was based). There will
+ only be zero or one BASE relationship for any given token in a
+ manifest.
+
+ LOCAL relationships are new relationships added (replacing or in
+ addition to BASE) when not updating. They may not be modified while
+ the manifest is updating. There will only be zero or one LOCAL
+ relationship for any given token in a manifest.
+
+ UPDATED relationships only are available in this manifest while
+ updating, and are relationships changed from the BASE of the same
+ token. They may not be modified, even if they have not been versioned
+ (e.g., added via `beginCollectionUpdate`). There will only be zero or
+ one UPDATED relationship for any given token in a manifest.
+
+ SUGGESTED relationships only exist while updating, and are intended to
+ be relationships that an IConflictResolver created (although the
+ resolvers have free reign). They may not be modified, even if they
+ have not been versioned. There will be zero or more (unbounded)
+ SUGGESTED relationships for any given token in a manifest. All
+ MODIFIED relationships are discarded after an `abortUpdate`.
+
+ MODIFIED relationships only exist while updating, and are the only
+ relationships that may be modified while updating. There will be zero
+ or more (unbounded) MODIFIED relationships for any given token in a
+ manifest. Unselected MODIFIED relationships are discarded after an
+ `completeUpdate`, and all MODIFIED relationships are discarded after
+ an `abortUpdate`.
+
+ MERGED relationships are those in the manifests returned by
+ `getBaseSources` that are not the `base_source`: that is, typically
+ those in manifests that have been merged into this one. There will
+ be zero or more MERGED relationships--no more than
+ `len(self.getBaseSources()) -1`--in a manifest for a given token.
+
+ '''
+
+ def isSelected(relationship):
+ '''bool whether relationship is selected'''
+
+ def select(relationship):
+ '''select the relationship for the given token. There should always be
+ one and only one selected relationship for any given token known about
+ by the manifest.'''
+
+ def getBase(token, default=None):
+ '''Get the base relationship for the token, or default if None'''
+
+ def getLocal(token, default=None):
+ '''Get the local relationship for the token, or default if None'''
+
+ def getUpdated(token, default=None):
+ '''Get the updated relationship for the token, or default if None'''
+
+ def iterSuggested(token):
+ '''Iterate over suggested relationships for the token.'''
+
+ def iterModified(token):
+ '''Iterate over modified relationships for the token.'''
+
+ def iterMerged(token):
+ '''Iterate over merged relationships for the token.'''
+
+ def iterSelectedParents(token):
+ '''Iterate over selected parent for the token. If there is more than
+ one, it is a parent conflict; if there are none and the token is not
+ the zc.vault.keyref.top_token, it is an orphan.'''
+
+ def iterParents(token):
+ '''iterate over all possible parents, selected and unselected, for the
+ token'''
+
+ def isLinked(token, child):
+ '''returns boolean, whether child token is transitively linked
+ beneath token using only selected relationships.'''
+
+ def iterUpdateConflicts():
+ '''iterate over unresolved update conflicts.'''
+
+ def iterUpdateResolutions():
+ '''iterate over resolved update conflicts.'''
+
+ def isUpdateConflict(token):
+ '''returns boolean, whether token is an unresolved update conflict.'''
+
+ def resolveUpdateConflict(token):
+ '''resolve the update conflict ("stop complaining, and use whatever is
+ selected")'''
+
+ def iterOrphanConflicts():
+ '''iterate over unresolved orphan conflicts--selected relationships
+ changed from the BASE and MERGED relationships.'''
+
+ def iterOrphanResolutions():
+ '''iterate over resolved orphan conflicts.'''
+
+ def isOrphan(token):
+ '''Whether token is an orphan.'''
+
+ def isOrphanConflict(token):
+ '''Whether token is an unresolved orphan token, as found in
+ iterOrphanConflicts'''
+
+ def resolveOrphanConflict(token):
+ '''resolve the orphan conflict'''
+
+ def undoOrphanConflictResolution(token):
+ '''undo orphan conflict resolution'''
+
+ def iterParentConflicts():
+ '''iterate over all selected relationships that have more than
+ one parent.'''
+
+ def iterAll():
+ '''iterate over all relationships known (of all types)'''
+
+ def iterSelections():
+ '''iterate over all selected relationships.'''
+
+ def __iter__():
+ '''iterate over linked, selected relationships: selected non-orphans.
+ '''
+
+ def iterUnchangedOrphans():
+ '''iterate over BASE and MERGED orphans (that do not cause conflicts)
+ '''
+
+ next = interface.Attribute('the next manifest in the vault')
+
+ previous = interface.Attribute(
+ "the previous manifest in the vault, or from a branch's source")
+
+ def isOption(relationship):
+ """boolean, whether relationship is known (in iterAll)"""
Added: zc.vault/trunk/src/zc/vault/keyref.py
===================================================================
--- zc.vault/trunk/src/zc/vault/keyref.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/keyref.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,74 @@
+import itertools
+import persistent
+from ZODB.interfaces import IConnection
+from zope import interface
+from zope.cachedescriptors.property import Lazy
+import zope.app.keyreference.interfaces
+import zope.app.container.contained
+from zc.vault import interfaces
+
+class AbstractUniqueReference(object):
+ interface.implements(
+ zope.app.keyreference.interfaces.IKeyReference,
+ interfaces.IUniqueReference)
+
+ __slots__ = ()
+
+ # must define identifiers and key_type_id
+
+ def __call__(self):
+ return self
+
+ def __hash__(self):
+ return hash(tuple(self.identifiers))
+
+ def __cmp__(self, other):
+ if interfaces.IUniqueReference.providedBy(other):
+ return cmp(tuple(self.identifiers), tuple(other.identifiers))
+ if zope.app.keyreference.interfaces.IKeyReference.providedBy(other):
+ assert self.key_type_id != other.key_type_id
+ return cmp(self.key_type_id, other.key_type_id)
+ raise ValueError(
+ "Can only compare against IUniqueIdentity and "
+ "IKeyReference objects")
+
+# XXX this is API; need to highlight in tests...
+def getPersistentIdentifiers(obj):
+ if obj._p_oid is None:
+ connection = IConnection(obj, None)
+ if connection is None:
+ raise zope.app.keyreference.interfaces.NotYet(obj)
+ connection.add(obj)
+ return (obj._p_jar.db().database_name, obj._p_oid)
+
+class Token(AbstractUniqueReference, persistent.Persistent,
+ zope.app.container.contained.Contained):
+
+ interface.implements(interfaces.IToken)
+
+ __slots__ = ()
+
+ key_type_id = 'zc.vault.keyref.Token'
+
+ @Lazy
+ def identifiers(self):
+ return (self.key_type_id,) + getPersistentIdentifiers(self)
+
+class _top_token_(AbstractUniqueReference): # creates singleton
+
+ interface.implements(interfaces.IToken)
+
+ __slots__ = ()
+
+ key_type_id = 'zc.vault.keyref.TopToken'
+
+ identifiers = (key_type_id,)
+
+ def __reduce__(self):
+ return _top_token, ()
+
+top_token = _top_token_()
+
+def _top_token():
+ return top_token
+_top_token.__safe_for_unpickling__ = True
Added: zc.vault/trunk/src/zc/vault/objectlog.py
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,176 @@
+from zope import interface, component, schema
+import zope.app.intid.interfaces
+import zope.lifecycleevent.interfaces
+import zope.location
+import zc.objectlog.interfaces
+from zc import objectlog
+import zc.copyversion.interfaces
+from zc.vault.i18n import _
+from zc.vault import interfaces
+
+class IManifestChangeset(interface.Interface):
+ update_source_intid = schema.Int(
+ title=_('Update Source'),
+ description=_('Will be None for collection update'),
+ required=False)
+ update_base_intid = schema.Int(
+ title=_('Update Base'),
+ description=_('Will be None for collection update'),
+ required=False)
+ vault_intid = schema.Int(
+ title=_('Vault'),
+ required=True)
+
+class ManifestChangesetAdapter(object):
+ interface.implements(IManifestChangeset)
+ component.adapts(interfaces.IManifest)
+ def __init__(self, man):
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ self.vault_intid = intids.register(man.vault)
+ self.update_source_intid = self.update_base_intid = None
+ if interfaces.IManifest.providedBy(man.update_source):
+ self.update_source_intid = intids.register(man.update_source)
+ if interfaces.IManifest.providedBy(man.update_base):
+ self.update_base_intid = intids.register(man.update_base)
+
+class IRelationshipChangeset(interface.Interface):
+ items = schema.Tuple(
+ title=_('Items'), required=True)
+ object_intid = schema.Int(title=_('Object'), required=False)
+
+class RelationshipChangesetAdapter(object):
+ interface.implements(IRelationshipChangeset)
+ component.adapts(interfaces.IRelationship)
+ def __init__(self, relationship):
+ self.items = tuple((k, v) for k, v in relationship.containment.items())
+ if relationship.object is None:
+ self.object_intid = None
+ else:
+ intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ self.object_intid = intids.register(relationship.object)
+
+ at component.adapter(
+ interfaces.IManifest, zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def createManifestLog(man, ev):
+ if not zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log = objectlog.Log(IManifestChangeset)
+ zope.location.locate(man.log, man, 'log')
+ interface.directlyProvides(man, zc.objectlog.interfaces.ILogging)
+ man.log(_('Created'))
+
+ at component.adapter(
+ interfaces.IRelationship,
+ zope.lifecycleevent.interfaces.IObjectCreatedEvent)
+def createRelationshipLog(rel, ev):
+ if not zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log = objectlog.Log(IRelationshipChangeset)
+ zope.location.locate(rel.log, rel, 'log')
+ interface.directlyProvides(rel, zc.objectlog.interfaces.ILogging)
+ rel.log(_('Created'))
+ rel.log(_('Created (end of transaction)'), defer=True, if_changed=True)
+
+ at component.adapter(interfaces.IObjectRemoved)
+def logRemoval(ev):
+ rel = ev.mapping.__parent__
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Child removed'))
+
+ at component.adapter(interfaces.IObjectAdded)
+def logAddition(ev):
+ rel = ev.mapping.__parent__
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Child added'))
+
+ at component.adapter(interfaces.IOrderChanged)
+def logOrderChanged(ev):
+ rel = ev.object.__parent__
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Child order changed'))
+
+ at component.adapter(interfaces.IManifestCommitted)
+def logCommit(ev):
+ man = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log(_('Committed'))
+
+ at component.adapter(
+ interfaces.IRelationship, zc.copyversion.interfaces.IObjectVersionedEvent)
+def logVersioning(rel, ev):
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Versioned'))
+
+ at component.adapter(interfaces.ILocalRelationshipAdded)
+def logNewLocal(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Added as local relationship'))
+
+ at component.adapter(interfaces.IModifiedRelationshipAdded)
+def logNewModified(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Added as modified relationship'))
+
+ at component.adapter(interfaces.ISuggestedRelationshipAdded)
+def logNewSuggested(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Added as suggested relationship'))
+
+ at component.adapter(interfaces.IUpdateBegun)
+def logUpdateBegun(ev):
+ man = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log(_('Update begun'))
+
+ at component.adapter(interfaces.IUpdateAborted)
+def logUpdateAborted(ev):
+ man = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log(_('Update aborted'))
+
+ at component.adapter(interfaces.IUpdateCompleted)
+def logUpdateCompleted(ev):
+ man = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log(_('Update completed'))
+
+ at component.adapter(interfaces.IVaultChanged)
+def logVaultChanged(ev):
+ man = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(man):
+ man.log(_('Vault changed'))
+
+# ADDITIONAL_SELECTION = _('Selected in additional manifest (${intid})')
+ at component.adapter(interfaces.IRelationshipSelected)
+def logSelection(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ if ev.manifest is rel.__parent__:
+ rel.log(_('Selected'))
+ # else:
+ # intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ # msg = i18n.Message(ADDITIONAL_SELECTION,
+ # mapping={'intid': intids.register(
+ # ev.manifest)})
+ # rel.log(msg)
+
+# ADDITIONAL_DESELECTION = _('Deselected from additional manifest (${intid})')
+ at component.adapter(interfaces.IRelationshipDeselected)
+def logDeselection(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ if ev.manifest is rel.__parent__:
+ rel.log(_('Deselected'))
+ # else:
+ # intids = component.getUtility(zope.app.intid.interfaces.IIntIds)
+ # msg = i18n.Message(ADDITIONAL_DESELECTION,
+ # mapping={'intid': intids.register(
+ # ev.manifest)})
+ # rel.log(msg)
+
+ at component.adapter(interfaces.IObjectChanged)
+def logObjectChanged(ev):
+ rel = ev.object
+ if zc.objectlog.interfaces.ILogging.providedBy(rel):
+ rel.log(_('Object changed'))
Added: zc.vault/trunk/src/zc/vault/objectlog.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.txt 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.txt 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,293 @@
+The objectlog module gives objectlogs to manifests and relationships and keeps
+track of most of the changes. It is all handled with subscribers and two
+adapters. These have already been installed for these examples; see
+objectlog.zcml for zcml that loads the subscribers, or tests.py for the code
+that installs the components when this file is run as a test.
+
+ >>> from zc.vault.vault import Vault, Inventory
+ >>> from zc.vault.core import Manifest
+ >>> from zc.vault import interfaces
+ >>> v = Vault()
+ >>> app['vault'] = v
+ >>> i = Inventory(vault=v)
+
+Initially, manifests do not have a log. You must fire an
+ObjectCreatedEvent to get it installed. However, creating a manifest
+with an inventory accomplishes this for you transparently, so you
+actually don't have to do anything.
+
+ >>> import zc.objectlog.interfaces
+ >>> man = i.manifest
+ >>> zc.objectlog.interfaces.ILogging.providedBy(man)
+ True
+
+This also makes a log entry recording the creation.
+
+ >>> len(man.log)
+ 1
+ >>> man.log[0].summary
+ u'Created'
+
+In addition to the basic log entry data (summary, description, time
+stamp, principals), the log record keeps track of the update sources
+and update bases, for manifest-based (non-collection) updates. For
+now, these are None.
+
+ >>> man.log[0].record.update_source_intid # None
+ >>> man.log[0].record.update_base_intid # None
+
+Merely accessing the inventory contents creates a top-level relationship, and
+this is logged.
+
+ >>> import persistent
+ >>> from zope.app.container.contained import Contained
+ >>> from zc.copyversion.versioning import Versioning
+ >>> class Demo(persistent.Persistent, Contained, Versioning):
+ ... def __repr__(self):
+ ... return "<%s %r>" % (self.__class__.__name__, self.__name__)
+ ...
+ >>> zc.objectlog.interfaces.ILogging.providedBy(i.contents.relationship)
+ True
+ >>> len(i.contents.relationship.log)
+ 3
+ >>> i.contents.relationship.log[0].summary
+ u'Created'
+ >>> i.contents.relationship.log[1].summary
+ u'Added as local relationship'
+ >>> i.contents.relationship.log[2].summary
+ u'Selected'
+
+If we add an item, the top relationship will have another log entry and the
+new relationship will also have a log.
+
+ >>> app['d1'] = Demo()
+ >>> i.contents[u'donald'] = app['d1']
+ >>> len(i.contents.relationship.log)
+ 4
+ >>> i.contents.relationship.log[3].summary
+ u'Child added'
+ >>> rel = i.contents('donald').relationship
+ >>> len(rel.log)
+ 3
+ >>> rel.log[0].summary
+ u'Created'
+ >>> rel.log[1].summary
+ u'Added as local relationship'
+ >>> rel.log[2].summary
+ u'Selected'
+ >>> i.contents.relationship.log[2].summary
+ u'Selected'
+
+The log's record keeps track of the items and the object intid.
+
+ >>> from zope.app.intid.interfaces import IIntIds
+ >>> from zope import component, interface
+ >>> intids = component.getUtility(IIntIds)
+ >>> intids.getObject(rel.log[-1].record.object_intid) is app['d1']
+ True
+ >>> i.contents.relationship.log[-1].record.object_intid # None
+ >>> rel.log[-1].record.items
+ ()
+ >>> [key for key, val in i.contents.relationship.log[-1].record.items]
+ [u'donald']
+ >>> i.manifest.get(
+ ... i.contents.relationship.log[-1].record.items[0][1]
+ ... ).object is app['d1']
+ True
+
+Let's add a few more items, then reorder: the reordering should generate a new
+log message.
+
+ >>> app['e1'] = Demo()
+ >>> app['f1'] = Demo()
+ >>> i.contents[u'edward'] = app['e1']
+ >>> i.contents[u'fred'] = app['f1']
+ >>> len(i.contents.relationship.log)
+ 6
+ >>> [key for key, val in i.contents.relationship.log[-1].record.items]
+ [u'donald', u'edward', u'fred']
+ >>> i.contents.updateOrder([u'donald', u'edward', u'fred']) # no change
+ >>> len(i.contents.relationship.log)
+ 6
+ >>> i.contents.updateOrder([u'donald', u'fred', u'edward'])
+ >>> len(i.contents.relationship.log)
+ 7
+ >>> i.contents.relationship.log[-1].summary
+ u'Child order changed'
+
+Now we'll delete one: another new message.
+
+ >>> del i.contents['donald']
+ >>> len(i.contents.relationship.log)
+ 8
+ >>> i.contents.relationship.log[-1].summary
+ u'Child removed'
+
+When we commit, the manifest log gets a new entry.
+
+ >>> len(i.manifest.log)
+ 1
+ >>> i.iterOrphanConflicts().next().resolveOrphanConflict()
+ >>> v.commit(i)
+ >>> len(i.manifest.log)
+ 2
+ >>> i.manifest.log[-1].summary
+ u'Committed'
+
+The individual relationships record when they were versioned--at the same time
+as the commit.
+
+ >>> i.contents.relationship.log[-1].summary
+ u'Versioned'
+
+Other logs occur during updates, so we need to create a situation that needs
+an update, and that generates a suggestion.
+
+ >>> i = Inventory(vault=v, mutable=True)
+ >>> concurrent = Inventory(vault=v, mutable=True)
+ >>> concurrent.contents('fred')['denise'] = app['d1']
+ >>> concurrent.contents['gary'] = app['g1'] = Demo()
+ >>> v.commit(concurrent)
+
+ >>> from zope import event
+ >>> import zope.lifecycleevent
+ >>> from zc.vault import interfaces
+ >>> from zc.vault.core import Relationship
+ >>> @component.adapter(interfaces.IVault)
+ ... @interface.implementer(interfaces.IConflictResolver)
+ ... def factory(vault):
+ ... def resolver(manifest, local, updated, base):
+ ... if local.object is not base.object:
+ ... if updated.object is base.object:
+ ... object = local.object
+ ... else:
+ ... return
+ ... else:
+ ... object = updated.object
+ ... if local.containment != base.containment:
+ ... if updated.containment != base.containment:
+ ... return
+ ... else:
+ ... containment = local.containment
+ ... else:
+ ... containment = updated.containment
+ ... suggested = Relationship(local.token, object, containment)
+ ... suggested.__parent__ = manifest
+ ... event.notify(zope.lifecycleevent.ObjectCreatedEvent(
+ ... suggested))
+ ... manifest.addSuggested(suggested)
+ ... manifest.select(suggested)
+ ... manifest.resolveUpdateConflict(local.token)
+ ... return resolver
+ ...
+ >>> component.provideAdapter(factory)
+
+While we are at it, changing an object generates a log entry.
+
+ >>> i.contents('fred').type == interfaces.BASE
+ True
+ >>> i.contents['fred'] = app['f2'] = Demo()
+ >>> i.contents('fred').type == interfaces.LOCAL
+ True
+ >>> len(i.contents('fred').relationship.log)
+ 4
+ >>> i.contents('fred').relationship.log[-1].summary
+ u'Object changed'
+
+When we begin the update, we again get a log.
+
+ >>> i.beginUpdate()
+ >>> i.manifest.log[-1].summary
+ u'Update begun'
+
+So, the manifest keeps track of the update. If it is based on a manifest, it
+also keeps track of which manifest was merged, and what the base was, using
+intids.
+
+ >>> (intids.getObject(i.manifest.log[-1].record.update_source_intid) is
+ ... v.manifest)
+ True
+ >>> (intids.getObject(i.manifest.log[-1].record.update_base_intid) is
+ ... v[-2])
+ True
+
+The suggested relationship has a log entry describing its entry into the
+system.
+
+ >>> i.contents('fred').type == interfaces.SUGGESTED
+ True
+ >>> i.contents('fred').relationship.log[-2].summary
+ u'Added as suggested relationship'
+ >>> i.contents('fred').relationship.log[-1].summary
+ u'Selected'
+
+If we select another relationship, the previously selected one gets a
+'Deselected' log entry.
+
+ >>> suggested = i.contents('fred')
+ >>> i.contents('fred').local_item.select()
+ >>> i.contents('fred').type == interfaces.LOCAL
+ True
+ >>> suggested.relationship.log[-1].summary
+ u'Deselected'
+ >>> i.contents('fred').relationship.log[-1].summary
+ u'Selected'
+
+If the update is aborted, it is logged.
+
+ >>> i.abortUpdate()
+ >>> i.manifest.log[-1].summary
+ u'Update aborted'
+ >>> i.manifest.log[-1].record.update_source_intid # None
+ >>> i.manifest.log[-1].record.update_base_intid # None
+
+Now we'll restart the update, add a modified relationship, and complete the
+update.
+
+ >>> i.beginUpdate()
+ >>> i.contents['edward'] = app['e2'] = Demo()
+ >>> i.contents('edward').relationship.log[-3].summary
+ u'Added as modified relationship'
+ >>> i.contents('edward').relationship.log[-2].summary
+ u'Selected'
+ >>> i.contents('edward').relationship.log[-1].summary
+ u'Object changed'
+ >>> i.completeUpdate()
+ >>> i.manifest.log[-1].summary
+ u'Update completed'
+
+Note that the edward and fred relationships that were modified and suggested,
+respectively, are now logged as local.
+
+ >>> i.contents('edward').relationship.log[-1].summary
+ u'Added as local relationship'
+ >>> i.contents('fred').relationship.log[-1].summary
+ u'Added as local relationship'
+
+The last log occurs when a manifest changes vaults. Manifest log records keep
+track of vaults, so the changes are available in history.
+
+ >>> intids.getObject(i.manifest.log[-1].record.vault_intid) is v
+ True
+
+Now we'll make a branch and move i over to the branch. Notice that we make a
+manifest ourselves here, not relying on the Inventory class, so we have to fire
+the creation event ourselves.
+
+ >>> branch = app['branch'] = Vault(v.intids)
+ >>> man = Manifest(v.manifest)
+ >>> event.notify(zope.lifecycleevent.ObjectCreatedEvent(man))
+ >>> branch.commit(man)
+ >>> branch.manifest.log[-2].summary
+ u'Vault changed'
+ >>> intids.getObject(branch.manifest.log[-2].record.vault_intid) is branch
+ True
+ >>> intids.getObject(branch.manifest.log[-3].record.vault_intid) is v
+ True
+ >>> i.vault = branch
+ >>> branch.manifest.log[-2].summary
+ u'Vault changed'
+ >>> i.beginUpdate()
+ >>> branch.commit(i)
+ >>> import transaction
+ >>> transaction.commit()
Property changes on: zc.vault/trunk/src/zc/vault/objectlog.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.vault/trunk/src/zc/vault/objectlog.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/objectlog.zcml 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/objectlog.zcml 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zc.vault">
+
+ <subscriber handler=".objectlog.createManifestLog" />
+
+ <subscriber handler=".objectlog.createRelationshipLog" />
+
+ <subscriber handler=".objectlog.logRemoval" />
+
+ <subscriber handler=".objectlog.logAddition" />
+
+ <subscriber handler=".objectlog.logOrderChanged" />
+
+ <subscriber handler=".objectlog.logCommit" />
+
+ <subscriber handler=".objectlog.logVersioning" />
+
+ <subscriber handler=".objectlog.logNewLocal" />
+
+ <subscriber handler=".objectlog.logNewModified" />
+
+ <subscriber handler=".objectlog.logNewSuggested" />
+
+ <subscriber handler=".objectlog.logUpdateBegun" />
+
+ <subscriber handler=".objectlog.logUpdateAborted" />
+
+ <subscriber handler=".objectlog.logUpdateCompleted" />
+
+ <subscriber handler=".objectlog.logVaultChanged" />
+
+ <subscriber handler=".objectlog.logSelection" />
+
+ <subscriber handler=".objectlog.logDeselection" />
+
+ <subscriber handler=".objectlog.logObjectChanged" />
+
+ <adapter factory=".objectlog.ManifestChangesetAdapter" />
+
+ <adapter factory=".objectlog.RelationshipChangesetAdapter" />
+
+ <class class=".core.Manifest">
+ <require permission="zope.View"
+ attributes="log"/>
+ </class>
+
+ <class class=".core.Relationship">
+ <require permission="zope.View"
+ attributes="log"/>
+ </class>
+
+
+</configure>
Added: zc.vault/trunk/src/zc/vault/tests.py
===================================================================
--- zc.vault/trunk/src/zc/vault/tests.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/tests.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Relationship tests
+
+$Id$
+"""
+import unittest
+import re
+from zope.testing import doctest, renormalizing
+import zope.testing.module
+
+# these are used by setup
+import transaction
+import persistent
+from persistent.interfaces import IPersistent
+from ZODB.interfaces import IConnection
+from ZODB.tests.util import DB
+
+from zope import component, interface, event
+import zope.component.interfaces
+from zope.app.testing import placelesssetup
+from zope.app.keyreference.persistent import (
+ KeyReferenceToPersistent, connectionOfPersistent)
+from zope.app.folder import rootFolder
+from zope.app.component.site import LocalSiteManager
+from zope.app.intid import IntIds
+from zope.app.intid.interfaces import IIntIds
+import zope.app.component.interfaces.registration
+import zope.annotation.interfaces
+import zope.annotation.attribute
+import zope.app.component.hooks
+
+def setUp(test):
+ placelesssetup.setUp()
+ events = test.globs['events'] = []
+ event.subscribers.append(events.append)
+ component.provideAdapter(KeyReferenceToPersistent, adapts=(IPersistent,))
+ import zope.app.component.site
+ import zope.component.interfaces
+ import zope.location.interfaces
+ component.provideAdapter(
+ zope.app.component.site.SiteManagerAdapter,
+ (zope.location.interfaces.ILocation,),
+ zope.component.interfaces.IComponentLookup)
+ component.provideAdapter(
+ connectionOfPersistent,
+ adapts=(IPersistent,),
+ provides=IConnection)
+ test.globs['db'] = db = DB()
+ test.globs['conn'] = conn = db.open()
+ test.globs['root'] = root = conn.root()
+ test.globs['app'] = app = root['app'] = rootFolder()
+ app.setSiteManager(LocalSiteManager(app))
+ transaction.commit()
+ app = test.globs['app']
+ sm = app.getSiteManager()
+ sm['intids'] = IntIds()
+ registry = zope.component.interfaces.IComponentRegistry(sm)
+ registry.registerUtility(sm['intids'], IIntIds)
+ transaction.commit()
+ zope.app.component.hooks.setSite(app)
+ zope.app.component.hooks.setHooks()
+ zope.testing.module.setUp(test, 'zc.vault.README')
+
+def tearDown(test):
+ import transaction
+ transaction.abort()
+ zope.testing.module.tearDown(test)
+ zope.app.component.hooks.resetHooks()
+ zope.app.component.hooks.setSite()
+ events = test.globs.pop('events')
+ assert event.subscribers.pop().__self__ is events
+ del events[:] # being paranoid
+ transaction.abort()
+ test.globs['db'].close()
+ placelesssetup.tearDown()
+
+def objectlogSetUp(test):
+ setUp(test)
+ from zope.app.container.contained import NameChooser
+ from zope.app.container.interfaces import IWriteContainer
+ component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+ from zc.vault import objectlog
+ component.provideAdapter(objectlog.ManifestChangesetAdapter)
+ component.provideAdapter(objectlog.RelationshipChangesetAdapter)
+ component.provideHandler(objectlog.createManifestLog)
+ component.provideHandler(objectlog.createRelationshipLog)
+ component.provideHandler(objectlog.logRemoval)
+ component.provideHandler(objectlog.logAddition)
+ component.provideHandler(objectlog.logOrderChanged)
+ component.provideHandler(objectlog.logCommit)
+ component.provideHandler(objectlog.logVersioning)
+ component.provideHandler(objectlog.logNewLocal)
+ component.provideHandler(objectlog.logNewSuggested)
+ component.provideHandler(objectlog.logNewModified)
+ component.provideHandler(objectlog.logUpdateBegun)
+ component.provideHandler(objectlog.logUpdateAborted)
+ component.provideHandler(objectlog.logUpdateCompleted)
+ component.provideHandler(objectlog.logVaultChanged)
+ component.provideHandler(objectlog.logSelection)
+ component.provideHandler(objectlog.logDeselection)
+ component.provideHandler(objectlog.logObjectChanged)
+
+def catalogSetUp(test):
+ setUp(test)
+ from zope.app.container.contained import NameChooser
+ from zope.app.container.interfaces import IWriteContainer
+ component.provideAdapter(NameChooser, adapts=(IWriteContainer,))
+ from zc.vault import catalog
+ component.provideHandler(catalog.makeReferences)
+ component.provideHandler(catalog.updateCompleted)
+ component.provideHandler(catalog.updateAborted)
+ component.provideHandler(catalog.manifestCreated)
+ component.provideHandler(catalog.relationshipSelected)
+ component.provideHandler(catalog.relationshipDeselected)
+ component.provideHandler(catalog.objectChanged)
+
+def traversalSetUp(test):
+ setUp(test)
+ import zc.copyversion.copier
+ component.provideAdapter(zc.copyversion.copier.location_copyfactory)
+ component.provideAdapter(zc.copyversion.copier.versiondata_copyfactory)
+ import zope.copypastemove
+ component.provideAdapter(zope.copypastemove.ObjectCopier)
+ import zc.shortcut.adapters
+ component.provideAdapter(zc.shortcut.adapters.ObjectLinkerAdapter)
+
+def test_suite():
+ checker = renormalizing.RENormalizing([
+ (re.compile(r'^\d+$', re.M), '1234567'),
+ ])
+
+ tests = (
+ doctest.DocFileSuite(
+ 'README.txt',
+ setUp=setUp, tearDown=tearDown, checker=checker),
+ doctest.DocFileSuite(
+ 'versions.txt',
+ setUp=setUp, tearDown=tearDown),
+ # separate this out so this only runs if pertinent modules available?
+ doctest.DocFileSuite(
+ 'catalog.txt',
+ setUp=catalogSetUp, tearDown=tearDown),
+ )
+
+ try:
+ import zc.objectlog
+ tests += (
+ doctest.DocFileSuite(
+ 'objectlog.txt',
+ setUp=objectlogSetUp, tearDown=tearDown),)
+ except ImportError:
+ # no zc.objectlog available, so don't try to test integration with it
+ pass
+
+ try:
+ import zc.shortcut
+ tests += (doctest.DocFileSuite(
+ 'traversal.txt',
+ setUp=traversalSetUp, tearDown=tearDown),)
+ except ImportError:
+ pass
+
+ return unittest.TestSuite(tests)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Added: zc.vault/trunk/src/zc/vault/traversal.py
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,324 @@
+import zope.component
+import zope.interface
+import zope.interface.common.mapping
+import zope.location
+import zope.proxy
+import zope.copypastemove.interfaces
+import zope.app.container.interfaces
+import zope.app.container.constraints
+
+import zc.copyversion.copier
+import zc.copyversion.interfaces
+import zc.shortcut
+import zc.shortcut.proxy
+import zc.shortcut.interfaces
+
+# interfaces
+
+class IInventoryItemAware(zope.interface.Interface):
+ _z_inventory_node = zope.interface.Attribute(
+ """a zc.vault.interfaces.IContained (an IInventoryItem or an
+ IInventoryContents.""")
+
+class IInventoryItemAwareFactory(zope.interface.Interface):
+ def __call__(item, parent, name):
+ """returns an object that provudes IInventoryItemAware"""
+
+class IProxy(
+ IInventoryItemAware, zc.shortcut.interfaces.ITraversalProxy):
+ """these proxies have _z_inventory_node, __traversed_parent__, and
+ __traversed_name__"""
+
+class IData(zope.interface.Interface):
+ """A marker interface that indicates that this object should be adapted
+ to IInventoryItemAwareFactory, and then the factory should be called with
+ the object's item, its parent, and its name within the parent."""
+
+# the proxy
+
+class Proxy(zc.shortcut.proxy.ProxyBase):
+ zc.shortcut.proxy.implements(IProxy)
+ __slots__ = '_z_inventory_node',
+
+ def __new__(self, ob, parent, name, item):
+ return zc.shortcut.proxy.ProxyBase.__new__(self, ob, parent, name)
+
+ def __init__(self, ob, parent, name, item):
+ zc.shortcut.proxy.ProxyBase.__init__(self, ob, parent, name)
+ self._z_inventory_node = item
+
+# the containers
+
+class ReadContainer(zope.location.Location):
+ zope.interface.classProvides(IInventoryItemAwareFactory)
+ zope.interface.implements(
+ IInventoryItemAware, zope.interface.common.mapping.IEnumerableMapping)
+
+ def __init__(self, item, parent=None, name=None):
+ self.__parent__ = parent
+ self.__name__ = name
+ self._z_inventory_node = item
+
+ def __len__(self):
+ return len(self._z_inventory_node)
+
+ def __iter__(self):
+ return iter(self._z_inventory_node)
+
+ def __contains__(self, key):
+ return key in self._z_inventory_node
+
+ def __getitem__(self, key):
+ item = self._z_inventory_node(key)
+ if item.object is None:
+ factory = zope.component.getUtility(IInventoryItemAwareFactory)
+ return factory(item, self, key)
+ elif IData.providedBy(item.object):
+ factory = IInventoryItemAwareFactory(item.object)
+ return factory(item, self, key)
+ else:
+ return Proxy(item.object, self, key, item)
+
+ def keys(self):
+ return self._z_inventory_node.keys()
+
+ def values(self):
+ return [self[key] for key in self]
+
+ def items(self):
+ return [(key, self[key]) for key in self]
+
+ def get(self, key, default=None):
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def __getstate__(self):
+ raise RuntimeError('This should not be persisted.')
+
+class Container(ReadContainer):
+ zope.interface.implements(zope.interface.common.mapping.IMapping)
+
+ def __setitem__(self, key, value):
+ self._z_inventory_node[key] = value
+
+ def __delitem__(self, key):
+ del self._z_inventory_node[key]
+
+ def updateOrder(self, order):
+ self._z_inventory_node.updateOrder(order)
+
+# the movers and shakers
+
+# Unfortunately we have to duplicate the standard checkObject so we can
+# weed out the IContainer check, which is not pertinent here.
+def checkObject(container, name, object):
+ """Check containment constraints for an object and container
+ """
+
+ # check __setitem__ precondition
+ containerProvided = zope.interface.providedBy(container)
+ __setitem__ = containerProvided.get('__setitem__')
+ if __setitem__ is not None:
+ precondition = __setitem__.queryTaggedValue('precondition')
+ if precondition is not None:
+ precondition(container, name, object)
+
+ # check the constraint on __parent__
+ __parent__ = zope.interface.providedBy(object).get('__parent__')
+ if __parent__ is not None:
+ try:
+ validate = __parent__.validate
+ except AttributeError:
+ pass
+ else:
+ validate(container)
+
+def isInventoryObject(obj):
+ return obj._z_inventory_node.object is zope.proxy.removeAllProxies(obj)
+
+class ObjectMover(object):
+ """can only move objects within and among manifests; moving elsewhere
+ has reparenting connotations that are inappropriate, since inventory
+ membership and parentage are unrelated."""
+ zope.interface.implements(zope.copypastemove.interfaces.IObjectMover)
+ zope.component.adapts(IInventoryItemAware)
+
+ def __init__(self, context):
+ self.context = context
+ self.__parent__ = context
+
+ def moveTo(self, target, new_name=None):
+ if not IInventoryItemAware.providedBy(target):
+ raise ValueError('target must be IInventoryItemAware')
+ node = self.context._z_inventory_node
+ if new_name is None:
+ new_name = node.name
+ if node == target._z_inventory_node and new_name == node.name:
+ return # noop
+ manifest = node.inventory.manifest
+ if manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError(manifest)
+ if target._z_inventory_node.inventory.manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError(
+ target._z_inventory_node.inventory.manifest)
+ checkObject(target, new_name, self.context)
+ chooser = zope.app.container.interfaces.INameChooser(target)
+ new_name = chooser.chooseName(new_name, node.object)
+ node.moveTo(target._z_inventory_node, new_name)
+ return new_name
+
+ def moveable(self):
+ manifest = self.context._z_inventory_node.inventory.manifest
+ return not manifest._z_versioned
+
+ def moveableTo(self, target, new_name=None):
+ node = self.context._z_inventory_node
+ manifest = node.inventory.manifest
+ if (not manifest._z_versioned and
+ IInventoryItemAware.providedBy(target) and
+ not target._z_inventory_node.inventory.manifest._z_versioned):
+ if new_name is None:
+ new_name = node.name
+ try:
+ checkObject(target, new_name, self.context)
+ except zope.interface.Invalid:
+ pass
+ else:
+ return True
+ return False
+
+class ObjectCopier(object):
+ """Generally, make new copies of objects.
+
+ If target is from a non-versioned manifest, use
+ copyTo and then copy all of the non-None data objects in the tree.
+
+ otherwise if the object is a proxied leaf node, do a normal copy; otherwise
+ puke (can't copy a vault-specific object out of a vault).
+ """
+ zope.interface.implements(zope.copypastemove.interfaces.IObjectCopier)
+ zope.component.adapts(IInventoryItemAware)
+
+ def __init__(self, context):
+ self.context = context
+ self.__parent__ = context
+
+ def copyTo(self, target, new_name=None):
+ if IInventoryItemAware.providedBy(target):
+ if target._z_inventory_node.inventory.manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError(
+ target._z_inventory_node.inventory.manifest)
+ else:
+ if not isInventoryObject(self.context):
+ raise ValueError # TODO better error
+ return zope.copypastemove.interfaces.IObjectCopier(
+ zc.shortcut.proxy.removeProxy(self.context)).copyTo(
+ target, new_name)
+ node = self.context._z_inventory_node
+ manifest = node.inventory.manifest
+ if new_name is None:
+ new_name = node.name
+ checkObject(target, new_name, self.context)
+ chooser = zope.app.container.interfaces.INameChooser(target)
+ new_name = chooser.chooseName(new_name, node.object)
+ node.copyTo(target._z_inventory_node, new_name)
+ new_node = zope.proxy.removeAllProxies(
+ target._z_inventory_node(new_name))
+ stack = [(lambda x: new_node, iter(('',)))]
+ while stack:
+ node, i = stack[-1]
+ try:
+ key = i.next()
+ except StopIteration:
+ stack.pop()
+ else:
+ next = node(key)
+ original = next.object
+ next.object = zc.copyversion.copier.copy(original)
+ stack.append((next, iter(next)))
+ return new_name
+
+ def copyable(self):
+ return True
+
+ def copyableTo(self, target, new_name=None):
+ if not self.copyable():
+ return False
+ if IInventoryItemAware.providedBy(target):
+ if target._z_inventory_node.inventory.manifest._z_versioned:
+ return False
+ check = checkObject
+ else:
+ if not isInventoryObject(self.context):
+ return False
+ check = zope.app.container.constraints.checkObject
+ node = self.context._z_inventory_node
+ manifest = node.inventory.manifest
+ if new_name is None:
+ new_name = node.name
+ try:
+ check(target, new_name, self.context)
+ except zope.interface.Invalid:
+ return False
+ else:
+ return True
+
+class ObjectLinker(object):
+ zope.component.adapts(IInventoryItemAware)
+ zope.interface.implements(zc.shortcut.interfaces.IObjectLinker)
+
+ def __init__(self, context):
+ self.context = context
+ self.__parent__ = context
+
+ def linkTo(self, target, new_name=None):
+ if IInventoryItemAware.providedBy(target):
+ if target._z_inventory_node.inventory.manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError(
+ target._z_inventory_node.inventory.manifest)
+ else:
+ if not isInventoryObject(self.context):
+ raise ValueError # TODO better error
+ return zc.shortcut.interfaces.IObjectLinker(
+ zc.shortcut.proxy.removeProxy(self.context)).linkTo(
+ target, new_name)
+ node = self.context._z_inventory_node
+ manifest = node.inventory.manifest
+ if new_name is None:
+ new_name = node.name
+ checkObject(target, new_name, self.context)
+ chooser = zope.app.container.interfaces.INameChooser(target)
+ new_name = chooser.chooseName(new_name, node.object)
+ node.copyTo(target._z_inventory_node, new_name)
+ return new_name
+
+ def linkable(self):
+ return True
+
+ def linkableTo(self, target, new_name=None):
+ if IInventoryItemAware.providedBy(target):
+ if target._z_inventory_node.inventory.manifest._z_versioned:
+ return False
+ obj = self.context
+ check = checkObject
+ else:
+ if not isInventoryObject(self.context):
+ return False
+ obj = self._createShortcut(
+ zc.shortcut.proxy.removeProxy(self.context))
+ check = zope.app.container.constraints.checkObject
+ node = self.context._z_inventory_node
+ manifest = node.inventory.manifest
+ if new_name is None:
+ new_name = node.name
+ try:
+ check(target, new_name, obj)
+ except zope.interface.Invalid:
+ return False
+ else:
+ return True
+
+ def _createShortcut(self, target):
+ return zc.shortcut.Shortcut(target)
Added: zc.vault/trunk/src/zc/vault/traversal.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.txt 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.txt 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,369 @@
+The vault package includes an add-on that helps build traversable elements.
+It has two main components: a containerish wrapper for inventory items, and
+optional proxies for returned objects.
+
+The container is intended to be a base class, typically exposing various
+elements of the data object. It's a mapping API, similar to a stripped down
+version of the inventory contents.
+
+ >>> import zc.vault.vault
+ >>> import zc.vault.traversal
+ >>> v = app['vault'] = zc.vault.vault.Vault()
+ >>> i = app['i'] = zc.vault.vault.Inventory(vault=v)
+ >>> container = zc.vault.traversal.Container(i.contents)
+ >>> list(container.keys())
+ []
+ >>> list(container.values())
+ []
+ >>> container['foo']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'foo'
+ >>> container.get('foo') # None
+ >>> list(container)
+ []
+
+These containers have a special attribute: _z_inventory_node, the inventory
+node that backs the container.
+
+ >>> container._z_inventory_node == i.contents
+ True
+
+This means that they implement IInventoryItemAware.
+
+ >>> zc.vault.traversal.IInventoryItemAware.providedBy(container)
+ True
+
+If you add a simple data object, it will be returned from the container wrapped
+with a proxy.
+
+ >>> import zope.location
+ >>> import persistent
+ >>> import zope.annotation.interfaces
+ >>> import zope.interface
+ >>> import zc.copyversion.versioning
+ >>> import zope.app.container.interfaces
+ >>> class Demo(persistent.Persistent, zope.location.Location,
+ ... zc.copyversion.versioning.Versioning):
+ ... zope.interface.implements(
+ ... zope.annotation.interfaces.IAttributeAnnotatable,
+ ... zope.app.container.interfaces.IContained)
+ ... _counter = 0
+ ... def __init__(self):
+ ... self.id = self._counter
+ ... self.__class__._counter += 1
+ ... def __repr__(self):
+ ... return "<%s %d>" % (self.__class__.__name__, self.id)
+ ... def __call__(self):
+ ... return "I am number %d" % (self.id,)
+ ...
+ >>> d = app['demo'] = Demo()
+ >>> d
+ <Demo 0>
+ >>> d()
+ 'I am number 0'
+ >>> container['d'] = d
+ >>> container['d']
+ <Demo 0>
+ >>> container['d']()
+ 'I am number 0'
+ >>> container['d'] is d
+ False
+ >>> type(d) # doctest: +ELLIPSIS
+ <class '...Demo'>
+ >>> type(container['d'])
+ <class 'zc.vault.traversal.Proxy'>
+
+This proxy is a zc.shortcut proxy subclass (the pertinent code in
+zc.shortcut should probably be moved out to zc.traversed). This means
+it has references to the traversed parent and the traversed name
+(without affecting __parent__ and __name__).
+
+ >>> container['d'].__traversed_parent__ is container
+ True
+ >>> container['d'].__traversed_name__
+ 'd'
+ >>> container['d'].__name__
+ u'demo'
+ >>> container['d'].__parent__ is app
+ True
+
+This makes some of the standard adapters provided by zc.shortcut work, such as
+the one for breadcrumbs and traversedURL.
+
+It also has a reference to the inventory item that represents this node in the
+tree.
+
+ >>> container['d']._z_inventory_node.object is d
+ True
+
+This makes the proxied object provide IInventoryItemAware.
+
+ >>> zc.vault.traversal.IInventoryItemAware.providedBy(container['d'])
+ True
+
+Finally, it provides a special zc.vault.traversal.IProxy interface (as well as
+the zc.shortcut.interfaces.ITraversalProxy interface).
+
+ >>> zc.vault.traversal.IProxy.providedBy(container['d'])
+ True
+ >>> zc.shortcut.interfaces.ITraversalProxy.providedBy(container['d'])
+ True
+
+The inventory item reference and the special interface make it possible to
+support custom object movers, custom object copiers, and custom traversers, as
+desired. We will observe some of those later.
+
+The containers have two special cases: None, and objects that implement
+zc.vault.traversal.IData. None values cause the container to look up a utility
+for zc.vault.traversal.IInventoryItemAwareFactory and call it with
+(inventory item, parent, name) and return the result. The
+zc.vault.traversal.Container itself can be used for this purpose.
+
+ >>> import zope.component
+ >>> zope.component.provideUtility(
+ ... zc.vault.traversal.Container,
+ ... provides=zc.vault.traversal.IInventoryItemAwareFactory)
+ >>> container['none'] = None
+ >>> isinstance(container['none'], zc.vault.traversal.Container)
+ True
+ >>> container['none'].__name__
+ 'none'
+ >>> container['none'].__parent__ is container
+ True
+ >>> list(container['none'].keys())
+ []
+ >>> container['none']['obj'] = Demo()
+ >>> container['none']['obj']
+ <Demo 1>
+ >>> (container['none']['obj'].__traversed_parent__._z_inventory_node ==
+ ... container['none']._z_inventory_node)
+ True
+
+Objects that implement zc.vault.traversal.IData are adapted to
+zc.vault.traversal.IInventoryItemAwareFactory, and then the result of calling
+the factory with (inventory item, parent, name) is returned.
+
+ >>> class DemoData(Demo):
+ ... zope.interface.implements(zc.vault.traversal.IData)
+ ... def __init__(self, title):
+ ... self.title = title
+ ... super(DemoData, self).__init__()
+ ...
+ >>> class DemoFactory(zc.vault.traversal.Container):
+ ... @property
+ ... def title(self):
+ ... return self._z_inventory_node.object.title
+ ...
+ >>> @zope.component.adapter(DemoData)
+ ... @zope.interface.implementer(
+ ... zc.vault.traversal.IInventoryItemAwareFactory)
+ ... def demofactory(obj):
+ ... return DemoFactory
+ ...
+ >>> zope.component.provideAdapter(demofactory)
+ >>> container['none']['data'] = DemoData('Demosthenes')
+ >>> container['none']['data'].title
+ 'Demosthenes'
+ >>> list(container['none']['data'].keys())
+ []
+ >>> container['none']['data']._z_inventory_node.object
+ <DemoData 2>
+
+This approach is more powerful than using None even for simple containers: for
+instance, you can put attribute annotations on the Demo object, while with None
+there's no place to store them.
+
+CopyPasteMove
+=============
+
+The module provides a mover, copier, and linker to work with standard and
+shortcut-extended container views. They adapt IInventoryItemAware, so can
+be used for objects wrapped with a zc.vault.traversal.Proxy,
+zc.vault.traversal.Containers, and any object that implements
+IInventoryItemAware.
+
+ >>> zope.component.provideAdapter(zc.vault.traversal.ObjectMover)
+ >>> zope.component.provideAdapter(zc.vault.traversal.ObjectCopier)
+ >>> zope.component.provideAdapter(zc.vault.traversal.ObjectLinker)
+
+ObjectMover
+-----------
+
+The mover will only move objects within and among manifests.
+
+ >>> import zope.copypastemove.interfaces
+ >>> mover = zope.copypastemove.interfaces.IObjectMover(container['d'])
+ >>> mover.moveable()
+ True
+ >>> mover.moveableTo(container['none']['data'])
+ True
+
+We need a name chooser to actually move. We'll register the default one, then
+move.
+
+ >>> import zope.app.container.contained
+ >>> zope.component.provideAdapter(
+ ... zope.app.container.contained.NameChooser,
+ ... adapts=(zope.interface.Interface,))
+ >>> mover.moveTo(container['none']['data'])
+ 'd'
+ >>> container['none']['data']['d']
+ <Demo 0>
+
+Insane moves won't work.
+
+ >>> mover = zope.copypastemove.interfaces.IObjectMover(container['none'])
+ >>> mover.moveTo(container['none']['data'])
+ Traceback (most recent call last):
+ ...
+ ValueError: May not move item to within itself
+
+When you move within an inventory, the move is remembered so that the previous
+location in an older inventory can be found. You can also move between
+inventories, which really just deletes from one and adds to the other without
+history.
+
+ >>> v2 = app['vault2'] = zc.vault.vault.Vault()
+ >>> i2 = app['i2'] = zc.vault.vault.Inventory(vault=v2)
+ >>> container2 = zc.vault.traversal.Container(i2.contents)
+ >>> mover.moveTo(container2)
+ 'none'
+ >>> list(container)
+ []
+ >>> list(container2)
+ ['none']
+ >>> container2['none']['data']['d']
+ <Demo 0>
+
+You can't move outside a vault: that has weird __parent__ issues that we claim
+make it insane.
+
+ >>> mover = zope.copypastemove.interfaces.IObjectMover(container2['none'])
+ >>> mover.moveableTo(app)
+ False
+ >>> mover.moveTo(app)
+ Traceback (most recent call last):
+ ...
+ ValueError: target must be IInventoryItemAware
+
+ObjectCopier
+------------
+
+The copier is similar, but supports more cases: sometimes you can copy outside
+of an inventory, too.
+
+ >>> copier = zope.copypastemove.interfaces.IObjectCopier(container2['none'])
+ >>> copier.copyable()
+ True
+ >>> copier.copyableTo(container2)
+ True
+
+Well, you can't for objects that aren't the proxied version of the
+actual object in the inventory, actually.
+
+ >>> copier.copyableTo(app)
+ False
+ >>> copier.copyTo(app)
+ Traceback (most recent call last):
+ ...
+ ValueError
+
+But they can be copied within the same manifest...
+
+ >>> copier.copyTo(container2)
+ u'none-2'
+ >>> sorted(container2)
+ ['none', u'none-2']
+ >>> container2['none']['data']['d']
+ <Demo 0>
+ >>> container2['none-2']['data']['d']
+ <Demo 0>
+ >>> import zc.shortcut.proxy
+ >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+ ... zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']))
+ False
+
+...and across manifests.
+
+ >>> copier.copyTo(container)
+ 'none'
+ >>> container['none']['data']['d']
+ <Demo 0>
+ >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+ ... zc.shortcut.proxy.removeProxy(container['none']['data']['d']))
+ False
+
+If the copier is for an object that is the proxied object in the inventory,
+then it may in fact copy outside of a manifest.
+
+ >>> copier = zope.copypastemove.interfaces.IObjectCopier(
+ ... container['none']['data']['d'])
+ >>> copier.copyableTo(app)
+ True
+ >>> copier.copyTo(app)
+ u'Demo-3'
+ >>> (zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']) is
+ ... app[u'Demo-3'])
+ False
+
+ObjectLinker
+------------
+
+The last adapter is a linker, as defined in zc.shortcut. It puts the same
+object in inventories, or creates shortcuts out of inventories.
+
+ >>> import zc.shortcut.interfaces
+ >>> linker = zc.shortcut.interfaces.IObjectLinker(container2['none'])
+ >>> linker.linkable()
+ True
+ >>> linker.linkableTo(container2)
+ True
+
+Well, you can't create shortcuts for objects that aren't the proxied
+version of the actual object in the inventory, actually.
+
+ >>> linker.linkableTo(app)
+ False
+ >>> linker.linkTo(app)
+ Traceback (most recent call last):
+ ...
+ ValueError
+
+But they can be linked within the same manifest...
+
+ >>> linker.linkTo(container2)
+ u'none-3'
+ >>> sorted(container2)
+ ['none', u'none-2', u'none-3']
+ >>> container2['none']['data']['d']
+ <Demo 0>
+ >>> container2['none-3']['data']['d']
+ <Demo 0>
+ >>> import zc.shortcut.proxy
+ >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+ ... zc.shortcut.proxy.removeProxy(container2['none-3']['data']['d']))
+ True
+
+...and across manifests.
+
+ >>> linker.linkTo(container)
+ u'none-2'
+ >>> container['none-2']['data']['d']
+ <Demo 0>
+ >>> (zc.shortcut.proxy.removeProxy(container2['none']['data']['d']) is
+ ... zc.shortcut.proxy.removeProxy(container['none-2']['data']['d']))
+ True
+
+If the linker is for an object that is the proxied object in the inventory,
+then it may in fact link outside of a manifest.
+
+ >>> linker = zc.shortcut.interfaces.IObjectLinker(
+ ... container['none']['data']['d'])
+ >>> linker.linkableTo(app)
+ True
+ >>> linker.linkTo(app)
+ u'Demo-3-2'
+ >>> (zc.shortcut.proxy.removeProxy(container2['none-2']['data']['d']) is
+ ... app[u'Demo-3-2'].raw_target)
+ False
Property changes on: zc.vault/trunk/src/zc/vault/traversal.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.vault/trunk/src/zc/vault/traversal.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/traversal.zcml 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/traversal.zcml 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,31 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zope"
+ >
+
+ <adapter
+ factory=".traversal.ObjectCopier"
+ permission="zope.ManageContent"
+ trusted="y"
+ />
+
+ <adapter
+ factory=".traversal.ObjectMover"
+ permission="zope.ManageContent"
+ trusted="y"
+ />
+
+ <adapter
+ factory=".traversal.ObjectLinker"
+ permission="zope.ManageContent"
+ trusted="y"
+ />
+
+ <class class=".traversal.Proxy">
+ <require
+ permission="zope.Public"
+ interface=".traversal.IProxy"
+ />
+ </class>
+
+</configure>
\ No newline at end of file
Added: zc.vault/trunk/src/zc/vault/vault.py
===================================================================
--- zc.vault/trunk/src/zc/vault/vault.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/vault.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,646 @@
+
+import persistent # Not sure
+from zope import interface, component, event
+import zope.app.container.contained
+import zope.lifecycleevent
+import zope.proxy
+# import rwproperty
+from zc.copyversion import rwproperty
+import zc.copyversion.interfaces
+
+from zc.vault import interfaces, core
+
+def makeItem(rel, inventory):
+ if rel.token == inventory.vault.top_token:
+ return InventoryContents(inventory, relationship=rel)
+ else:
+ return InventoryItem(inventory, relationship=rel)
+
+class InventoryContents(object):
+ interface.implements(interfaces.IInventoryContents)
+
+ def __init__(self, inventory, relationship=None):
+ self.__parent__ = self.inventory = inventory
+ if relationship is not None:
+ if relationship.token != self.inventory.vault.top_token:
+ raise ValueError('contents must use top_token')
+ if not inventory.manifest.isOption(relationship):
+ raise ValueError('relationship is not in manifest')
+ self._relationship = relationship
+ else:
+ rel = inventory.manifest.get(self.inventory.vault.top_token)
+ if rel is None:
+ rel = core.Relationship(self.inventory.vault.top_token)
+ rel.__parent__ = inventory.manifest
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ inventory.manifest.addLocal(rel)
+ self._relationship = rel
+
+ def __getstate__(self):
+ raise RuntimeError('This should not be persisted.') # negotiable
+
+ def __eq__(self, other):
+ return (interfaces.IInventoryContents.providedBy(other) and
+ self.relationship is other.relationship and
+ self.inventory.manifest is other.inventory.manifest)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ @property
+ def relationship(self):
+ return self._relationship
+
+ def _getRelationshipFromKey(self, key, default=None):
+ token = self.relationship.containment.get(key)
+ if token is None:
+ return default
+ return self.inventory.manifest.get(token)
+
+ def __len__(self):
+ return len(self.relationship.containment)
+
+ def __getitem__(self, key):
+ rel = self._getRelationshipFromKey(key)
+ if rel is None:
+ raise KeyError(key)
+ return rel.object
+
+ def get(self, key, default=None):
+ rel = self._getRelationshipFromKey(key)
+ if rel is None:
+ return default
+ return rel.object
+
+ def items(self):
+ get = self.inventory.manifest.get
+ return [(k, get(t).object) for k, t in
+ self.relationship.containment.items()]
+
+ def keys(self):
+ return self.relationship.containment.keys()
+
+ def values(self):
+ get = self.inventory.manifest.get
+ return [get(t).object for t in self.relationship.containment.values()]
+
+ def __iter__(self):
+ return iter(self.relationship.containment)
+
+ def __contains__(self, key):
+ return key in self.relationship.containment
+
+ has_key = __contains__
+
+ def getKey(self, item, default=None):
+ if interfaces.IInventoryItem.providedBy(item):
+ item = item.relationship
+ return self.relationships.containment.getKey(item.token, default)
+
+ def makeMutable(self):
+ # local is mutable normally; modified is mutable while updating
+ if self.inventory.manifest._z_versioned:
+ raise zc.copyversion.interfaces.VersionedError
+ typ = self.type
+ if not self.inventory.manifest.updating:
+ if typ == interfaces.LOCAL:
+ return typ
+ elif self.has_local:
+ raise ValueError('Local revision already created')
+ elif typ == interfaces.MODIFIED:
+ return typ
+ selected = self.selected
+ rel = core.Relationship(
+ self.relationship.token, relationship=self.relationship,
+ source_manifest=self.inventory.manifest)
+ rel.__parent__ = self.inventory.manifest
+ event.notify(zope.lifecycleevent.ObjectCreatedEvent(rel))
+ if not self.inventory.manifest.updating:
+ self.inventory.manifest.addLocal(rel)
+ if selected:
+ self.inventory.manifest.select(rel)
+ res = interfaces.LOCAL
+ else:
+ self.inventory.manifest.addModified(rel)
+ if selected:
+ self.inventory.manifest.select(rel)
+ res = interfaces.MODIFIED
+ self._relationship = rel
+ return res
+
+ def __delitem__(self, key):
+ self.makeMutable()
+ old = self.relationship.containment[key]
+ del self.relationship.containment[key]
+
+ def __setitem__(self, key, value):
+ relset = self.inventory.manifest
+ token = self.relationship.containment.get(key)
+ if token is None:
+ self.makeMutable()
+ sub_rel = core.Relationship()
+ sub_rel.__parent__ = self.inventory.manifest
+ sub_rel.token = self.inventory.vault.intids.register(sub_rel)
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(sub_rel))
+ sub_rel.object = value
+ if self.inventory.updating:
+ relset.addModified(sub_rel)
+ else:
+ relset.addLocal(sub_rel)
+ self.relationship.containment[key] = sub_rel.token
+ else:
+ sub_rel = self.inventory.manifest.get(token)
+ assert sub_rel is not None
+ if relset.getType(sub_rel) not in (
+ interfaces.LOCAL, interfaces.SUGGESTED, interfaces.MODIFIED):
+ item = makeItem(sub_rel, self.inventory)
+ item.makeMutable()
+ sub_rel = item.relationship
+ sub_rel.object = value
+
+ def updateOrder(self, order):
+ self.makeMutable()
+ self.relationship.containment.updateOrder(order)
+
+ def updateOrderFromTokens(self, order):
+ self.makeMutable()
+ c = self.relationship.containment
+ c.updateOrder(c.getKey(t) for t in order)
+
+ @property
+ def token(self):
+ return self.relationship.token
+
+ def __call__(self, name, *args):
+ if args:
+ if len(args) > 1:
+ raise TypeError(
+ '__call__() takes at most 2 arguments (%d given)' %
+ len(args) + 1)
+ rel = self.relationship.containment.get(name)
+ if rel is None:
+ return args[0]
+ else:
+ rel = self.relationship.containment[name]
+ return InventoryItem(self.inventory, rel)
+
+ @property
+ def previous(self):
+ previous = self.inventory.manifest.previous
+ if previous is None:
+ return None
+ previous_relationship = previous.get(self.relationship.token)
+ if previous_relationship is None:
+ return None
+ return makeItem(previous_relationship, Inventory(previous))
+
+ @property
+ def next(self):
+ next = self.inventory.manifest.next
+ if next is None:
+ return None
+ next_relationship = next.get(self.relationship.token)
+ if next_relationship is None:
+ return None
+ return makeItem(next_relationship, Inventory(next))
+
+ @property
+ def previous_version(self):
+ rel = self.inventory.vault.getPrevious(self.relationship)
+ if rel is not None:
+ return makeItem(rel, Inventory(rel.__parent__))
+
+ @property
+ def next_version(self):
+ rel = self.inventory.vault.getNext(self.relationship)
+ if rel is not None:
+ return makeItem(rel, Inventory(rel.__parent__))
+
+ @property
+ def type(self):
+ return self.inventory.manifest.getType(self.relationship)
+
+ @property
+ def selected(self):
+ return self.inventory.manifest.isSelected(self.relationship)
+
+ def select(self):
+ self.inventory.manifest.select(self.relationship)
+
+ @property
+ def is_update_conflict(self):
+ return self.inventory.manifest.isUpdateConflict(
+ self.relationship.token)
+
+ def resolveUpdateConflict(self):
+ if not self.inventory.manifest.updating:
+ raise RuntimeError('can only resolve merges while updating')
+ if not self.is_update_conflict:
+ raise RuntimeError('Not a conflict')
+ self.select() # XXX is this good behavior?
+ self.inventory.manifest.resolveUpdateConflict(
+ self.relationship.token)
+
+ @property
+ def has_base(self):
+ return bool(self.inventory.manifest.getBase(
+ self.relationship.token))
+
+ @property
+ def has_local(self):
+ return bool(self.inventory.manifest.getLocal(
+ self.relationship.token))
+
+ @property
+ def has_updated(self):
+ return bool(self.inventory.manifest.getUpdated(
+ self.relationship.token))
+
+ @property
+ def has_suggested(self):
+ return bool(list(
+ self.inventory.manifest.iterSuggested(
+ self.relationship.token)))
+
+ @property
+ def has_modified(self):
+ return bool(list(
+ self.inventory.manifest.iterModified(
+ self.relationship.token)))
+
+ @property
+ def has_merged(self):
+ return bool(list(
+ self.inventory.manifest.iterMerged(
+ self.relationship.token)))
+
+ @property
+ def base_item(self):
+ rel = self.inventory.manifest.getBase(
+ self.relationship.token)
+ if rel is not None:
+ return makeItem(rel, self.inventory)
+
+ @property
+ def local_item(self):
+ rel = self.inventory.manifest.getLocal(
+ self.relationship.token)
+ if rel is not None:
+ return makeItem(rel, self.inventory)
+
+ @property
+ def updated_item(self):
+ rel = self.inventory.manifest.getUpdated(
+ self.relationship.token)
+ if rel is not None:
+ return makeItem(rel, self.inventory)
+
+ def iterSuggestedItems(self):
+ for rel in self.inventory.manifest.iterSuggested(
+ self.relationship.token):
+ yield makeItem(rel, self.inventory)
+
+ def iterModifiedItems(self):
+ for rel in self.inventory.manifest.iterModified(
+ self.relationship.token):
+ yield makeItem(rel, self.inventory)
+
+ def iterMergedItems(self):
+ for rel in self.inventory.manifest.iterMerged(
+ self.relationship.token):
+ yield makeItem(rel, self.inventory)
+
+ @property
+ def copy_source(self):
+ if self.relationship.copy_source is not None:
+ return makeItem(
+ self.relationship.copy_source[0],
+ Inventory(self.relationship.copy_source[1]))
+
+ @property
+ def selected_item(self):
+ if self.selected:
+ return self
+ return makeItem(
+ self.inventory.manifest.get(self.relationship.token),
+ self.inventory)
+
+class InventoryItem(InventoryContents):
+
+ interface.implements(interfaces.IInventoryItem)
+
+ def __init__(self, inventory, token=None, relationship=None):
+ if token is inventory.vault.top_token:
+ raise ValueError('Cannot create inventory item with top_token')
+ self.__parent__ = self.inventory = inventory
+ if relationship is None:
+ if token is None:
+ raise ValueError('must provide one of relationship or token')
+ relationship = inventory.manifest.get(token)
+ if relationship is None:
+ raise ValueError('token is not used in this inventory')
+ elif not inventory.manifest.isOption(relationship):
+ raise ValueError('relationship is not in inventory')
+ self._relationship = relationship
+
+ def resolveOrphanConflict(self):
+ self.inventory.manifest.resolveOrphanConflict(
+ self.relationship.token)
+
+ @property
+ def is_orphan(self):
+ return self.inventory.manifest.isOrphan(self.relationship.token)
+
+ @property
+ def is_orphan_conflict(self):
+ return self.inventory.manifest.isOrphanConflict(
+ self.relationship.token)
+
+ @property
+ def is_parent_conflict(self):
+ return len(list(
+ self.inventory.manifest.iterSelectedParents(
+ self.relationship.token))) > 1
+
+ @property
+ def parent(self):
+ res = self.inventory.manifest.getParent(self.relationship.token)
+ if res is not None:
+ return makeItem(res, self.inventory)
+
+ @property
+ def name(self):
+ res = self.inventory.manifest.getParent(self.relationship.token)
+ if res is not None:
+ return res.containment.getKey(self.relationship.token)
+
+ def iterSelectedParents(self):
+ return (makeItem(rel, self.inventory) for rel in
+ self.inventory.manifest.iterSelectedParents(
+ self.relationship.token))
+
+ def iterParents(self):
+ return (makeItem(rel, self.inventory) for rel in
+ self.inventory.manifest.iterParents(
+ self.relationship.token))
+
+ @property
+ def object(self):
+ return self.relationship.object
+ @rwproperty.setproperty
+ def object(self, value):
+ self.relationship.object = value # may raise VersionedError
+
+ def moveTo(self, location=None, name=None):
+ clean_location = zope.proxy.removeAllProxies(location)
+ if clean_location.inventory.manifest is not self.inventory.manifest:
+ self.copyTo(location, name)
+ if self.name:
+ del self.parent[self.name]
+ # go down & resolve all orphan conflicts: too much of a heuristic?
+ stack = [(lambda x: self, iter(('',)))]
+ while stack:
+ s, i = stack[-1]
+ try:
+ key = i.next()
+ except StopIteration:
+ stack.pop()
+ else:
+ val = s(key)
+ if val.is_orphan_conflict:
+ val.resolveOrphanConflict()
+ stack.append((val, iter(val)))
+ return
+ if location is None:
+ location = self.parent
+ if location is None:
+ raise ValueError('location must be supplied for orphans')
+ old_name = self.name
+ if name is None:
+ name = old_name
+ if name is None:
+ raise ValueError('Must specify name')
+ if name in location:
+ if zope.proxy.removeAllProxies(
+ location(name).relationship) is self.relationship:
+ return
+ raise ValueError(
+ 'Object with same name already exists in new location')
+ if (self.selected and location.selected and
+ self.inventory.manifest.isLinked(
+ self.relationship.token, location.relationship.token)):
+ # location is in self
+ raise ValueError('May not move item to within itself')
+ parent = self.parent
+ if old_name:
+ del self.parent[old_name]
+ if (parent is None or
+ clean_location.relationship is not parent.relationship):
+ location.makeMutable()
+ else:
+ location = parent
+ location.relationship.containment[name] = self.relationship.token
+ if location.selected and not self.selected:
+ self.select()
+
+ def copyTo(self, location, name=None):
+ if name is None:
+ name = self.name
+ if name in location:
+ raise ValueError(
+ 'Object with same name already exists in new location')
+ location.makeMutable()
+ # to get around error-checking constraints in the core, we go from
+ # bottom-to-top.
+ # this also prevents the possibility of infinite recursion in the copy.
+ stack = [
+ (location.relationship, iter((name,)), lambda x: self, {})]
+ if location.inventory.updating:
+ add = location.inventory.manifest.addModified
+ else:
+ add = location.inventory.manifest.addLocal
+ clean_location = zope.proxy.removeAllProxies(location)
+ while stack:
+ relationship, keys, src, queued = stack[-1]
+ try:
+ key = keys.next()
+ except StopIteration:
+ stack.pop()
+ for k, v in queued.items():
+ relationship.containment[k] = v
+ if (zope.proxy.removeAllProxies(relationship) is not
+ clean_location.relationship):
+ add(relationship)
+ else:
+ value = src(key)
+ rel = core.Relationship(
+ relationship=value.relationship,
+ source_manifest=value.inventory.manifest)
+ rel.__parent__ = clean_location.inventory.manifest
+ rel.token = location.inventory.vault.intids.register(rel)
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(rel))
+ queued[key] = rel.token
+ stack.append((rel, iter(value), value, {}))
+
+class Inventory(persistent.Persistent, zope.app.container.contained.Contained):
+
+ interface.implements(interfaces.IInventory)
+
+ def __init__(
+ self, manifest=None, inventory=None, vault=None, mutable=False):
+ if manifest is None:
+ if vault is None:
+ if inventory is None:
+ raise ValueError(
+ 'must provide manifest, inventory, or vault')
+ manifest = inventory.manifest
+ else: # vault exists
+ if inventory is None:
+ manifest = vault.manifest
+ if manifest is None:
+ manifest = core.Manifest(vault=vault)
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(
+ manifest))
+ else:
+ manifest = inventory.manifest
+ elif inventory is not None:
+ raise ValueError('cannot pass both manifest and inventory')
+ if mutable and manifest._z_versioned:
+ manifest = core.Manifest(manifest, vault)
+ event.notify(
+ zope.lifecycleevent.ObjectCreatedEvent(manifest))
+ self._manifest = manifest
+ self.__parent__ = manifest.vault
+
+ def __eq__(self, other):
+ return (interfaces.IInventory.providedBy(other) and
+ self.manifest is other.manifest)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ @property
+ def vault(self):
+ return self._manifest.vault
+ @rwproperty.setproperty
+ def vault(self, value):
+ self._manifest.vault = value
+
+ @property
+ def contents(self):
+ return InventoryContents(
+ self, self.manifest.get(self.vault.top_token))
+
+ @property
+ def manifest(self):
+ return self._manifest
+
+ def iterUpdateConflicts(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterUpdateConflicts())
+
+ def iterUpdateResolutions(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterUpdateResolutions())
+
+ def iterOrphanConflicts(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterOrphanConflicts())
+
+ def iterOrphanResolutions(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterOrphanResolutions())
+
+ def iterUnchangedOrphans(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterUnchangedOrphans())
+
+ def iterParentConflicts(self):
+ return (makeItem(r, self)
+ for r in self.manifest.iterParentConflicts())
+
+ def __iter__(self): # selected items
+ return (makeItem(r, self) for r in self.manifest)
+
+ @property
+ def updating(self):
+ return self.manifest.updating
+
+ @property
+ def merged_sources(self):
+ return tuple(Inventory(r) for r in self.manifest.merged_sources)
+
+ @property
+ def update_source(self):
+ res = self.manifest.update_source
+ if res is not None:
+ return Inventory(res)
+
+ def beginUpdate(self, source=None, previous=None):
+ if interfaces.IInventory.providedBy(source):
+ source = source.manifest
+ if interfaces.IInventory.providedBy(previous):
+ previous = previous.manifest
+ self.manifest.beginUpdate(source, previous)
+
+ def completeUpdate(self):
+ self.manifest.completeUpdate()
+
+ def abortUpdate(self):
+ self.manifest.abortUpdate()
+
+ def beginCollectionUpdate(self, items):
+ self.manifest.beginCollectionUpdate(
+ frozenset(i.relationship for i in items))
+
+ def iterChangedItems(self, source=None):
+ if interfaces.IInventory.providedBy(source):
+ source = source.manifest
+ return (makeItem(r, self)
+ for r in self.manifest.iterChanges(source))
+
+ def getItemFromToken(self, token, default=None):
+ rel = self.manifest.get(token)
+ if rel is None:
+ return default
+ return makeItem(rel, self)
+
+ @property
+ def previous(self):
+ p = self.manifest.previous
+ if p is not None:
+ return Inventory(p)
+ return None
+
+ @property
+ def next(self):
+ p = self.manifest.next
+ if p is not None:
+ return Inventory(p)
+ return None
+
+class Vault(core.Vault):
+ interface.implements(interfaces.IInventoryVault)
+
+ def getInventory(self, ix):
+ return Inventory(self[ix])
+
+ @property
+ def inventory(self):
+ if self._data:
+ return Inventory(self._data[self._data.maxKey()])
+ return None
+
+ def commit(self, value):
+ if interfaces.IInventory.providedBy(value):
+ value = value.manifest
+ super(Vault, self).commit(value)
+
+ def commitFrom(self, value):
+ if interfaces.IInventory.providedBy(value):
+ value = value.manifest
+ super(Vault, self).commitFrom(value)
Added: zc.vault/trunk/src/zc/vault/versions.py
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.py 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.py 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,174 @@
+import sys
+import persistent
+import zope.interface
+import zope.component
+import zope.location
+import zc.vault.vault
+import zope.publisher.browser
+import zope.publisher.interfaces
+import zope.publisher.interfaces.browser
+
+class IReadVersions(zope.interface.Interface):
+ """abstract: see IVersions"""
+
+ vault = zope.interface.Attribute(
+ """the vault that this collection of versions uses.""")
+
+ factory = zope.interface.Attribute(
+ """the (persistable) callable that gets an inventory and returns the
+ persistable wrapper object that has the desired API.""")
+
+ def __getitem__(ix):
+ """return version for given index, or raise KeyError
+ if no such index exists."""
+
+ def __len__():
+ """return number of versions"""
+
+class IWriteVersions(zope.interface.Interface):
+ """abstract: see IVersions"""
+
+ def commit(version):
+ """commit version"""
+
+ def commitFrom(version):
+ """commit from previous version"""
+
+ def create():
+ """create and return an editable version of the most recently
+ committed."""
+
+class IVersions(IReadVersions, IWriteVersions):
+ """a collection of versions"""
+
+class IWrapperAware(zope.interface.Interface):
+ """A manifest that has a wrapper attribute pointing to it's
+ desired wrapper"""
+
+ wrapper = zope.interface.Attribute(
+ """the desired wrapper""")
+
+
+class Versions(persistent.Persistent, zope.location.Location):
+ """Sequence of capability family versions.
+
+ Used to implement CapabilityFamily.versions
+ """
+
+ zope.interface.implements(IVersions)
+
+ def __init__(self, vault, factory, parent=None, name=None, initialize=None):
+ self.vault = vault
+ self.factory = factory
+ if vault.__parent__ is None:
+ zope.location.locate(self.vault, self, 'vault')
+ elif parent is None:
+ raise RuntimeError(
+ "programmer error: Locate the vault, or pass a parent in, "
+ "or both")
+ if parent is not None:
+ if name is not None:
+ zope.location.locate(self, parent, name)
+ else:
+ self.__parent__ = parent
+ for ix in range(len(vault)):
+ i = vault.getInventory(ix)
+ assert not IWrapperAware.providedBy(i.manifest), (
+ 'programmer error: manifests in vault have already been placed '
+ 'in a Versions container')
+ i.__parent__ = self
+ wrapper = self.factory(i)
+ i.manifest.wrapper = wrapper
+ zope.interface.directlyProvides(i.manifest, IWrapperAware)
+ if zope.location.interfaces.ILocation.providedBy(wrapper):
+ zope.location.locate(wrapper, self, str(i.manifest.vault_index))
+ if initialize is not None:
+ res = self.create()
+ initialize(res)
+ self.commit(res)
+
+ def __len__(self):
+ return len(self.vault)
+
+ def __getitem__(self, idx):
+ manifest = self.vault[idx]
+ return manifest.wrapper
+
+ def __iter__(self):
+ for m in self.vault:
+ yield m.wrapper
+
+ def commit(self, wrapper):
+ manifest = wrapper.inventory.manifest # XXX currently .inventory is
+ # undocumented, hard requirement of wrapper...
+ assert manifest.wrapper is wrapper, (
+ 'programmer error: manifest should have back reference to '
+ 'version')
+ self.vault.commit(manifest)
+ if zope.location.interfaces.ILocation.providedBy(wrapper):
+ zope.location.locate(wrapper, self, str(manifest.vault_index))
+
+ def commitFrom(self, wrapper):
+ manifest = wrapper.inventory.manifest
+ assert manifest.wrapper is wrapper, (
+ 'programmer error: manifest should have back reference to '
+ 'version')
+ self.vault.commitFrom(manifest)
+ i = self.vault.getInventory(-1)
+ wrapper = self.factory(i)
+ i.manifest.wrapper = wrapper
+ zope.interface.directlyProvides(i.manifest, IWrapperAware)
+ if zope.location.interfaces.ILocation.providedBy(wrapper):
+ zope.location.locate(wrapper, self, str(i.manifest.vault_index))
+
+ def create(self):
+ inventory = zc.vault.vault.Inventory(vault=self.vault, mutable=True)
+ inventory.__parent__ = self
+ res = self.factory(inventory)
+ inventory.manifest.wrapper = res
+ zope.interface.directlyProvides(
+ inventory.manifest, IWrapperAware)
+ res.__parent__ = self
+ return res
+
+
+class deferredProperty(object):
+ def __init__(self, name, initialize):
+ self.name = name
+ sys._getframe(1).f_locals[name] = self
+ self.initialize = initialize
+ def __get__(self, obj, typ=None):
+ if obj is not None:
+ self.initialize(obj)
+ return obj.__dict__[self.name]
+ return self
+
+
+class Traverser(zope.publisher.browser.BrowserView):
+ zope.component.adapts(
+ IVersions, zope.publisher.interfaces.browser.IBrowserRequest)
+ zope.interface.implements(
+ zope.publisher.interfaces.browser.IBrowserPublisher)
+
+ _default = 'index.html'
+
+ def browserDefault(self, request):
+ return self.context, (self._default, )
+
+ def publishTraverse(self, request, name):
+ try:
+ ix = int(name)
+ except ValueError:
+ pass
+ else:
+ try:
+ v = self.context[ix]
+ except IndexError:
+ name = self._default
+ else:
+ return v
+ view = zope.component.queryMultiAdapter(
+ (self.context, request), name=name)
+ if view is not None:
+ return view
+ raise zope.publisher.interfaces.NotFound(self.context, name, request)
Added: zc.vault/trunk/src/zc/vault/versions.txt
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.txt 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.txt 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,165 @@
+========
+Versions
+========
+
+A pattern for using the vault often involves wrapping it, so that clients
+are not aware of the vault or inventory API, but have a custom API more
+pertinent to the specific use case. One convenient way to follow this pattern
+is to have an object that acts as a container and factory for the
+wrappers. The `versions` module provides this object, and a traverser that
+can use it.
+
+To show it in use, we will need a class that uses a Versions object and a
+factory that creates the persistent versions. Here, we'll have a Project class
+that has ProjectVersion classes.
+
+ >>> import persistent
+ >>> import zc.vault.vault
+ >>> import zc.vault.versions
+ >>> import zc.vault.core
+ >>> import zope.location
+ >>> class ProjectVersion(persistent.Persistent, zope.location.Location):
+ ... def __init__(self, inventory):
+ ... self.inventory = inventory
+ ... zope.location.locate(self.inventory, self, 'inventory')
+ ... def __getitem__(self, key):
+ ... return self.inventory.contents('library')[key]
+ ... def __setitem__(self, key, value):
+ ... self.inventory.contents('library')[key] = value
+ ... def get(self, key, default=None):
+ ... try:
+ ... return self[key]
+ ... except KeyError:
+ ... return default
+ ...
+ >>> def initialize(obj):
+ ... assert obj.__dict__.get('held') is None, 'programmer error'
+ ... obj.held = zc.vault.core.HeldContainer()
+ ... zope.location.locate(obj.held, obj, 'held')
+ ... def _initialize(version):
+ ... inventory = version.inventory
+ ... if inventory.contents.get('library', inventory) is inventory:
+ ... # initialize inventory
+ ... inventory.contents['library'] = None
+ ... inventory.contents['data'] = None
+ ... obj.versions = zc.vault.versions.Versions(
+ ... zc.vault.vault.Vault(held=obj.held), ProjectVersion,
+ ... obj, 'versions', _initialize)
+ ... obj.active = obj.versions.create()
+ ... zope.location.locate(obj.active, obj, 'active')
+ ...
+ >>> class Project(persistent.Persistent, zope.location.Location):
+ ... zc.vault.versions.deferredProperty('active', initialize)
+ ... zc.vault.versions.deferredProperty('versions', initialize)
+ ... zc.vault.versions.deferredProperty('held', initialize)
+ ... def commit(self):
+ ... self.versions.commit(self.active)
+ ... self.active = self.versions.create()
+ ... zope.location.locate(self.active, self, 'active')
+ ...
+
+Now if we instantiate the Project (and put it in an application), we can
+examine the versions.
+
+ >>> p = app['project'] = Project()
+ >>> from zope.interface.verify import verifyObject
+ >>> verifyObject(zc.vault.versions.IVersions, p.versions)
+ True
+ >>> len(p.versions)
+ 1
+ >>> p.versions[0].get('foo') # None
+ >>> import zc.copyversion.versioning
+ >>> class Demo(persistent.Persistent, zc.copyversion.versioning.Versioning):
+ ... pass
+ >>> o = p.active['foo'] = Demo()
+ >>> p.commit()
+ >>> len(p.versions)
+ 2
+ >>> p.versions[1]['foo'] is o
+ True
+ >>> o2 = p.active['bar'] = Demo()
+
+Old versions that are ILocations have been placed within the versions object
+and given __name__ values that correspond to their indices.
+
+ >>> p.versions[0].__name__
+ '0'
+ >>> p.versions[0].__parent__ is p.versions
+ True
+ >>> p.versions[1].__name__
+ '1'
+ >>> p.versions[1].__parent__ is p.versions
+ True
+
+In addition to `commit`, the Versions object also provides `commitFrom`.
+This works the same as the vault `commitFrom` method, and is not explained
+further here (for now).
+
+ >>> new_o = p.active['foo'] = Demo()
+ >>> p.commit()
+ >>> p.versions[2]['foo'] is new_o
+ True
+ >>> p.versions.commitFrom(p.versions[1])
+ >>> p.versions[3]['foo'] is new_o
+ False
+ >>> p.versions[3]['foo'] is o
+ True
+ >>> p.versions[3].__name__
+ '3'
+ >>> p.versions[3].__parent__ is p.versions
+ True
+
+If you instantiate a Versions object with a vault that has pre-existing
+manifests--like one that comes from a branch--the pre-existing manifests will
+automatically get set up as if they had been part of the versions object.
+
+ >>> p.alt_versions = zc.vault.versions.Versions(
+ ... p.versions.vault.createBranch(), ProjectVersion, p, 'alt_versions')
+ >>> len(p.alt_versions)
+ 1
+ >>> p.alt_versions[0].get('foo') is o
+ True
+ >>> p.alt_versions[0].__parent__ is p.alt_versions
+ True
+ >>> p.alt_versions[0].__name__
+ '0'
+
+The module also includes a traverser.
+
+ >>> import zope.publisher.browser
+ >>> request = zope.publisher.browser.TestRequest()
+ >>> traverser = zc.vault.versions.Traverser(p.versions, request)
+ >>> traverser.browserDefault(request) # doctest: +ELLIPSIS
+ (<zc.vault.versions.Versions object at ...>, ('index.html',))
+ >>> traverser.publishTraverse(request, '1') is p.versions[1]
+ True
+ >>> traverser.publishTraverse(request, '0') is p.versions[0]
+ True
+
+If the traverser gets a non-integer, it tries to get a view, but fails with
+a NotFound error.
+
+ >>> traverser.publishTraverse(request, 'index.html')
+ ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ NotFound: Object: <zc.vault.versions.Versions object at ...>,
+ name: 'index.html'
+
+If the traverser gets an integer out-of-range, it tries for 'index.html' at
+the moment; this may need to be rethought.
+
+ >>> traverser.publishTraverse(request, '200')
+ ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ NotFound: Object: <zc.vault.versions.Versions object at ...>,
+ name: 'index.html'
+
+With this stuff, I usually like to make sure we can actually commit the
+transaction.
+
+ >>> import transaction
+ >>> transaction.commit()
+
+Yay.
\ No newline at end of file
Property changes on: zc.vault/trunk/src/zc/vault/versions.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.vault/trunk/src/zc/vault/versions.zcml
===================================================================
--- zc.vault/trunk/src/zc/vault/versions.zcml 2006-08-15 20:54:02 UTC (rev 69537)
+++ zc.vault/trunk/src/zc/vault/versions.zcml 2006-08-15 20:58:28 UTC (rev 69538)
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zc.vault"
+ >
+
+ <class class=".versions.Versions">
+ <factory id="zc.vault.versions.Versions" />
+ <require
+ permission="zope.View"
+ interface=".versions.IReadVersions"
+ />
+ <require
+ permission="zope.ManageContent"
+ interface=".versions.IWriteVersions"
+ />
+ </class>
+
+ <view
+ for=".versions.IVersions"
+ type="zope.publisher.interfaces.browser.IBrowserRequest"
+ provides="zope.publisher.interfaces.browser.IBrowserPublisher"
+ factory=".versions.Traverser"
+ permission="zope.Public"
+ allowed_interface="zope.publisher.interfaces.browser.IBrowserPublisher"
+ />
+
+</configure>
More information about the Checkins
mailing list