# HG changeset patch # User Tero Marttila # Date 1244664877 -10800 # Node ID e5400304a3d39928f53c359e68f217ccf4be470e # Parent d580323b4bfa4d85b5143636ef6d864a6c1d0f11 fix filesystem.Path.path_segments and add some doctests diff -r d580323b4bfa -r e5400304a3d3 degal/filesystem.py --- a/degal/filesystem.py Wed Jun 10 21:59:49 2009 +0300 +++ b/degal/filesystem.py Wed Jun 10 23:14:37 2009 +0300 @@ -26,6 +26,9 @@ Decode the given raw byte string representing a filesystem name into an user-readable unicode name. XXX: currently just hardcoded as utf-8 + + >>> Node(None, 'foo').decode_fsname('\xa5\xa6') + u'\\ufffd\\ufffd' """ return fsname.decode('utf-8', 'replace') @@ -35,28 +38,33 @@ Returns a suitable fsname for the given unicode name or strict ASCII str XXX: currently just hardcoded as utf-8 + + >>> Node(None, 'foo').encode_name(u'ab') + 'ab' """ # this should fail for non-ASCII str return name.encode('utf-8') - @classmethod - def from_node (cls, node) : - """ - Construct from a Node object - """ - - return cls(node.parent, node.fsname, node.name, node.config) - def __init__ (self, parent, fsname=None, name=None, config=None) : """ Initialize the node with a parent and both name/fsname. If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name. - If parent is given, but both fsname and name are None, then this node will be equal to the parent. + If parent is given, but both fsname and name are None, then this node will be cloned from the parent. + + >>> Node(Root('/'), 'foo') + Node('/', 'foo') + >>> Node(Node(Root('/'), 'bar')) + Node('/', 'bar') + >>> Node(None, fsname='foo\xa5').name + u'foo\\ufffd' + >>> Node(None, name=u'foo').fsname + 'foo' """ - + + # fsname must not be an unicode string assert not fsname or isinstance(fsname, str) if parent and not fsname and not name : @@ -71,8 +79,12 @@ if config : self.config = config - elif parent : # XXX: else : + elif parent : self.config = parent.config + + else : + # XXX: no config + self.config = None # fsname if fsname : @@ -83,7 +95,7 @@ # name if name : - self.name = name + self.name = unicode(name) else : self.name = self.decode_fsname(fsname) @@ -93,6 +105,9 @@ Returns a Node object representing the given name behind this node. The name should either be a plain ASCII string or unicode object. + + >>> Node(Root('/'), 'foo').subnode('bar') + Node('/foo', 'bar') """ return Node(self, name=name) @@ -100,52 +115,100 @@ def nodepath (self) : """ Returns the path of nodes from this node to the root node, inclusive + >>> list(Node(Root('/'), 'foo').subnode('bar').nodepath()) + [Root('/'), Node('/', 'foo'), Node('/foo', 'bar')] """ + + # recursive generator + for node in self.parent.nodepath() : + yield node - return self.parent.nodepath() + [self] + yield self @lazy_load def path (self) : """ Return the machine-readable root-path for this node + + >>> Node(Root('/'), 'foo').subnode('bar').path + '/foo/bar' + >>> Node(Root('/'), name=u'foo').path + '/foo' + >>> Node(Root('/'), fsname='\\x01\\x02').path + '/\\x01\\x02' """ # build using parent path and our fsname + # XXX: rewrite using nodepath? return os.path.join(self.parent.path, self.fsname) @lazy_load def unicodepath (self) : """ Return the human-readable root-path for this node + + >>> Node(Root('/'), 'foo').subnode('bar').unicodepath + u'/foo/bar' + >>> Node(Root('/'), name=u'foo').unicodepath + u'/foo' + >>> Node(Root('/'), fsname='\\x01\\x02').unicodepath + u'/??' """ # build using parent unicodepath and our name + # XXX: rewrte using nodepath? return os.path.join(self.parent.path, self.name) def path_segments (self, unicode=True) : """ - Return a series of single-level names describing the path from the root to this node + Return a series of single-level names describing the path from the root to this node. + + If `unicode` is given, then the returned items will be the unicode names, otherwise, the binary names. + + >>> list(Node(Root('/'), 'foo').subnode('bar').path_segments()) + [u'/', u'foo', u'bar'] + >>> list(Node(Root('/'), 'foo').subnode('bar').path_segments(unicode=False)) + ['/', 'foo', 'bar'] """ - return self.parent.path_segments(unicode=unicode) + [self.name if unicode else self.fsname] + # iter + for segment in self.parent.path_segments(unicode=unicode) : + yield segment + + yield self.name if unicode else self.fsname def exists (self) : """ Tests if this node exists on the physical filesystem + + >>> Node(Root('.'), '.').exists() + True + >>> Node(Root('/'), 'nonexistant').exists() + False """ return os.path.exists(self.path) def is_dir (self) : """ - Tests if this node represents a directory on the filesystem + Tests if this node represents a directory on the physical filesystem + + >>> Node(Root('/'), '.').is_dir() + True + >>> Root('/').subnode('dev').subnode('null').is_dir() + False """ return os.path.isdir(self.path) def is_file (self) : """ - Tests if this node represents a normal file on the filesystem + Tests if this node represents a normal file on the physical filesystem + + >>> Node(Root('/'), '.').is_file() + False + >>> Root('/').subnode('dev').subnode('null').is_file() + False """ return os.path.isfile(self.path) @@ -160,6 +223,29 @@ return self + def path_to (self, node) : + """ + Returns a relative path from this node to the given node + + XXX: doctests + """ + + # get real paths for both + from_path = list(self.nodepath()) + to_path = list(node.nodepath()) + pivot = None + + # reduce common prefix + while from_path and to_path and from_path[0] == to_path[0] : + from_path.pop(0) + pivot = to_path.pop(0) + + # full path + path = itertools.chain(reversed(from_path), [pivot] if pivot else (), to_path) + + # build path + return Path(*path) + def path_from (self, node) : """ Returns a relative path to this node from the given node. @@ -169,29 +255,16 @@ return node.path_to(self) - def path_to (self, node) : - """ - Returns a relative path from this node to the given node - """ - - # get real paths for both - from_path = self.nodepath() - to_path = node.nodepath() - pivot = None - - # reduce common prefix - while from_path[0] == to_path[0] : - from_path.pop(0) - pivot = to_path.pop(0) - - # build path - return Path(*itertools.chain(reversed(from_path), [pivot], to_path)) - def stat (self, soft=False) : """ Returns the os.stat struct for this node. If `soft` is given, returns None if this node doesn't exist + + >>> Root('/').stat() is not None + True + >>> Root('/nonexistant').stat(soft=True) is None + True """ try : @@ -218,23 +291,60 @@ return "Node(%r, %r)" % (self.parent.path, self.fsname) + def __eq__ (self, other) : + """ + Compare for equality + """ + + return isinstance(other, Node) and self.name == other.name and self.parent == other.parent + def __cmp__ (self, other) : """ - Comparisons between Nodes + Arbitrary comparisons between Nodes + + >>> cmp(Node(None, 'foo'), Node(None, 'foo')) + 0 + >>> cmp(Node(None, 'aaa'), Node(None, 'bbb')) + -1 + >>> cmp(Node(None, 'bbb'), Node(None, 'aaa')) + 1 + + >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'a'), 'aa')) + 0 + >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'a'), 'ab')) + -1 + >>> cmp(Node(Node(None, 'a'), 'ab'), Node(Node(None, 'a'), 'aa')) + 1 + + >>> cmp(Node(Node(None, 'a'), 'zz'), Node(Node(None, 'b'), 'aa')) + -1 + >>> cmp(Node(Node(None, 'a'), 'aa'), Node(Node(None, 'b'), 'zz')) + -1 + >>> cmp(Node(Node(None, 'z'), 'aa'), Node(Node(None, 'a'), 'zz')) + 1 """ - return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name)) + if other is None : + # arbitrary... + return 1 + + else : + return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name)) class Path (object) : """ - A Path is a sequence of Nodes that form a path through the node tree. + A Path is a sequence of Nodes that form a path through a Node tree rooted at some Root. Each node must either be the parent or the child of the following node. + + The first and last nodes may be Files, but all other objects must be Directories. """ def __init__ (self, *nodes) : """ - Initialize with the given node path + Initialize with the given node path. + + The node path must not be empty. """ self.nodes = nodes @@ -248,27 +358,74 @@ def path_segments (self, unicode=True) : """ - Yields a series of physical path segments for this path + Yields a series of physical path segments for this path. + + File -> Directory : + file.parent == dir -> nothing + + Directory -> Directory : + dir_1.parent == dir_2 -> '..' + dir_1 == dir_2.parent -> dir_2.name + + Directory -> File : + file.parent == dir -> file.name + + >>> root = Root('root'); Path(root, root.subfile('foo')) + Path('foo') + >>> root = Root('root'); Path(root, root.subdir('foo'), root.subdir('foo').subfile('bar')) + Path('foo', 'bar') + >>> root = Root('root'); Path(root.subfile('foo'), root) + Path('.') + >>> root = Root('root'); Path(root.subfile('foo'), root, root.subfile('bar')) + Path('bar') + >>> root = Root('root'); Path(root.subfile('foo')) + Path('foo') """ - prev = None - + # XXX: this logic should be implemented as methods in Node + + prev = prev_last = None + + # output directory components for node in self.nodes : if not prev : - # ignore + # ignore the first item for now pass - elif prev.parent and prev.parent == node : - # up a level - yield u'..' if unicode else '..' + elif isinstance(prev, File) : + # going from a file to its dir doesn't require anything + assert isinstance(node, Directory) and prev.parent == node + + elif isinstance(node, File) : + # final target, must come from a directory + assert node is self.nodes[-1] and (not prev or (isinstance(prev, Directory) and node.parent == prev)) + + elif prev.parent == node : + # going from a dir into the dir above it + yield '..' + + elif node.parent == prev : + # going from a dir into a dir underneath it + yield node.name if unicode else node.fsname else : - # down a level - yield node.name if unicode else node.fsname + raise Exception("invalid path: %r" % (self.nodes, )) + + # chained together + prev_last = prev + prev = node - # chained together - prev = node + # output final file/lone dir component + if isinstance(node, File) : + # the last/only node is the final file target and must *always* be output + yield node.name if unicode else node.fsname + elif isinstance(node, Directory) and (prev_last is None or isinstance(prev_last, File)) : + assert prev_last.parent == node + + # going from a file into it's own directory is a direct reference + yield '.' + def __iter__ (self) : """ Iterate over the nodes @@ -485,14 +642,7 @@ else : return iter - def __iter__ (self) : - """ - Iterating over a Directory yields sub-Nodes. - - Dotfiles are skipped. - """ - - return self.subnodes() + __iter__ = subnodes @property def root_path (self) : @@ -586,10 +736,10 @@ def path_segments (self, unicode=True) : """ - No path segments + No path segments other than our own """ - - return [] + + yield self.name if unicode else self.fsname def __repr__ (self) : """ @@ -598,3 +748,9 @@ return "Root(%r)" % self.fsname +# testing +if __name__ == '__main__' : + import doctest + + doctest.testmod() +