fix filesystem.Path.path_segments and add some doctests
authorTero Marttila <terom@fixme.fi>
Wed, 10 Jun 2009 23:14:37 +0300
changeset 79 e5400304a3d3
parent 78 d580323b4bfa
child 80 f4b637ae775c
fix filesystem.Path.path_segments and add some doctests
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()
+