|
1 """ |
|
2 Filesystem path handling |
|
3 """ |
|
4 |
|
5 import os, os.path |
|
6 |
|
7 class Node (object) : |
|
8 """ |
|
9 A filesystem object is basically just complicated representation of a path. |
|
10 |
|
11 On the plus side, it has a parent node and can handle unicode/binary paths. |
|
12 """ |
|
13 |
|
14 # the binary name |
|
15 fsname = None |
|
16 |
|
17 # the unicode name |
|
18 name = None |
|
19 |
|
20 def decode_fsname (self, fsname) : |
|
21 """ |
|
22 Decode the given raw byte string representing a filesystem name into an user-readable unicode name. |
|
23 |
|
24 XXX: currently just hardcoded as utf-8 |
|
25 """ |
|
26 |
|
27 return fsname.decode('utf-8', 'replace') |
|
28 |
|
29 def encode_name (self, name) : |
|
30 """ |
|
31 Returns a suitable fsname for the given unicode name or strict ASCII str |
|
32 |
|
33 XXX: currently just hardcoded as utf-8 |
|
34 """ |
|
35 |
|
36 # this should fail for non-ASCII str |
|
37 return name.encode('utf-8') |
|
38 |
|
39 @classmethod |
|
40 def from_node (cls, node) : |
|
41 """ |
|
42 Construct from a Node object |
|
43 """ |
|
44 |
|
45 return cls(node.parent, node.fsname, node.name, node.config) |
|
46 |
|
47 def __init__ (self, parent, fsname=None, name=None, config=None) : |
|
48 """ |
|
49 Initialize the node with a parent and both name/fsname. |
|
50 |
|
51 If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name. |
|
52 |
|
53 If parent is given, but both fsname and name are None, then this node will be equal to the parent. |
|
54 """ |
|
55 |
|
56 assert not fsname or isinstance(fsname, str) |
|
57 |
|
58 if parent and not fsname and not name : |
|
59 # no name given -> we're the same as parent |
|
60 self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name |
|
61 |
|
62 else : |
|
63 # store |
|
64 self.parent = parent |
|
65 |
|
66 # config, either as given, or copy from parent |
|
67 if config : |
|
68 self.config = config |
|
69 |
|
70 else : |
|
71 self.config = parent.config |
|
72 |
|
73 # fsname |
|
74 if fsname : |
|
75 self.fsname = fsname |
|
76 |
|
77 else : |
|
78 self.fsname = self.encode_name(name) |
|
79 |
|
80 # name |
|
81 if name : |
|
82 self.name = name |
|
83 |
|
84 else : |
|
85 self.name = self.decode_fsname(fsname) |
|
86 |
|
87 def subnode (self, name) : |
|
88 """ |
|
89 Returns a Node object representing the given name behind this node. |
|
90 |
|
91 The name should either be a plain ASCII string or unicode object. |
|
92 """ |
|
93 |
|
94 return Node(self, name=name) |
|
95 |
|
96 @property |
|
97 def path (self) : |
|
98 """ |
|
99 Build and return the real filesystem path for this node |
|
100 """ |
|
101 |
|
102 # build using parent path and our fsname |
|
103 return os.path.join(self.parent.path, self.fsname) |
|
104 |
|
105 @property |
|
106 def unicodepath (self) : |
|
107 """ |
|
108 Build and return the fake unicode filesystem path for this node |
|
109 """ |
|
110 |
|
111 # build using parent unicodepath and our name |
|
112 return os.path.join(self.parent.path, self.name) |
|
113 |
|
114 def exists (self) : |
|
115 """ |
|
116 Tests if this node exists on the physical filesystem |
|
117 """ |
|
118 |
|
119 return os.path.exists(self.path) |
|
120 |
|
121 def is_dir (self) : |
|
122 """ |
|
123 Tests if this node represents a directory on the filesystem |
|
124 """ |
|
125 |
|
126 return os.path.isdir(self.path) |
|
127 |
|
128 def is_file (self) : |
|
129 """ |
|
130 Tests if this node represents a normal file on the filesystem |
|
131 """ |
|
132 |
|
133 return os.path.isfile(self.path) |
|
134 |
|
135 def __str__ (self) : |
|
136 """ |
|
137 Returns the raw filesystem path |
|
138 """ |
|
139 |
|
140 return self.path |
|
141 |
|
142 def __unicode__ (self) : |
|
143 """ |
|
144 Returns the human-readable path |
|
145 """ |
|
146 |
|
147 return self.unicodepath |
|
148 |
|
149 def __repr__ (self) : |
|
150 """ |
|
151 Returns a str representing this dir |
|
152 """ |
|
153 |
|
154 return "Node(%r, %r)" % (self.parent.path, self.fsname) |
|
155 |
|
156 def __cmp__ (self) : |
|
157 """ |
|
158 Comparisons between Nodes |
|
159 """ |
|
160 |
|
161 return cmp((self.parent, self.name), (other.parent, other.name)) |
|
162 |
|
163 class File (Node) : |
|
164 """ |
|
165 A file. Simple, eh? |
|
166 """ |
|
167 |
|
168 @property |
|
169 def basename (self) : |
|
170 """ |
|
171 Returns the "base" part of this file's name, i.e. the filename without the extension |
|
172 """ |
|
173 |
|
174 basename, _ = os.path.splitext(self.name) |
|
175 |
|
176 return basename |
|
177 |
|
178 @property |
|
179 def fileext (self) : |
|
180 """ |
|
181 Returns the file extension part of the file's name, without any leading dot |
|
182 """ |
|
183 |
|
184 _, fileext = os.path.splitext(self.name) |
|
185 |
|
186 return fileext.rstrip('.') |
|
187 |
|
188 def matchext (self, ext_list) : |
|
189 """ |
|
190 Tests if this file's extension is part of the recognized list of extensions |
|
191 """ |
|
192 |
|
193 return (self.fileext.lower() in ext_list) |
|
194 |
|
195 class Directory (Node) : |
|
196 """ |
|
197 A directory is a node that contains other nodes. |
|
198 """ |
|
199 |
|
200 # a list of (test_func, node_type) tuples for use by children() to build subnodes with |
|
201 NODE_TYPES = None |
|
202 |
|
203 def subdir (self, name) : |
|
204 """ |
|
205 Returns a Directory object representing the name underneath this dir |
|
206 """ |
|
207 |
|
208 return Directory(self, name=name) |
|
209 |
|
210 def subfile (self, name) : |
|
211 """ |
|
212 Returns a File object representing the name underneath this dir |
|
213 """ |
|
214 |
|
215 return Directory(self, name=name) |
|
216 |
|
217 def listdir (self, skip_dotfiles=True) : |
|
218 """ |
|
219 Yield a series of raw fsnames for nodes in this dir |
|
220 """ |
|
221 |
|
222 # expressed |
|
223 return (fsname for fsname in os.listdir(self.path) if not (skip_dotfiles and fsname.startswith('.'))) |
|
224 |
|
225 def child_nodes (self, skip_dotfiles=True) : |
|
226 """ |
|
227 Yield a series of nodes contained in this dir |
|
228 """ |
|
229 |
|
230 return (Node(self, fsname) for fsname in self.listdir(skip_dotfiles)) |
|
231 |
|
232 def __iter__ (self) : |
|
233 """ |
|
234 Iterating over a Directory yields sub-Nodes. |
|
235 |
|
236 Dotfiles are skipped. |
|
237 """ |
|
238 |
|
239 return self.childnodes() |
|
240 |
|
241 @property |
|
242 def root_path (self) : |
|
243 """ |
|
244 Build and return a relative path to the root of this dir tree |
|
245 """ |
|
246 |
|
247 # build using parent root_path |
|
248 return os.path.join('..', self.parent.root_path) |
|
249 |
|
250 def children (self) : |
|
251 """ |
|
252 Yield a series of Node subclasses representing the items in this dir. |
|
253 |
|
254 This uses self.NODE_TYPES to figure out what kind of sub-node object to build. This should be a list of |
|
255 (test_func, node_type) |
|
256 |
|
257 tuples, of which the first is a function that takes a Node as it's sole argument, and returns a boolean. |
|
258 For the first test_func which returns True, a Node-subclass object is constructed using node_type.from_node. |
|
259 """ |
|
260 |
|
261 for node in self : |
|
262 # figure out what type to use |
|
263 for test_func, node_type in self.NODE_TYPES : |
|
264 if test_func(node) : |
|
265 # matches, build |
|
266 yield node_type.from_node(node) |
|
267 |
|
268 else : |
|
269 # unknown file type! |
|
270 raise Exception("unrecongized type of file: %s" % node); |
|
271 |
|
272 # assign default Directory.NODE_TYPES |
|
273 Directory.NODE_TYPES = [ |
|
274 (Node.is_dir, Directory), |
|
275 (Node.is_file, File), |
|
276 ] |
|
277 |
|
278 |
|
279 class Root (Directory) : |
|
280 """ |
|
281 A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path. |
|
282 """ |
|
283 |
|
284 def __init__ (self, fspath, config) : |
|
285 """ |
|
286 Construct the directory tree root at the given 'real' path, which must be a raw str |
|
287 """ |
|
288 |
|
289 # abuse Node's concept of a "name" a bit |
|
290 super(Root, self).__init__(None, fspath, config=config) |
|
291 |
|
292 @property |
|
293 def path (self) : |
|
294 """ |
|
295 Returns the raw path |
|
296 """ |
|
297 |
|
298 return self.fsname |
|
299 |
|
300 @property |
|
301 def unicodepath (self) : |
|
302 """ |
|
303 Returns the raw decoded path |
|
304 """ |
|
305 |
|
306 return self.name |
|
307 |
|
308 @property |
|
309 def root_path (self) : |
|
310 """ |
|
311 Returns an empty string representing this dir |
|
312 """ |
|
313 |
|
314 return '' |
|
315 |
|
316 def __repr__ (self) : |
|
317 """ |
|
318 Override Node.__repr__ to not use self.parent.path |
|
319 """ |
|
320 |
|
321 return "Root(%r)" % self.fsname |
|
322 |