root / trunk / twisted / python / zippath.py

Revision 19305, 6.3 kB (checked in by glyph, 2 years ago)

Convert Twisted's plugin system to use twisted.python.modules for code discovery

This fixes several bugs in the traversal and caching logic of the plugins system, and simplifies it by factoring the code such that the representation of the Python path and import system is now delegated entirely to an API designed to do it. The other major bug fixed here made plugin modules from a development and system installation of Twisted conflict, even if one path entry clearly "won" from the perspective of sys.path, PYTHONPATH, __import__, et. al.

In addition to the main fix here, several new features were introduced into the twisted.python.filepath, twisted.python.zippath and twisted.python.modules modules.

  • twisted.python.filepath
    • New methods: getStatusChangeTime, getModificationTime, and getAccessTime, to access higher precision timestamps as floats.
    • New exception-handling behavior: FilePath.children will now raise UnlistableError, allowing users to catch non-fatal reasons why a directory might not be listable. More importantly this allows for portable error handling between UNIX and Windows.
    • FilePath instances are now usable as dictionary keys.
  • twisted.python.zippath
    • Acquired error-handling similar to FilePath
    • Acquired new methods for inspecting timestamps, both the old deprecated get(m|c|a)time form and the newer form. This should allow both older and newer code to work with ZipPath instances. The modification stamp is actually pulled from the zipfile metadata.
  • twisted.python.modules
    • Gratuitous Windows error-handling logic was removed and replaced with a simple except UnlistableError thanks to FilePath's new features.

Finally, test coverage was improved, and a few gratuitous pyflakes warnings and bits of trailing whitespace were eliminated.

Fixes #1951

Author: glyph

Reviewers: jerub, exarkun

Line 
1 # -*- test-case-name: twisted.test.test_paths.ZipFilePathTestCase -*-
2
3 """
4
5 This module contains partial re-implementations of FilePath, pending some
6 specification of formal interfaces it is a duck-typing attempt to emulate them
7 for certain restricted uses.
8
9 See the constructor for ZipArchive for use.
10
11 """
12
13 __metaclass__ = type
14
15 import os
16 import time
17 import errno
18
19 from twisted.python.zipstream import ChunkingZipFile
20
21 from twisted.python.filepath import FilePath, _PathHelper
22
23 # using FilePath here exclusively rather than os to make sure that we don't do
24 # anything OS-path-specific here.
25
26 ZIP_PATH_SEP = '/'              # In zipfiles, "/" is universally used as the
27                                 # path separator, regardless of platform.
28
29
30 class ZipPath(_PathHelper):
31     """
32     I represent a file or directory contained within a zip file.
33     """
34     def __init__(self, archive, pathInArchive):
35         """
36         Don't construct me directly.  Use ZipArchive.child().
37
38         @param archive: a ZipArchive instance.
39
40         @param pathInArchive: a ZIP_PATH_SEP-separated string.
41         """
42         self.archive = archive
43         self.pathInArchive = pathInArchive
44         # self.path pretends to be os-specific because that's the way the
45         # 'zipimport' module does it.
46         self.path = os.path.join(archive.zipfile.filename,
47                                  *(self.pathInArchive.split(ZIP_PATH_SEP)))
48
49     def __cmp__(self, other):
50         if not isinstance(other, ZipPath):
51             return NotImplemented
52         return cmp((self.archive, self.pathInArchive),
53                    (other.archive, other.pathInArchive))
54
55     def __repr__(self):
56         return 'ZipPath(%r)' % (self.path,)
57
58     def parent(self):
59         splitup = self.pathInArchive.split(ZIP_PATH_SEP)
60         if len(splitup) == 1:
61             return self.archive
62         return ZipPath(self.archive, ZIP_PATH_SEP.join(splitup[:-1]))
63
64     def child(self, path):
65         return ZipPath(self.archive, ZIP_PATH_SEP.join([self.pathInArchive, path]))
66
67     def sibling(self, path):
68         return self.parent().child(path)
69
70     # preauthChild = child
71
72     def exists(self):
73         return self.isdir() or self.isfile()
74
75     def isdir(self):
76         return self.pathInArchive in self.archive.childmap
77
78     def isfile(self):
79         return self.pathInArchive in self.archive.zipfile.NameToInfo
80
81     def islink(self):
82         return False
83
84     def listdir(self):
85         if self.exists():
86             if self.isdir():
87                 return self.archive.childmap[self.pathInArchive].keys()
88             else:
89                 raise OSError(errno.ENOTDIR, "Leaf zip entry listed")
90         else:
91             raise OSError(errno.ENOENT, "Non-existent zip entry listed")
92
93
94     def splitext(self):
95         """
96         Return a value similar to that returned by os.path.splitext.
97         """
98         # This happens to work out because of the fact that we use OS-specific
99         # path separators in the constructor to construct our fake 'path'
100         # attribute.
101         return os.path.splitext(self.path)
102
103
104     def basename(self):
105         return self.pathInArchive.split(ZIP_PATH_SEP)[-1]
106
107     def dirname(self):
108         # XXX NOTE: This API isn't a very good idea on filepath, but it's even
109         # less meaningful here.
110         return self.parent().path
111
112     def open(self):
113         return self.archive.zipfile.readfile(self.pathInArchive)
114
115     def restat(self):
116         pass
117
118
119     def getAccessTime(self):
120         """
121         Retrieve this file's last access-time.  This is the same as the last access
122         time for the archive.
123
124         @return: a number of seconds since the epoch
125         """
126         return self.archive.getAccessTime()
127
128
129     def getModificationTime(self):
130         """
131         Retrieve this file's last modification time.  This is the time of
132         modification recorded in the zipfile.
133
134         @return: a number of seconds since the epoch.
135         """
136         return time.mktime(
137             self.archive.zipfile.NameToInfo[self.pathInArchive].date_time
138             + (0, 0, 0))
139
140
141     def getStatusChangeTime(self):
142         """
143         Retrieve this file's last modification time.  This name is provided for
144         compatibility, and returns the same value as getmtime.
145
146         @return: a number of seconds since the epoch.
147         """
148         return self.getModificationTime()
149
150
151
152 class ZipArchive(ZipPath):
153     """ I am a FilePath-like object which can wrap a zip archive as if it were a
154     directory.
155     """
156     archive = property(lambda self: self)
157     def __init__(self, archivePathname):
158         """Create a ZipArchive, treating the archive at archivePathname as a zip file.
159
160         @param archivePathname: a str, naming a path in the filesystem.
161         """
162         self.zipfile = ChunkingZipFile(archivePathname)
163         self.path = archivePathname
164         self.pathInArchive = ''
165         # zipfile is already wasting O(N) memory on cached ZipInfo instances,
166         # so there's no sense in trying to do this lazily or intelligently
167         self.childmap = {}      # map parent: list of children
168
169         for name in self.zipfile.namelist():
170             name = name.split(ZIP_PATH_SEP)
171             for x in range(len(name)):
172                 child = name[-x]
173                 parent = ZIP_PATH_SEP.join(name[:-x])
174                 if parent not in self.childmap:
175                     self.childmap[parent] = {}
176                 self.childmap[parent][child] = 1
177             parent = ''
178
179     def child(self, path):
180         """
181         Create a ZipPath pointing at a path within the archive.
182
183         @param path: a str with no path separators in it, either '/' or the
184         system path separator, if it's different.
185         """
186         return ZipPath(self, path)
187
188     def exists(self):
189         """
190         Returns true if the underlying archive exists.
191         """
192         return FilePath(self.zipfile.filename).exists()
193
194
195     def getAccessTime(self):
196         """
197         Return the archive file's last access time.
198         """
199         return FilePath(self.zipfile.filename).getAccessTime()
200
201
202     def getModificationTime(self):
203         """
204         Return the archive file's modification time.
205         """
206         return FilePath(self.zipfile.filename).getModificationTime()
207
208
209     def getStatusChangeTime(self):
210         """
211         Return the archive file's status change time.
212         """
213         return FilePath(self.zipfile.filename).getStatusChangeTime()
214
215
Note: See TracBrowser for help on using the browser.