24 def decode_fsname (self, fsname) : |
24 def decode_fsname (self, fsname) : |
25 """ |
25 """ |
26 Decode the given raw byte string representing a filesystem name into an user-readable unicode name. |
26 Decode the given raw byte string representing a filesystem name into an user-readable unicode name. |
27 |
27 |
28 XXX: currently just hardcoded as utf-8 |
28 XXX: currently just hardcoded as utf-8 |
|
29 |
|
30 >>> Node(None, 'foo').decode_fsname('\xa5\xa6') |
|
31 u'\\ufffd\\ufffd' |
29 """ |
32 """ |
30 |
33 |
31 return fsname.decode('utf-8', 'replace') |
34 return fsname.decode('utf-8', 'replace') |
32 |
35 |
33 def encode_name (self, name) : |
36 def encode_name (self, name) : |
34 """ |
37 """ |
35 Returns a suitable fsname for the given unicode name or strict ASCII str |
38 Returns a suitable fsname for the given unicode name or strict ASCII str |
36 |
39 |
37 XXX: currently just hardcoded as utf-8 |
40 XXX: currently just hardcoded as utf-8 |
|
41 |
|
42 >>> Node(None, 'foo').encode_name(u'ab') |
|
43 'ab' |
38 """ |
44 """ |
39 |
45 |
40 # this should fail for non-ASCII str |
46 # this should fail for non-ASCII str |
41 return name.encode('utf-8') |
47 return name.encode('utf-8') |
42 |
48 |
43 @classmethod |
|
44 def from_node (cls, node) : |
|
45 """ |
|
46 Construct from a Node object |
|
47 """ |
|
48 |
|
49 return cls(node.parent, node.fsname, node.name, node.config) |
|
50 |
|
51 def __init__ (self, parent, fsname=None, name=None, config=None) : |
49 def __init__ (self, parent, fsname=None, name=None, config=None) : |
52 """ |
50 """ |
53 Initialize the node with a parent and both name/fsname. |
51 Initialize the node with a parent and both name/fsname. |
54 |
52 |
55 If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name. |
53 If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name. |
56 |
54 |
57 If parent is given, but both fsname and name are None, then this node will be equal to the parent. |
55 If parent is given, but both fsname and name are None, then this node will be cloned from the parent. |
58 """ |
56 |
59 |
57 >>> Node(Root('/'), 'foo') |
|
58 Node('/', 'foo') |
|
59 >>> Node(Node(Root('/'), 'bar')) |
|
60 Node('/', 'bar') |
|
61 >>> Node(None, fsname='foo\xa5').name |
|
62 u'foo\\ufffd' |
|
63 >>> Node(None, name=u'foo').fsname |
|
64 'foo' |
|
65 """ |
|
66 |
|
67 # fsname must not be an unicode string |
60 assert not fsname or isinstance(fsname, str) |
68 assert not fsname or isinstance(fsname, str) |
61 |
69 |
62 if parent and not fsname and not name : |
70 if parent and not fsname and not name : |
63 # no name given -> we're the same as parent |
71 # no name given -> we're the same as parent |
64 self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name |
72 self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name |
69 |
77 |
70 # config, either as given, or copy from parent |
78 # config, either as given, or copy from parent |
71 if config : |
79 if config : |
72 self.config = config |
80 self.config = config |
73 |
81 |
74 elif parent : # XXX: else : |
82 elif parent : |
75 self.config = parent.config |
83 self.config = parent.config |
|
84 |
|
85 else : |
|
86 # XXX: no config |
|
87 self.config = None |
76 |
88 |
77 # fsname |
89 # fsname |
78 if fsname : |
90 if fsname : |
79 self.fsname = fsname |
91 self.fsname = fsname |
80 |
92 |
81 else : |
93 else : |
82 self.fsname = self.encode_name(name) |
94 self.fsname = self.encode_name(name) |
83 |
95 |
84 # name |
96 # name |
85 if name : |
97 if name : |
86 self.name = name |
98 self.name = unicode(name) |
87 |
99 |
88 else : |
100 else : |
89 self.name = self.decode_fsname(fsname) |
101 self.name = self.decode_fsname(fsname) |
90 |
102 |
91 def subnode (self, name) : |
103 def subnode (self, name) : |
92 """ |
104 """ |
93 Returns a Node object representing the given name behind this node. |
105 Returns a Node object representing the given name behind this node. |
94 |
106 |
95 The name should either be a plain ASCII string or unicode object. |
107 The name should either be a plain ASCII string or unicode object. |
|
108 |
|
109 >>> Node(Root('/'), 'foo').subnode('bar') |
|
110 Node('/foo', 'bar') |
96 """ |
111 """ |
97 |
112 |
98 return Node(self, name=name) |
113 return Node(self, name=name) |
99 |
114 |
100 def nodepath (self) : |
115 def nodepath (self) : |
101 """ |
116 """ |
102 Returns the path of nodes from this node to the root node, inclusive |
117 Returns the path of nodes from this node to the root node, inclusive |
103 """ |
118 >>> list(Node(Root('/'), 'foo').subnode('bar').nodepath()) |
104 |
119 [Root('/'), Node('/', 'foo'), Node('/foo', 'bar')] |
105 return self.parent.nodepath() + [self] |
120 """ |
|
121 |
|
122 # recursive generator |
|
123 for node in self.parent.nodepath() : |
|
124 yield node |
|
125 |
|
126 yield self |
106 |
127 |
107 @lazy_load |
128 @lazy_load |
108 def path (self) : |
129 def path (self) : |
109 """ |
130 """ |
110 Return the machine-readable root-path for this node |
131 Return the machine-readable root-path for this node |
|
132 |
|
133 >>> Node(Root('/'), 'foo').subnode('bar').path |
|
134 '/foo/bar' |
|
135 >>> Node(Root('/'), name=u'foo').path |
|
136 '/foo' |
|
137 >>> Node(Root('/'), fsname='\\x01\\x02').path |
|
138 '/\\x01\\x02' |
111 """ |
139 """ |
112 |
140 |
113 # build using parent path and our fsname |
141 # build using parent path and our fsname |
|
142 # XXX: rewrite using nodepath? |
114 return os.path.join(self.parent.path, self.fsname) |
143 return os.path.join(self.parent.path, self.fsname) |
115 |
144 |
116 @lazy_load |
145 @lazy_load |
117 def unicodepath (self) : |
146 def unicodepath (self) : |
118 """ |
147 """ |
119 Return the human-readable root-path for this node |
148 Return the human-readable root-path for this node |
|
149 |
|
150 >>> Node(Root('/'), 'foo').subnode('bar').unicodepath |
|
151 u'/foo/bar' |
|
152 >>> Node(Root('/'), name=u'foo').unicodepath |
|
153 u'/foo' |
|
154 >>> Node(Root('/'), fsname='\\x01\\x02').unicodepath |
|
155 u'/??' |
120 """ |
156 """ |
121 |
157 |
122 # build using parent unicodepath and our name |
158 # build using parent unicodepath and our name |
|
159 # XXX: rewrte using nodepath? |
123 return os.path.join(self.parent.path, self.name) |
160 return os.path.join(self.parent.path, self.name) |
124 |
161 |
125 def path_segments (self, unicode=True) : |
162 def path_segments (self, unicode=True) : |
126 """ |
163 """ |
127 Return a series of single-level names describing the path from the root to this node |
164 Return a series of single-level names describing the path from the root to this node. |
128 """ |
165 |
129 |
166 If `unicode` is given, then the returned items will be the unicode names, otherwise, the binary names. |
130 return self.parent.path_segments(unicode=unicode) + [self.name if unicode else self.fsname] |
167 |
|
168 >>> list(Node(Root('/'), 'foo').subnode('bar').path_segments()) |
|
169 [u'/', u'foo', u'bar'] |
|
170 >>> list(Node(Root('/'), 'foo').subnode('bar').path_segments(unicode=False)) |
|
171 ['/', 'foo', 'bar'] |
|
172 """ |
|
173 |
|
174 # iter |
|
175 for segment in self.parent.path_segments(unicode=unicode) : |
|
176 yield segment |
|
177 |
|
178 yield self.name if unicode else self.fsname |
131 |
179 |
132 def exists (self) : |
180 def exists (self) : |
133 """ |
181 """ |
134 Tests if this node exists on the physical filesystem |
182 Tests if this node exists on the physical filesystem |
|
183 |
|
184 >>> Node(Root('.'), '.').exists() |
|
185 True |
|
186 >>> Node(Root('/'), 'nonexistant').exists() |
|
187 False |
135 """ |
188 """ |
136 |
189 |
137 return os.path.exists(self.path) |
190 return os.path.exists(self.path) |
138 |
191 |
139 def is_dir (self) : |
192 def is_dir (self) : |
140 """ |
193 """ |
141 Tests if this node represents a directory on the filesystem |
194 Tests if this node represents a directory on the physical filesystem |
|
195 |
|
196 >>> Node(Root('/'), '.').is_dir() |
|
197 True |
|
198 >>> Root('/').subnode('dev').subnode('null').is_dir() |
|
199 False |
142 """ |
200 """ |
143 |
201 |
144 return os.path.isdir(self.path) |
202 return os.path.isdir(self.path) |
145 |
203 |
146 def is_file (self) : |
204 def is_file (self) : |
147 """ |
205 """ |
148 Tests if this node represents a normal file on the filesystem |
206 Tests if this node represents a normal file on the physical filesystem |
|
207 |
|
208 >>> Node(Root('/'), '.').is_file() |
|
209 False |
|
210 >>> Root('/').subnode('dev').subnode('null').is_file() |
|
211 False |
149 """ |
212 """ |
150 |
213 |
151 return os.path.isfile(self.path) |
214 return os.path.isfile(self.path) |
152 |
215 |
153 def test (self) : |
216 def test (self) : |
158 if not self.exists() : |
221 if not self.exists() : |
159 raise Exception("Filesystem node does not exist: %s" % self) |
222 raise Exception("Filesystem node does not exist: %s" % self) |
160 |
223 |
161 return self |
224 return self |
162 |
225 |
163 def path_from (self, node) : |
|
164 """ |
|
165 Returns a relative path to this node from the given node. |
|
166 |
|
167 This is the same as path_to, but just reversed. |
|
168 """ |
|
169 |
|
170 return node.path_to(self) |
|
171 |
|
172 def path_to (self, node) : |
226 def path_to (self, node) : |
173 """ |
227 """ |
174 Returns a relative path from this node to the given node |
228 Returns a relative path from this node to the given node |
|
229 |
|
230 XXX: doctests |
175 """ |
231 """ |
176 |
232 |
177 # get real paths for both |
233 # get real paths for both |
178 from_path = self.nodepath() |
234 from_path = list(self.nodepath()) |
179 to_path = node.nodepath() |
235 to_path = list(node.nodepath()) |
180 pivot = None |
236 pivot = None |
181 |
237 |
182 # reduce common prefix |
238 # reduce common prefix |
183 while from_path[0] == to_path[0] : |
239 while from_path and to_path and from_path[0] == to_path[0] : |
184 from_path.pop(0) |
240 from_path.pop(0) |
185 pivot = to_path.pop(0) |
241 pivot = to_path.pop(0) |
186 |
242 |
|
243 # full path |
|
244 path = itertools.chain(reversed(from_path), [pivot] if pivot else (), to_path) |
|
245 |
187 # build path |
246 # build path |
188 return Path(*itertools.chain(reversed(from_path), [pivot], to_path)) |
247 return Path(*path) |
|
248 |
|
249 def path_from (self, node) : |
|
250 """ |
|
251 Returns a relative path to this node from the given node. |
|
252 |
|
253 This is the same as path_to, but just reversed. |
|
254 """ |
|
255 |
|
256 return node.path_to(self) |
189 |
257 |
190 def stat (self, soft=False) : |
258 def stat (self, soft=False) : |
191 """ |
259 """ |
192 Returns the os.stat struct for this node. |
260 Returns the os.stat struct for this node. |
193 |
261 |
194 If `soft` is given, returns None if this node doesn't exist |
262 If `soft` is given, returns None if this node doesn't exist |
|
263 |
|
264 >>> Root('/').stat() is not None |
|
265 True |
|
266 >>> Root('/nonexistant').stat(soft=True) is None |
|
267 True |
195 """ |
268 """ |
196 |
269 |
197 try : |
270 try : |
198 return os.stat(self.path) |
271 return os.stat(self.path) |
199 |
272 |
216 Returns a str representing this dir |
289 Returns a str representing this dir |
217 """ |
290 """ |
218 |
291 |
219 return "Node(%r, %r)" % (self.parent.path, self.fsname) |
292 return "Node(%r, %r)" % (self.parent.path, self.fsname) |
220 |
293 |
|
294 def __eq__ (self, other) : |
|
295 """ |
|
296 Compare for equality |
|
297 """ |
|
298 |
|
299 return isinstance(other, Node) and self.name == other.name and self.parent == other.parent |
|
300 |
221 def __cmp__ (self, other) : |
301 def __cmp__ (self, other) : |
222 """ |
302 """ |
223 Comparisons between Nodes |
303 Arbitrary comparisons between Nodes |
224 """ |
304 |
225 |
305 >>> cmp(Node(None, 'foo'), Node(None, 'foo')) |
226 return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name)) |
306 0 |
|
307 >>> cmp(Node(None, 'aaa'), Node(None, 'bbb')) |
|
308 -1 |
|
309 >>> cmp(Node(None, 'bbb'), Node(None, 'aaa')) |
|
310 1 |
|
311 |
|
312 >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'a'), 'aa')) |
|
313 0 |
|
314 >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'a'), 'ab')) |
|
315 -1 |
|
316 >>> cmp(Node(Node(None, 'a'), 'ab'), Node(Node(None, 'a'), 'aa')) |
|
317 1 |
|
318 |
|
319 >>> cmp(Node(Node(None, 'a'), 'zz'), Node(Node(None, 'b'), 'aa')) |
|
320 -1 |
|
321 >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'b'), 'zz')) |
|
322 -1 |
|
323 >>> cmp(Node(Node(None, 'z'), 'aa'), Node(Node(None, 'a'), 'zz')) |
|
324 1 |
|
325 """ |
|
326 |
|
327 if other is None : |
|
328 # arbitrary... |
|
329 return 1 |
|
330 |
|
331 else : |
|
332 return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name)) |
227 |
333 |
228 class Path (object) : |
334 class Path (object) : |
229 """ |
335 """ |
230 A Path is a sequence of Nodes that form a path through the node tree. |
336 A Path is a sequence of Nodes that form a path through a Node tree rooted at some Root. |
231 |
337 |
232 Each node must either be the parent or the child of the following node. |
338 Each node must either be the parent or the child of the following node. |
|
339 |
|
340 The first and last nodes may be Files, but all other objects must be Directories. |
233 """ |
341 """ |
234 |
342 |
235 def __init__ (self, *nodes) : |
343 def __init__ (self, *nodes) : |
236 """ |
344 """ |
237 Initialize with the given node path |
345 Initialize with the given node path. |
|
346 |
|
347 The node path must not be empty. |
238 """ |
348 """ |
239 |
349 |
240 self.nodes = nodes |
350 self.nodes = nodes |
241 |
351 |
242 def subpath (self, *nodes) : |
352 def subpath (self, *nodes) : |
246 |
356 |
247 return Path(*itertools.chain(self.nodes, nodes)) |
357 return Path(*itertools.chain(self.nodes, nodes)) |
248 |
358 |
249 def path_segments (self, unicode=True) : |
359 def path_segments (self, unicode=True) : |
250 """ |
360 """ |
251 Yields a series of physical path segments for this path |
361 Yields a series of physical path segments for this path. |
252 """ |
362 |
253 |
363 File -> Directory : |
254 prev = None |
364 file.parent == dir -> nothing |
255 |
365 |
|
366 Directory -> Directory : |
|
367 dir_1.parent == dir_2 -> '..' |
|
368 dir_1 == dir_2.parent -> dir_2.name |
|
369 |
|
370 Directory -> File : |
|
371 file.parent == dir -> file.name |
|
372 |
|
373 >>> root = Root('root'); Path(root, root.subfile('foo')) |
|
374 Path('foo') |
|
375 >>> root = Root('root'); Path(root, root.subdir('foo'), root.subdir('foo').subfile('bar')) |
|
376 Path('foo', 'bar') |
|
377 >>> root = Root('root'); Path(root.subfile('foo'), root) |
|
378 Path('.') |
|
379 >>> root = Root('root'); Path(root.subfile('foo'), root, root.subfile('bar')) |
|
380 Path('bar') |
|
381 >>> root = Root('root'); Path(root.subfile('foo')) |
|
382 Path('foo') |
|
383 """ |
|
384 |
|
385 # XXX: this logic should be implemented as methods in Node |
|
386 |
|
387 prev = prev_last = None |
|
388 |
|
389 # output directory components |
256 for node in self.nodes : |
390 for node in self.nodes : |
257 if not prev : |
391 if not prev : |
258 # ignore |
392 # ignore the first item for now |
259 pass |
393 pass |
260 |
394 |
261 elif prev.parent and prev.parent == node : |
395 elif isinstance(prev, File) : |
262 # up a level |
396 # going from a file to its dir doesn't require anything |
263 yield u'..' if unicode else '..' |
397 assert isinstance(node, Directory) and prev.parent == node |
|
398 |
|
399 elif isinstance(node, File) : |
|
400 # final target, must come from a directory |
|
401 assert node is self.nodes[-1] and (not prev or (isinstance(prev, Directory) and node.parent == prev)) |
|
402 |
|
403 elif prev.parent == node : |
|
404 # going from a dir into the dir above it |
|
405 yield '..' |
|
406 |
|
407 elif node.parent == prev : |
|
408 # going from a dir into a dir underneath it |
|
409 yield node.name if unicode else node.fsname |
264 |
410 |
265 else : |
411 else : |
266 # down a level |
412 raise Exception("invalid path: %r" % (self.nodes, )) |
267 yield node.name if unicode else node.fsname |
413 |
268 |
|
269 # chained together |
414 # chained together |
|
415 prev_last = prev |
270 prev = node |
416 prev = node |
271 |
417 |
|
418 # output final file/lone dir component |
|
419 if isinstance(node, File) : |
|
420 # the last/only node is the final file target and must *always* be output |
|
421 yield node.name if unicode else node.fsname |
|
422 |
|
423 elif isinstance(node, Directory) and (prev_last is None or isinstance(prev_last, File)) : |
|
424 assert prev_last.parent == node |
|
425 |
|
426 # going from a file into it's own directory is a direct reference |
|
427 yield '.' |
|
428 |
272 def __iter__ (self) : |
429 def __iter__ (self) : |
273 """ |
430 """ |
274 Iterate over the nodes |
431 Iterate over the nodes |
275 """ |
432 """ |
276 |
433 |