degal/filesystem.py
changeset 79 e5400304a3d3
parent 77 2a53c5ade434
child 85 7da934333469
equal deleted inserted replaced
78:d580323b4bfa 79:e5400304a3d3
    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 
   483             return sorted(iter)
   640             return sorted(iter)
   484 
   641 
   485         else :
   642         else :
   486             return iter
   643             return iter
   487 
   644 
   488     def __iter__ (self) :
   645     __iter__ = subnodes
   489         """
       
   490             Iterating over a Directory yields sub-Nodes.
       
   491 
       
   492             Dotfiles are skipped.
       
   493         """
       
   494         
       
   495         return self.subnodes()
       
   496 
   646 
   497     @property
   647     @property
   498     def root_path (self) :
   648     def root_path (self) :
   499         """
   649         """
   500             Build and return a relative path to the root of this dir tree
   650             Build and return a relative path to the root of this dir tree
   584 
   734 
   585         return ''
   735         return ''
   586 
   736 
   587     def path_segments (self, unicode=True) :
   737     def path_segments (self, unicode=True) :
   588         """
   738         """
   589             No path segments
   739             No path segments other than our own
   590         """
   740         """
   591 
   741         
   592         return []
   742         yield self.name if unicode else self.fsname
   593 
   743 
   594     def __repr__ (self) :
   744     def __repr__ (self) :
   595         """
   745         """
   596             Override Node.__repr__ to not use self.parent.path
   746             Override Node.__repr__ to not use self.parent.path
   597         """
   747         """
   598 
   748 
   599         return "Root(%r)" % self.fsname
   749         return "Root(%r)" % self.fsname
   600 
   750 
       
   751 # testing
       
   752 if __name__ == '__main__' :
       
   753     import doctest
       
   754 
       
   755     doctest.testmod()
       
   756