--- 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()
+