文章来自《Python cookbook》. 翻译仅仅是为了个人学习,其它商业版权纠纷与此无关!
-- 218.25.65.60 [DateTime(2004-09-30T20:35:59Z)]
描述,问题,讨论
Module: Versioned Backups
模块: 版本备份
Credit: Mitch Chapman
Before overwriting an existing file, it is often desirable to make a backup. Example 4-1 emulates the behavior of Emacs by saving versioned backups. It's also compatible with the marshal module, so you can use versioned output files for output in marshal format. If you find other file-writing modules that, like marshal, type-test rather than using file-like objects polymorphically, the class supplied here will stand you in good stead.
When Emacs saves a file foo.txt, it first checks to see if foo.txt already exists. If it does, the current file contents are backed up. Emacs can be configured to use versioned backup files, so, for example, foo.txt might be backed up to foo.txt.~1~. If other versioned backups of the file already exist, Emacs saves to the next available version. For example, if the largest existing version number is 19, Emacs will save the new version to foo.txt.~20~. Emacs can also prompt you to delete old versions of your files. For example, if you save a file that has six backups, Emacs can be configured to delete all but the three newest backups.
Example 4-1 emulates the versioning backup behavior of Emacs. It saves backups with version numbers (e.g., backing up foo.txt to foo.txt.~n~ when the largest existing backup number is n-1. It also lets you specify how many old versions of a file to save. A value that is less than zero means not to delete any old versions.
The marshal module lets you marshal an object to a file by way of the dump function, but dump insists that the file object you provide actually be a Python file object, rather than any arbitrary object that conforms to the file-object interface. The versioned output file shown in this recipe provides an asFile method for compatibility with marshal.dump. In many (but, alas, far from all) cases, you can use this approach to use wrapped objects when a module type-tests and thus needs the unwrapped object, solving (or at least ameliorating) the type-testing issue mentioned in Recipe 5.9. Note that Example 4-1 can be seen as one of many uses of the automatic-delegation idiom mentioned there.
The only true solution to the problem of modules using type tests rather than Python's smooth, seamless polymorphism is to change those errant modules, but this can be hard in the case of errant modules that you did not write (particularly ones in Python's standard library).
Example 4-1. Saving backups when writing files
1 """ This module provides versioned output files. When you write to such
2 a file, it saves a versioned backup of any existing file contents. """
3
4 import sys, os, glob, string, marshal
5
6 class VersionedOutputFile:
7 """ Like a file object opened for output, but with versioned backups
8 of anything it might otherwise overwrite """
9
10 def _ _init_ _(self, pathname, numSavedVersions=3):
11 """ Create a new output file. pathname is the name of the file to
12 [over]write. numSavedVersions tells how many of the most recent
13 versions of pathname to save. """
14 self._pathname = pathname
15 self._tmpPathname = "%s.~new~" % self._pathname
16 self._numSavedVersions = numSavedVersions
17 self._outf = open(self._tmpPathname, "wb")
18
19 def _ _del_ _(self):
20 self.close( )
21
22 def close(self):
23 if self._outf:
24 self._outf.close( )
25 self._replaceCurrentFile( )
26 self._outf = None
27
28 def asFile(self):
29 """ Return self's shadowed file object, since marshal is
30 pretty insistent on working with real file objects. """
31 return self._outf
32
33 def _ _getattr_ _(self, attr):
34 """ Delegate most operations to self's open file object. """
35 return getattr(self._outf, attr)
36
37 def _replaceCurrentFile(self):
38 """ Replace the current contents of self's named file. """
39 self._backupCurrentFile( )
40 os.rename(self._tmpPathname, self._pathname)
41
42 def _backupCurrentFile(self):
43 """ Save a numbered backup of self's named file. """
44 # If the file doesn't already exist, there's nothing to do
45 if os.path.isfile(self._pathname):
46 newName = self._versionedName(self._currentRevision( ) + 1)
47 os.rename(self._pathname, newName)
48
49 # Maybe get rid of old versions
50 if ((self._numSavedVersions is not None) and
51 (self._numSavedVersions > 0)):
52 self._deleteOldRevisions( )
53
54 def _versionedName(self, revision):
55 """ Get self's pathname with a revision number appended. """
56 return "%s.~%s~" % (self._pathname, revision)
57
58 def _currentRevision(self):
59 """ Get the revision number of self's largest existing backup. """
60 revisions = [0] + self._revisions( )
61 return max(revisions)
62
63 def _revisions(self):
64 """ Get the revision numbers of all of self's backups. """
65 revisions = []
66 backupNames = glob.glob("%s.~[0-9]*~" % (self._pathname))
67 for name in backupNames:
68 try:
69 revision = int(string.split(name, "~")[-2])
70 revisions.append(revision)
71 except ValueError:
72 # Some ~[0-9]*~ extensions may not be wholly numeric
73 pass
74 revisions.sort( )
75 return revisions
76
77 def _deleteOldRevisions(self):
78 """ Delete old versions of self's file, so that at most
79 self._numSavedVersions versions are retained. """
80 revisions = self._revisions( )
81 revisionsToDelete = revisions[:-self._numSavedVersions]
82 for revision in revisionsToDelete:
83 pathname = self._versionedName(revision)
84 if os.path.isfile(pathname):
85 os.remove(pathname)
86
87 def main( ):
88 """ mainline module (for isolation testing) """
89 basename = "TestFile.txt"
90 if os.path.exists(basename):
91 os.remove(basename)
92 for i in range(10):
93 outf = VersionedOutputFile(basename)
94 outf.write("This is version %s.\n" % i)
95 outf.close( )
96
97 # Now there should be just four versions of TestFile.txt:
98 expectedSuffixes = ["", ".~7~", ".~8~", ".~9~"]
99 expectedVersions = []
100 for suffix in expectedSuffixes:
101 expectedVersions.append("%s%s" % (basename, suffix))
102 expectedVersions.sort( )
103 matchingFiles = glob.glob("%s*" % basename)
104 matchingFiles.sort( )
105 for filename in matchingFiles:
106 if filename not in expectedVersions:
107 sys.stderr.write("Found unexpected file %s.\n" % filename)
108 else:
109 # Unit tests should clean up after themselves:
110 os.remove(filename)
111 expectedVersions.remove(filename)
112 if expectedVersions:
113 sys.stderr.write("Not found expected file")
114 for ev in expectedVersions:
115 sys.sdterr.write(' '+ev)
116 sys.stderr.write('\n')
117
118 # Finally, here's an example of how to use versioned
119 # output files in concert with marshal:
120 import marshal
121
122 outf = VersionedOutputFile("marshal.dat")
123 # Marshal out a sequence:
124 marshal.dump([1, 2, 3], outf.asFile( ))
125 outf.close( )
126 os.remove("marshal.dat")
127
128 if _ _name_ _ == "_ _main_ _":
129 main( )
For a more lightweight, simpler approach to file versioning, see Recipe 4.26.
参考 See Also
Recipe 4.26 and Recipe 5.9; documentation for the marshal module in the Library Reference.