低级版本支持
项目描述
zc.vault 包提供类似版本控制系统的低级版本支持,带有示例用法和几个示例附加组件。它与ZODB兼容。
详细文档
保险库
保险库模型版本化容器。保险库的单个版本通常被视为“库存”。库存实际操作的是本文件中提到的底层对象,称为清单。库存是主要的API。
库存模型容器,但它们并不是传统映射:包含关系位于库存中实际对象的外部。您必须查询库存以发现层次结构,而不是直接查询对象。例如,如果您将一个对象放入库存并希望将其视为一个版本化文件夹,您不需要在对象中放置子对象,而是在包装对象的库存节点中放置。这将在下面反复演示并深入探讨。
仓库仅包含版本化、冻结的清单,作为库存访问。可以从保险库中的任何库存创建工作库存。然后可以对它们进行修改,并将它们自己提交到保险库。提交库存会将其及其“包含”的所有对象冻结。
让我们来看一个例子。仓库存储清单,因此当您首次创建一个时,它是空的。仓库有一个基本的序列API,因此一个 len 将返回 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
最后一个库存(-1索引)是当前的库存。到这个库存的简写是库存属性。
>>> v.inventory # None
仓库和库存必须有一个数据库连接,才能存储其数据。我们假设我们有一个名为“app”的ZODB文件夹,我们可以将其信息存储在其中。当此文件作为测试运行时,会在tests.py中设置此。
>>> app['vault'] = v
创建初始工作库存只需要实例化它。通常,我们会传递一个版本化库存,作为新库存的基础,但如果没有,我们至少传递仓库。
>>> i = Inventory(vault=v) >>> verifyObject(interfaces.IInventory, i) True
技术上,我们所做的是创建一个清单——管理内容的核心API——并在其周围包装一个库存API。
>>> verifyObject(interfaces.IManifest, i.manifest) True
我们也可以显式创建清单。
>>> manifest = Manifest(vault=v) >>> verifyObject(interfaces.IManifest, manifest) True >>> i = Inventory(manifest) >>> verifyObject(interfaces.IInventory, i) True
库存(或者至少它们依赖的清单)在提交之前必须在数据库中的某个位置存储。它们提供 zope.app.location.interfaces.ILocation,以便它们可以在开发过程中存储在标准的Zope容器中。
>>> app['inventory'] = i
库存具有可以看似直接包含对象的内容。它们有一个映射API,并遵循 IInventoryContents 接口。
>>> 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'
(高级侧注:请随意忽略)
内容对象是一个API便利性,用于包装关系。关系将令牌与各种信息连接起来。所有库存内容的令牌(顶级节点)作为顶级_token属性存储在保险库中,较低级别则获得代表保险库中给定位置的唯一令牌。
内容和项目(见下文)基本上从关系及其包含它们的关联清单中获取所有数据。
>>> 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.
(高级侧注结束)
由于通常方便使用令牌作为特定对象的全球唯一标识符,所有库存项目都有一个“令牌”属性。
>>> i.contents.token 1234567
与在 zope.app.container 中定义的典型Zope 3包含不同,这种包含不会影响对象的 __parent__ 或 __name__。
存储在库存中的所有对象必须是 None,或者能够适配 zope.app.keyreference.interfaces.IKeyReference。在标准Zope 3中,这包括任何扩展 persistent.Persistent 的类的实例。
所有非None对象还必须能够适配 zc.freeze.interfaces.IFreezable。
在这里,我们创建一个对象,将其添加到应用程序中,并尝试将其添加到库存中。
>>> 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 freezable objects in vault, or None
此错误发生是因为提交库存必须冻结自身及其所有“包含”的对象,这样查看历史库存时就可以显示提交时的对象。这是一个简单的演示适配器用于Demo对象。我们还声明Demo是 IFreezable,这是一个重要的标记。
>>> import pytz >>> import datetime >>> from zope import interface, component, event >>> from zc.freeze.interfaces import ( ... IFreezing, ObjectFrozenEvent, IFreezable) >>> from zc.freeze import method >>> class DemoFreezingAdapter(object): ... interface.implements(IFreezing) ... component.adapts(Demo) ... def __init__(self, context): ... self.context = context ... @property ... def _z_frozen(self): ... return (getattr(self.context, '_z__freeze_timestamp', None) ... is not None) ... @property ... def _z_freeze_timestamp(self): ... return getattr(self.context, '_z__freeze_timestamp', None) ... @method ... def _z_freeze(self): ... self.context._z__freeze_timestamp = datetime.datetime.now( ... pytz.utc) ... event.notify(ObjectFrozenEvent(self)) ... >>> component.provideAdapter(DemoFreezingAdapter) >>> interface.classImplements(Demo, IFreezable)
顺便说一下,值得注意的是,清单对象提供了原生的 IFreezing,因此它们可以在不进行适配的情况下查询冻结状态和时间戳。当清单被冻结时,所有“包含”的对象也应被冻结。
现在还没有被冻结——我们的演示实例也没有被冻结。
>>> manifest._z_frozen False >>> IFreezing(app['d1'])._z_frozen False
由于演示实例现在是可冻结的,我们可以将对象添加到库存中。这意味着添加和删除对象。这里我们添加一个。
>>> 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
现在我们的有效层次结构看起来像这样
(top node) | 'mydemo' (<Demo u'd1'>)
我们将随着进程的进行更新这个层次结构。
添加一个对象会触发一个(仅针对该包的!)IObjectAdded事件。这个事件不是来自标准的生命周期事件包,因为那个包有不同的含义——例如,如前所述,将对象放入库存中不会设置__parent__或__name__(除非它还没有位置,在这种情况下,它将被放入一个可能临时的“手持”容器中,下面将讨论)。
>>> 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'
现在我们删除该对象。
>>> 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'
删除对象会触发一个特殊的IObjectRemoved事件(同样,不是来自生命周期事件)。
>>> 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'
除了映射API之外,库存内容还支持一个有序容器API,这与zope.app.container.ordered中的有序容器非常相似。内容的有序性意味着迭代基于对象添加的顺序,默认情况下(最早的首先);并且库存支持一个“updateOrder”方法。该方法接收容器中的名称的可迭代对象:新的顺序将是给定的顺序。如果给定的名称集合与当前的键集合有任何不同,则该方法将引发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'>]
现在我们的层次结构看起来像这样
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd1'>
重新排序容器会触发一个事件。
>>> interfaces.IOrderChanged.providedBy(events[-1]) True >>> events[-1].object is i.contents.relationship.containment True >>> events[-1].old_keys ('abe', 'barbara', 'donald')
在某些情况下,从一组令牌设置新顺序更容易。在这种情况下,“updateOrderFromTokens”方法很有用。
>>> 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']
就像“updateOrder”一样,也会触发一个事件。
>>> interfaces.IOrderChanged.providedBy(events[-1]) True >>> events[-1].object is i.contents.relationship.containment True >>> events[-1].old_keys ('barbara', 'abe', 'donald')
把它们放回原处也很容易,这样层次结构仍然与上一个示例结束时的样子一样。
>>> new_order = [getToken(k) for k in ('barbara', 'abe', 'donald')] >>> i.contents.updateOrderFromTokens(new_order) >>> list(i.contents.keys()) ['barbara', 'abe', 'donald']
如本文档的引言中所述,版本化层次结构保留在对象本身之外。这意味着不是容器本身的对象仍然可以是分支节点——在库存中的某种容器。实际上,直到出现合理的使用案例,作者不鼓励在保险库中作为分支节点使用真正的容器:两个维度的“容器式”行为太容易混淆。
为了获取可以作为库存中某个对象容器的对象,可以调用库存内容:“i.contents('abe')”。如果键存在,则返回IInventoryItem。默认情况下,它会对缺失的键引发KeyError,但可以接受一个默认值。
>>> 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扩展IInventoryContents以添加一个'object'属性,它表示它们所代表的对象。与IInventoryContents一样,映射接口允许用户操纵顶级以下的层次结构。例如,这里我们实际上将'cathy'演示对象放入'abe'演示对象的容器空间中。
>>> 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()) []
现在我们的层次结构看起来像这样
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> | | 'catherine' <Demo u'c1'>
值得注意的是,同一个对象可以在库存中的多个地方。这不会复制层次结构,也不会保持更改同步。如果需要,应该在使用保险库的代码中执行此策略;同样,如果保险库应该一次只包含一个对象,则应该在使用保险库的代码中强制执行此策略。
>>> 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'>
现在我们的层次结构看起来像这样
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> | | 'catherine' <Demo u'c1'> | | 'anna' <Demo u'a1'>
尽管a1包含c1包含a1,但这并不构成循环:层次结构与对象是分开的。
InventoryItems和InventoryContents目前是即时创建的,并且不持久化。它们应该使用“==”进行比较,而不是“is”。它们代表一个持久的核心数据对象,提供zc.vault.interfaces.IRelationship。IRelationship本身在本讨论的大部分中是隐藏的,仅在文档末尾介绍。但无论如何...
>>> 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
比较库存也会比较它们的库存内容
>>> i == None False >>> i == i True >>> i != i False
库存项目的另一个重要特征是,即使它们周围的对象发生变化,它们仍然保留正确的信息——例如,如果一个对象从层次结构的一部分移动到另一部分(见下面的moveTo),在移动之前生成的项目仍然会正确反映这一变化。
值得注意的是,多亏了zc.shortcut代码的神奇之处,视图可以为对象存在,并且从代理那里也可以访问InventoryItem的信息:这需要进一步阐述(待办)。
现在我们将尝试提交。
>>> v.commit(i) # doctest: +ELLIPSIS Traceback (most recent call last): ... ConflictError: <zc.vault.core.Manifest object at ...>
有冲突吗?我们不需要任何恶心的冲突!我们甚至没有合并!这从哪里来的?
默认仓库对跟踪冲突采取了非常严格的方法:例如,如果你在同一个库存中添加某物然后删除它,它将把这视为“孤儿冲突”:在这个库存中发生的将不会被提交的变化。你必须明确表示这些孤儿变化可以丢失。让我们看看这些孤儿。
>>> 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
啊,是的——你可以看到我们上面删除了这些对象:我们删除了“mydemo”(d1)和cathy(c1)。我们只需告诉库存,不包括它们是可以的。如果仓库客户端想要有更多的自动化,以便自动解决删除操作,那么他们有做到这一点的方法。在解决之后,iterOrphanConflicts将变为空,iterOrphanResolutions将包括对象。
>>> 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'>"]
现在当我们提交时,所有对象都将被版本化,我们将收到冻结和提交的事件。事件列表表示最近的事件;当此文档作为测试运行时,它通过监听所有事件并将它们附加到列表中来填充。
>>> v.commit(i) >>> interfaces.IManifestCommitted.providedBy(events[-1]) True >>> events[-1].object is manifest True >>> manifest.__parent__ is v True >>> IFreezing(app['a1'])._z_frozen True >>> IFreezing(app['b1'])._z_frozen True >>> IFreezing(app['c1'])._z_frozen True >>> IFreezing(app['d1'])._z_frozen True >>> manifest._z_frozen True >>> v.manifest is manifest True >>> len(v) 1
提交后,库存将强制执行冻结:不允许再进行任何更改。
>>> i.contents['foo'] = Demo() Traceback (most recent call last): ... FrozenError >>> i.contents.updateOrder(()) Traceback (most recent call last): ... FrozenError >>> i.contents('abe')('catherine')['foo'] = Demo() Traceback (most recent call last): ... FrozenError>>> v.manifest._z_frozen True
强制执行库存对象的冻结是其他代码或配置的责任,而不是仓库包。
现在清单有一个__name__,它是其索引的字符串。这非常有用,但有了正确的遍历器,仍然可能允许遍历保留容器中的项目。
>>> i.manifest.__name__ u'0'
每次提交后,仓库都应该能够确定每个关系的先前和下一个版本。由于这是第一次提交,previous将是None,但我们将检查它,现在就构建一个检查仓库最新清单的功能。
>>> 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)
创建一个新的工作库存需要一个基于旧清单的新清单。
无论好坏,该包提供了四种方法。我们可以通过指定一个仓库来创建一个新的工作库存,从中选择最新的清单,并设置“mutable=True”;
>>> i = Inventory(vault=v, mutable=True) >>> manifest = i.manifest >>> manifest._z_frozen False
通过指定一个库存,从中提取其清单,并设置“mutable=True”;
>>> i = Inventory(inventory=v.inventory, mutable=True) >>> manifest = i.manifest >>> manifest._z_frozen False
通过指定一个版本化的清单和“mutable=True”;
>>> i = Inventory(v.manifest, mutable=True) >>> manifest = i.manifest >>> manifest._z_frozen False
或通过指定一个可变的清单。
>>> i = Inventory(Manifest(v.manifest)) >>> i.manifest._z_frozen False
这些多种写法将在以后重新审查,并且可能有一个弃用期。最后的写法——将清单显式传递给库存——最有可能保持稳定,因为它清楚地允许为工作清单或版本化清单实例化库存包装器。
请注意,如上所述,库存只是清单的API包装器:因此,对共享清单的库存的更改将在它们之间共享。
>>> i_extra = Inventory(i.manifest) >>> manifest._z_frozen False
无论如何,我们现在有一个与原始内容相同的库存。
>>> 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
现在我们可以像操作旧库存那样操作新库存。
>>> app['d2'] = Demo() >>> i.contents['donald'] = app['d2'] >>> i.contents['donald'] is v.inventory.contents['donald'] False
现在我们的层次结构看起来像这样
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd2'> | | 'catherine' <Demo u'c1'> | | 'anna' <Demo u'a1'>
现在我们可以观察我们的本地更改。一种方法是检查iterChangedItems的结果。
>>> len(list(i.iterChangedItems())) 1 >>> iter(i.iterChangedItems()).next() == i.contents('donald') True
另一种方法是查看每个库存项。这些项指定了项目中的信息类型:它是否来自“基本”部分、“本地”更改,或者我们在检查合并时看到的少数几个其他选项。
>>> i.contents('abe').type 'base' >>> i.contents('donald').type 'local'
无论更改是否手动恢复到原始值,这都是正确的。
>>> i.contents['donald'] = app['d1'] >>> v.inventory.contents['donald'] is i.contents['donald'] True
然而,未更改的本地副本不包括在iterChangedItems结果中;在下面我们将看到,它们在提交时也会被丢弃。
>>> len(list(i.iterChangedItems())) 0
现在我们的层次结构看起来又这样了
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> | | 'catherine' <Demo u'c1'> | | 'anna' <Demo u'a1'>
每个库存项代表一个存储对象及其有效层次结构的数据集合。因此,更改其中任何一个(或两个)都会生成一个本地库存项。
>>> 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
这是两个更改:一个新节点(edna)和一个更改的节点(barbara获得了一个新的子节点)。
现在我们的层次结构看起来像这样(“*”表示一个更改的节点)
(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'>
修改顶级内容的集合意味着我们也有一项更改:尽管库存没有跟踪层次结构顶部的单个对象,但它确实跟踪了顶层的包含关系。
>>> i.contents.type 'base' >>> app['f1'] = Demo() >>> i.contents['fred'] = app['f1'] >>> i.contents.type 'local' >>> len(list(i.iterChangedItems())) 4
这是四个更改:edna、barbara、fred和顶级节点。
现在我们的层次结构看起来像这样(“*”表示一个更改或新的节点)
(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'>
您实际上可以从更改的项中检查基本内容——甚至可以切换回去。总是返回原始对象和包含关系的base_item
属性。返回具有本地更改的项,如果没有更改,则返回None。一个select
方法允许您通过默认值切换给定的项以查看一个或另一个。只读的selected
属性允许内省。
>>> 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
那是fred,顶级节点,/和/ edna:edna仍然是一个更改,尽管她不能使用旧的barbara版本访问。如果我们现在提交,我们必须解决上面的弃儿。
>>> 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'>]
让我们再四处看看,然后再切换回去
>>> 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']
库存有布尔值来检查是否存在基本项或本地项,这是一个便利(和优化机会)。
>>> 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
它还有四个其他类似属性,has_updated
、has_suggested
、has_modified
和has_merged
,我们将在后面进行考察。
在我们提交之前,我们将对库存进行一项更改。我们将对“anna”进行更改。注意我们在代码中如何拼写它:这是我们将放入库存的第一个对象,它还没有在应用程序中有一个位置。当一个库存被要求对没有ILocation的对象进行版本控制时,它将其存储在名为“held”的清单上的一个特殊文件夹中。保留对象使用标准的Zope 3名称选择器模式命名,即使已经进行版本控制,也可以将其移动出来。在这种情况下,我们需要为我们的演示对象注册一个名称选择器。我们将使用标准的。
>>> 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
现在我们的层次结构看起来像这样(“*”表示一个更改或新的节点)
(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 ...>
在我们的先前库存提交中,对象是在原地版本化的。库代码提供了一个钩子来生成提交到库的对象:它尝试将想要版本化的对象适配到zc.vault.interfaces.IVersionFactory。此接口指定任何可调用对象。让我们提供一个示例。
这里的策略是,如果对象在库存的保留容器中,则直接返回它,否则“制作一个副本”——在我们的演示中,这只是在新的实例上作为属性放置旧实例的名称。
>>> @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)
现在让我们提交,以显示结果。我们将丢弃对barbara的更改。
>>> len(list(i.iterChangedItems())) 5 >>> i.contents('barbara')('edna').resolveOrphanConflict() >>> i.contents('barbara').base_item.select() >>> len(list(i.iterChangedItems())) 4
即使edna已经解决,她也包括在内。
现在我们的层次结构看起来像这样(“*”表示一个更改或新的节点)
(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
我们提交了fred的添加,但没有提交edna的添加。一旦库存被提交,未选中的更改就会被丢弃。此外,如上所述,donald
的本地项数据已被丢弃,因为它没有包括任何更改。
>>> 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' >>> IFreezing(app['e1'])._z_frozen False
我们的变更与开始提交时的版本有所不同,这是由于版本工厂的原因。f1 未进行版本控制,因为我们已经制作了一个副本。
>>> IFreezing(app['f1'])._z_frozen 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' >>> IFreezing(new_changed['anna'].object)._z_frozen True >>> IFreezing(new_changed['fred'].object)._z_frozen True
现在,在保险库中有两个版本,我们可以引入库存、内容项的另外两个属性:next 和 previous。这些属性让您可以在保险库的历史中时间旅行。
我们还查看清单上的类似属性,以及保险库的 getInventory 方法。
例如,当前库存的 previous 属性指向原始库存,反之亦然。
>>> 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
对库存项也是如此。
>>> 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
一旦您移动到前一个或下一个项,从该项开始的进一步步骤将保留在前一个或下一个库存中。
>>> v.inventory.contents('abe')('catherine')['anna'].__name__ == 'a1' False >>> v.inventory.contents.previous('abe')('catherine')['anna'] <Demo u'a1'>
此外,库存项支持 previous_version 和 next_version。这些与 previous 和 next 的区别在于,*_version 变体会跳转到与当前项不同的项。例如,虽然 'anna' 的 previous_version 是旧的 'a1' 对象,就像 previous 值一样,但 'abe' 的 previous_version 是 None,因为它没有上一个版本。
>>> v.inventory.contents( ... 'abe')('catherine')('anna').previous_version.object <Demo u'a1'> >>> v.inventory.contents('abe').previous_version # None
这些利用了保险库上的 getPrevious 和 getNext 方法,这些方法与关系一起工作。
当令牌移动时,前一个和下一个工具甚至更有趣:您可以看到在层次结构中的位置变化。库存有一个 moveTo 方法,可以让库存跟随移动以维护历史。我们将创建一个新的库存副本并演示。在这个过程中,请注意,在移动之前获得的库存项正确反映了移动,如上所述。
>>> 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']
实际上,这个更改只影响移动的源和目标。
>>> 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
因此,现在我们的层次结构看起来像这样(“*”表示已更改的节点)
(top node) / / \ \ ---- / \ --------- / | | \ 'barbara' 'abe'* 'donald' 'fred'* <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> <Demo u'f1'> | | 'catherine' <Demo u'c1'> | | 'anna' <Demo ...>
如果您尝试将层次结构的一部分移动到具有相同名称的地方,除非您指定一个不冲突的名称,否则您将收到 ValueError。
>>> 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'>)]
现在我们的层次结构看起来像这样(“*”表示一个更改或新的节点)
(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 ...>
如果您尝试将层次结构的一部分移动到其内部,您也会收到 ValueError。
>>> i.contents('fred').moveTo(i.contents('fred')('catherine')('anna')) Traceback (most recent call last): ... ValueError: May not move item to within itself
这就是为什么内容不支持 moveTo 操作的原因。
>>> hasattr(i.contents, 'moveTo') False
如果您将对象移动到与其相同的文件夹中,这将是静默的空操作,除非您正在使用移动作为重命名操作,并且新名称冲突。
>>> 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'>)]
请注意,在上面的示例的最后部分,移动到文件夹内部也改变了顺序。
值得注意的是,尽管有了所有这些更改,我们只有两个额外的更改项:new_donald 的添加,以及内容的包含更改。例如,old_donald 并没有被考虑为更改;只有其容器更改了。
>>> 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']
现在我们已经移动了一些以前存在于库存中的对象——catherine(包含 anna)从 abe 移动到 fred,donald 从根内容移动到 abe 并重命名为 'old_donald'——我们可以检查前一个和 previous_version 指针。
>>> i.contents('abe')('old_donald').previous.parent == i.previous.contents True >>> i.contents('abe')('old_donald').previous_version # None
previous_version 为 None,因为在 iterChangedItems 示例中可以看到,donald 实际上并没有改变——只有其容器改变了。previous_version 适用于本地更改和早期库存中的更改。
>>> 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
anna 的 previous_version 是在初始库存中首次提交的第一个版本——它在这个版本中没有改变,但在最近提交的库存中改变,因此 previous_version 是第一个提交的。
顺便说一下,请注意,尽管 previous 和 previous_version 指向给定项来自的库存,但保险库中历史版本化的库存不会在 next 或 next_version 中指向此工作库存,因为此库存尚未提交。
>>> v.inventory.contents('abe').next # None >>> v.inventory.contents('abe').next_version # None
如上所述,只有库存项目支持moveTo
,而不是顶级节点库存内容。内容和库存项目都支持copyTo
方法。这与moveTo
类似,但它为相同对象在库存中创建新的附加位置;新的位置不保留任何历史记录。这主要是对“location1['foo'] = location2['foo']”的简写,用于库存的一部分中所有对象。唯一的区别是,在库存之间复制时,如以下所示。
copyTo
的基本机制与moveTo
非常相似。我们首先将catherine和anna复制到内容中。
>>> 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
现在我们的层次结构看起来像这样(“*”表示一个更改或新的节点)
(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 ...>
现在我们已经将对象从一个位置复制到另一个位置。这些副本与原始副本不同,因为它们没有任何历史记录。
>>> i.contents('fred')('catherine')('anna').previous is None False >>> i.contents('catherine')('anna').previous is None True
然而,它们知道它们的副本来源。
>>> (i.contents('catherine')('anna').copy_source == ... i.contents('fred')('catherine')('anna')) True
与moveTo
一样,你不能覆盖一个名称,但你可以明确提供。
>>> 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']
与moveTo
不同,如果你尝试将层次结构的一部分复制到其本身之上(相同位置,相同名称),库存将引发错误。
>>> i.contents('catherine')('anna').copyTo(i.contents('catherine')) Traceback (most recent call last): ... ValueError: Object with same name already exists in new location
实际上,你可以将copyTo
复制到完全不同的库存中的位置,甚至从不同的保险库。
>>> 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'>
我们有一段时间没有提交了,所以让我们提交这个第三版。我们做了很多删除,所以我们可以接受所有的孤立冲突。
>>> for item in i.iterOrphanConflicts(): ... item.resolveOrphanConflict() ... >>> v.commit(i) >>> checkManifest(v.manifest)
在zc.vault包的将来版本中,可能可以在库存之间移动和复制。在撰写本文时,这种情况是不必要的,并且这样做将具有未指定的行为。
我们已经讨论了用于基本用途的保险库系统的核心API。然而,还有许多其他用例很重要
恢复到较旧的库存;
合并并发更改;
跟踪保险库中的对象;
使用URL或TALES路径遍历保险库。
恢复到较旧的库存相对简单:使用'commitFrom'方法复制并提交较旧版本到新的副本。这同样适用于清单。
>>> v.commitFrom(v[0])
数据现在与旧版本一样。
>>> list(v.inventory.contents.keys()) ['barbara', 'abe', 'donald']
现在我们的层次结构看起来又这样了
(top node) / | \ / | \ 'barbara' 'abe' 'donald' <Demo u'b1'> <Demo u'a1'> <Demo u'd1'> | | 'catherine' <Demo u'c1'> | | 'anna' <Demo u'a1'>
commitFrom
方法将从一个与共享同一intids实用程序的保险库中提取任何已提交的清单。它创建一个新的清单,该清单复制了提供的清单。
>>> 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
请注意,这种方法将导致错误
>>> v.commit(Manifest(v[0])) # doctest: +ELLIPSIS Traceback (most recent call last): ... OutOfDateError: <zc.vault.core.Manifest object at ...>
再次,使用commitFrom
来恢复。
现在我们来到最复杂的保险库用例:对保险库的并发更改,合并库存。保险库设计支持这些用例的许多功能。
基本合并故事是,如果一个或多个提交发生在保险库上,而保险库的库存正在被处理,以至于工作库存的基不再是最新的已提交库存,因此无法正常提交...
>>> 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 ...>
...然后可以更新库存;如果没有问题,则可以提交库存。
short_running和保险库的头部现在看起来像这样("*"表示与前一个版本的变化)
(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看起来像这样
(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'>
内容节点已更改,并添加了“edna”。
默认情况下,更新是针对库存基保险库的当前库存。
这是更新。它不会产生冲突,因为节点更改没有重叠(请参阅上面的图)。
>>> long_running.beginUpdate() >>> long_running.updating True
合并后,long_running看起来像这样(“M”表示合并节点)
(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'>
(高级)
在更新期间,本地关系可能不会更改,即使它们没有版本。
>>> 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_frozen 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')
当你更改一个项目或内容时,这会被切换到MODIFIED关系所隐藏,如下所示。
(高级结束)
现在我们已经更新了,我们的库存中的 update_source 显示了用于更新的库存。
>>> long_running.manifest.base_source is v[-3] True >>> long_running.manifest.update_source is short_running.manifest True
更新应该反映哪些变化?iterChangedItems 接受一个可选参数,可以使用替代基准来计算变化,因此我们可以使用它和 long_running.base 来查看有效的合并。
>>> 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']
我们的内容显示了这些合并结果。
>>> 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'
在 abortUpdate 或 completeUpdate 之前,您无法更新到另一个库存,正如我们在下面讨论的那样。
>>> long_running.beginUpdate(v[-2]) Traceback (most recent call last): ... UpdateError: cannot begin another update while updating
我们将展示 abortUpdate,然后重新进行更新。abortUpdate 的一个特点是它应该撤销您在更新过程中所做的所有更改。例如,我们将选择内容的另一个版本,甚至添加一个项目。当我们中止时,所有更改都将消失。
>>> 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'
现在我们将更详细地观察事情的状态。我们可以使用 iterChangedItems 获取所有更改和更新的列表。如示例中所示,库存中的 update_source 显示了用于更新的库存。
>>> 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'>
当库存处于更新过程中时,才会生效的 has_updated 和 updated_item 属性,让您可以从更局部角度检查更改。
>>> 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
有三种可能阻止合并提交后的问题:项目冲突、孤儿和父冲突。项目冲突是指与本地更改冲突的项目更新,系统无法合并(关于这一点将在下面详细说明)。孤儿是指被接受的项目更改(本地或更新)无法从顶级内容访问,因此将丢失。父冲突是指被移动到源中一个位置和本地更改中另一个位置的项目,因此现在有两个父项:这是一个非法状态,因为它使得未来的合并和合理的历分析变得困难。
这三种问题可以通过 iterUpdateConflicts、iterOrphanConflicts 和 iterParentConflicts 分别分析。我们已经看到了 iterOrphanConflicts。在我们当前的合并中,我们没有任何这些问题,并且可以成功提交(或 completeUpdate)。
>>> list(long_running.iterUpdateConflicts()) [] >>> list(long_running.iterOrphanConflicts()) [] >>> list(long_running.iterParentConflicts()) [] >>> v.commit(long_running) >>> checkManifest(v.manifest)
在这里有很多重要的讨论,所以为了回顾,在简单情况下,我们只需要做这个
long_running.beginUpdate() v.commit(long_running)
我们可能拒绝了某些更新和本地更改,这可能会使事情更有趣;这两个步骤可以让您分析更新更改,并根据需要调整它们。但最简单的情况允许简单的拼写。
现在让我们探索可能的合并问题。第一个,也是可能最复杂的是项目冲突。项目冲突很容易引发。我们可以通过操作项目的包含或对象来实现。在这里,我们将操作根的包含顺序。
>>> 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']
如您所见,我们有工具来查找冲突并检查它们。要解决这个冲突,我们只需要使用 resolveUpdateConflict 方法。我们可以在标记为解决之前选择我们想要的,甚至创建一个新的并修改它。
让我们创建一个新的。您只需要开始更改项目,就会创建一个新的。在更新时,不允许您直接修改本地更改,这样系统就可以恢复它们;但您可能创建“修改”版本(如果更新被中止,这些版本将被丢弃)。
>>> 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']
现在我们来解决它。
>>> 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
如果我们调用 abortUpdate,local_item 的外观将与更新之前相同,因为我们修改了另一个对象。不过,让我们提交。
>>> v.commit(long_running) >>> checkManifest(v.manifest)
我们的层次结构现在看起来是这样的
(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'>
保险库代码允许适配器尝试并建议合并。例如,一个简单的合并可能有这样的策略:一个版本有对象更改,另一个版本有包含更改,可以简单地合并。这使用了一些我们还没有讨论的API:如果这个目录中有core.txt,你就幸运了;否则,希望从interfaces.py中得到帮助,并打扰Gary获取文档(抱歉)。
>>> 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)
现在,如果我们合并这个策略可以处理的更改,我们将有平滑的更新。
>>> 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)
这意味着我们自动合并了这个……
(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'>
……与这个(这通常会产生与‘catherine’节点的冲突)……
(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'>
……生成这个
(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'>
这结束了我们对项目冲突的巡礼。我们剩下的是孤儿和父级冲突。
如上所述,孤儿是被接受、已更改的项目,通常是来自更新或本地更改,它们从库存的根不可访问。例如,考虑以下情况。
>>> 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'
在图中再次重申,短运行库存删除了‘catherine’分支
(top node) ----------/ / \ \---------- / / \ \ 'abe' 'donald' 'barbara' 'edna' <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'> | | 'fred' <Demo u'f1'>
然而,长时间运行的分支更改了一个已删除的对象(‘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'>
所以,有了孤儿,你可以发现允许更改发生的节点旧版本,从而隐藏孤儿的变化。
要解决孤儿,如前所述,你可以使用resolveOrphanConflict,或者以某种方式更改树,使孤儿再次位于树内(使用moveTo)。我们只需解决它即可。请注意,解决方法保持选中状态:它只是停止了抱怨。
>>> orphan.selected True >>> orphan.resolveOrphanConflict() >>> orphan.selected True >>> len(list(long_running.iterOrphanConflicts())) 0 >>> v.commit(long_running) >>> checkManifest(v.manifest)
如果更改是由于反转发生的,也会发生同样的情况——长时间运行的库存执行删除。
这也可以发生在用户显式选择一个消除已接受更改的选择上,即使在没有合并的情况下,如我们所见。
父级冲突是最后一种冲突。
我们的层次结构现在看起来像这样
(top node) ----------/ / \ \---------- / / \ \ 'abe' 'donald' 'barbara' 'edna' <Demo u'a1'> <Demo u'd2'> <Demo u'b1'> <Demo u'e1'> | | 'fred' <Demo u'f1'>
短运行版本将被更改成这样
(top node) ------/ | \------- / | \ 'abe' 'barbara'* 'edna' <Demo u'a1'> <Demo u'b1'> <Demo u'e1'> / \ / \ 'fred' 'donald' <Demo u'f1'> <Demo u'd2'>
长时间运行的版本将看起来像这样。
(top node) ------/ | \------- / | \ 'abe' 'barbara' 'edna' <Demo u'a1'> <Demo u'b1'> <Demo u'e1'> | | 'fred'* <Demo u'f1'> | | 'donald' <Demo u'd2'>
合并后树看起来像这样
(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'>
问题是Donald。它是两个或更多地方的一个标记:一个父级冲突。
>>> 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']
你只需通过接受一个先前的版本,在合并之外,就可以引起这些冲突。例如,我们现在可以通过选择根节点来创建一个三方父级冲突。
>>> all['base'].select() >>> selected = list(conflict.iterSelectedParents()) >>> len(selected) 3
现在,如果我们通过拒绝本地更改来解决原始问题,我们仍然会存在问题,因为我们接受了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
现在我们的层次结构再次看起来像是短运行。
(top node) ------/ | \------- / | \ 'abe' 'barbara' 'edna' <Demo u'a1'> <Demo u'b1'> <Demo u'e1'> / \ / \ 'fred' 'donald' <Demo u'f1'> <Demo u'd2'>
我们无法提交这个,因为在这次提交和上一次提交之间没有有效的更改。
>>> v.commit(long_running) # doctest: +ELLIPSIS Traceback (most recent call last): ... NoChangesError: <zc.vault.core.Manifest object at ...>
所以,实际上,我们将恢复本地更改,拒绝短运行更改(在barbara中的放置),然后提交。
>>> all['local'].select() >>> all['updated'].base_item.select() >>> v.commit(long_running) >>> checkManifest(v.manifest)
请注意,尽管我们选择了base_item,但完成更新生成的关联实际上是本地的,因为它是从先前更新的源的变化。
>>> v.inventory.contents('barbara').type 'local'
实际上还有第四种错误:在选择的关系中存在子节点,而对于这些子节点没有选择的关系。代码试图禁止这种情况,所以应该不会遇到。
接下来,我们将讨论如何使用保险库创建和管理分支。这个简单的基本方法是,你可以基于一个保险库提交一个库存到一个新保险库,然后你可以在这两个保险库之间更新。要创建一个可以有合并清单的保险库,你必须共享内部的‘intids’属性。创建分支方法是为了这样做,并且默认情况下将当前保险库的最新清单作为分支的第一个版本提交。
>>> 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)
现在我们想要合并主线更改和分支。
>>> 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'
平滑更新。但与此同时,如果有人更改了分支,在提交之前,会发生什么呢?我们使用completeUpdate,然后在分支上再次更新。completeUpdate将所有选定的更改移动到本地,无论来源如何,就像提交一样(事实上,提交使用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)
完成这项操作后,分支的头部基于原始保险库的头部,因此我们可以立即在主库存中检查分支库存。
>>> v.commit(Inventory(Manifest(branch.manifest))) >>> checkManifest(v.manifest)
最后,还可以进行 cherry-picking 变更,尽管这可能会导致常规更新出现混淆。《beginCollectionUpdate》接受一个项目可迭代的项(如 iterChangedItems 生成的),并使用我们上面看到的常规冲突和检查方法应用更新。《completeUpdate》然后可以接受更改以进行附加更新。
>>> 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)
如果尝试添加一组导致清单具有不映射到值的键的关系,代码将阻止你——或者更精确地说,没有匹配选定关系的子令牌。例如,考虑以下。
>>> 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...
这是不允许的,因为 katrina 节点包含 'loyola' 节点,但我们没有包含匹配的 'loyola' 项目。
如果包括两者,合并将按常规进行。
>>> 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'>
《beginCollectionUpdate》和《iterChangedItems》的组合可以提供一种应用任意更改集到修订版本的有力方式。
存储None
有时你可能只想为了组织目的创建一个空节点。虽然通常存储的对象必须是可版本化和适应 IKeyReference 的,但 None 是一个特殊情况。我们可以在任何节点中存储 None。让我们快速举一个例子。
>>> 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
特殊的“持有”容器
有时指定一个“保留”容器用于存储在保险库中存储的所有对象,可以覆盖上述所述的每个清单的“保留”容器。可以通过指定保留容器来实例化保险库。
>>> 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
如果你创建一个分支,默认情况下它将使用相同的保留容器。
>>> 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
你还可以在创建分支时指定另一个保留容器。
>>> 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
提交事务
我们将确保所有这些更改实际上都可以提交到 ZODB。
>>> import transaction >>> transaction.commit()
CHANGES
0.11 (2011-04-08)
使用 eggs 而不是 zope3 checkout。
使用 Python 的《doctest》模块而不是已废弃的《zope.testing.doctest》。
将测试更新为使用 ZTK 1.0 运行。
0.10 (2008-03-04)
添加对《rwproperty》的依赖,而不是使用它的副本。
添加《zc.vault.versions.Traversable》。
修复 r <= 78553 中冻结后重命名的问题。
0.9 (2006-12-03)
初始 egg 版本。
项目详情
《zc.vault-0.11.tar.gz》的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | b7b6334cee979a54b3532a93884aa33704c8989b45a7708b9059349acc7c41b2 |
|
MD5 | 20fc3bbba33ed2a2976735870422ff7c |
|
BLAKE2b-256 | be639719a6732cbc628381f80667a91866435d4d1c1d86965b97ece3ff668ddf |