[Zope3-checkins] CVS: Zope3/src/zope/app/fssync - compare.py:1.1
Guido van Rossum
guido@python.org
Thu, 8 May 2003 15:50:51 -0400
Update of /cvs-repository/Zope3/src/zope/app/fssync
In directory cvs.zope.org:/tmp/cvs-serv29648
Added Files:
compare.py
Log Message:
Tools to compare parallel trees as written by toFS().
This is used to implement an "up-to-date" check such as used before
committing changes.
=== Added File Zope3/src/zope/app/fssync/compare.py ===
##############################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (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.
#
##############################################################################
"""Tools to compare parallel trees as written by toFS().
$Id: compare.py,v 1.1 2003/05/08 19:50:50 gvanrossum Exp $
"""
from __future__ import generators
import os
import filecmp
from os.path import exists, isfile, isdir, join, normcase
from zope.xmlpickle import loads
def checkUptodate(working, current):
"""Up-to-date check before committing changes.
Given a working tree containing the user's changes and Original
subtrees, and a current tree containing the current state of the
database (for the same object tree), decide whether all the
Original entries in the working tree match the entries in the
current tree. Return a list of error messages if something's
wrong, [] if everything is up-to-date.
"""
if not isdir(current):
return []
if not isdir(working):
return ["missing working directory %r" % working]
errors = []
for (left, right, common, lentries, rentries, ldirs, lnondirs,
rdirs, rnondirs) in treeComparisonWalker(working, current):
if rentries:
# Current has entries that working doesn't (the reverse
# means things added to working, which is fine)
for x in rentries:
errors.append("missing working entry for %r" % join(left, x))
for x in common:
nx = normcase(x)
if nx in rnondirs:
# Compare files (directories are compared by the walk)
lfile = join(left, "@@Zope", "Original", x)
rfile = join(right, x)
if not isfile(lfile):
errors.append("missing working original file %r" % lfile)
elif not filecmp.cmp(lfile, rfile, shallow=False):
errors.append("files %r and %r differ" % (lfile, rfile))
# Compare extra data (always)
lextra = join(left, "@@Zope", "Extra", x)
rextra = join(right, "@@Zope", "Extra", x)
errors.extend(checkUptodate(lextra, rextra))
# Compare annotations (always)
lann = join(left, "@@Zope", "Annotations", x)
rann = join(right, "@@Zope", "Annotations", x)
errors.extend(checkUptodate(lann, rann))
return errors
def treeComparisonWalker(left, right):
"""Generator that walks two parallel trees created by toFS().
Each item yielded is a tuple of 9 items:
left -- left directory path
right -- right directory path
common -- dict mapping common entry names to (left, right) entry dicts
lentries -- entry dicts unique to left
rentries -- entry dicts unique to right
ldirs -- names of subdirectories of left
lnondirs -- nondirectory names in left
rdirs -- names subdirectories of right
rnondirs -- nondirectory names in right
It's okay for the caller to modify the dicts to affect the rest of
the walk.
IOError exceptions may be raised.
"""
# XXX There may be problems on a case-insensitive filesystem when
# the Entries.xml file mentions two objects whose name only
# differs in case. Otherwise, case-insensitive filesystems are
# handled correctly.
queue = [(left, right)]
while queue:
left, right = queue.pop(0)
lentries = loadEntries(left)
rentries = loadEntries(right)
common = {}
for key in lentries.keys():
if key in rentries:
common[key] = lentries[key], rentries[key]
del lentries[key], rentries[key]
ldirs, lnondirs = classifyContents(left)
rdirs, rnondirs = classifyContents(right)
yield (left, right,
common, lentries, rentries,
ldirs, lnondirs, rdirs, rnondirs)
commonkeys = common.keys()
commonkeys.sort()
for x in commonkeys:
nx = normcase(x)
if nx in ldirs and nx in rdirs:
queue.append((ldirs[nx], rdirs[nx]))
# XXX Need to push @@Zope/Annotations/ and @@Zope/Extra/ as well.
nczope = normcase("@@Zope") # Constant used by classifyContents
def classifyContents(path):
"""Classify contents of a directory into directories and non-directories.
Return a pair of dicts, the first containing directory names, the
second containing names of non-directories. Each dict maps the
normcase'd version of the name to the path formed by concatenating
the path with the original name. '@@Zope' is excluded.
"""
dirs = {}
nondirs = {}
for name in os.listdir(path):
ncname = normcase(name)
if ncname == nczope:
continue
full = join(path, name)
if isdir(full):
dirs[ncname] = full
else:
nondirs[ncname] = full
return dirs, nondirs
def loadEntries(dir):
"""Return the Entries.xml file as a dict; default to {}."""
filename = join(dir, "@@Zope", "Entries.xml")
if exists(filename):
f = open(filename)
data = f.read()
f.close()
return loads(data)
else:
return {}