degal/filesystem.py
changeset 60 406da27a4be2
parent 55 77abe8dca695
child 68 49388e9fd5fa
equal deleted inserted replaced
59:fbbe956229cc 60:406da27a4be2
     1 """
     1 """
     2     Filesystem path handling
     2     Filesystem path handling
     3 """
     3 """
     4 
     4 
     5 import os, os.path, errno
     5 import os, os.path, errno
     6 import codecs
     6 import codecs, shutil
       
     7 import itertools
       
     8 
       
     9 from utils import lazy_load
     7 
    10 
     8 class Node (object) :
    11 class Node (object) :
     9     """
    12     """
    10         A filesystem object is basically just complicated representation of a path.
    13         A filesystem object is basically just complicated representation of a path.
    11         
    14         
    92             The name should either be a plain ASCII string or unicode object.
    95             The name should either be a plain ASCII string or unicode object.
    93         """
    96         """
    94         
    97         
    95         return Node(self, name=name)
    98         return Node(self, name=name)
    96  
    99  
    97     @property
   100     def nodepath (self) :
       
   101         """
       
   102             Returns the path of nodes from this node to the root node, inclusive
       
   103         """
       
   104 
       
   105         return self.parent.nodepath() + [self]
       
   106 
       
   107     @lazy_load
    98     def path (self) :
   108     def path (self) :
    99         """
   109         """
   100             Build and return the real filesystem path for this node
   110             Return the machine-readable root-path for this node
   101         """
   111         """
   102         
   112         
   103         # build using parent path and our fsname
   113         # build using parent path and our fsname
   104         return os.path.join(self.parent.path, self.fsname)
   114         return os.path.join(self.parent.path, self.fsname)
   105     
   115     
   106     @property
   116     @lazy_load
   107     def unicodepath (self) :
   117     def unicodepath (self) :
   108         """
   118         """
   109             Build and return the fake unicode filesystem path for this node
   119             Return the human-readable root-path for this node
   110         """
   120         """
   111         
   121         
   112         # build using parent unicodepath and our name
   122         # build using parent unicodepath and our name
   113         return os.path.join(self.parent.path, self.name)
   123         return os.path.join(self.parent.path, self.name)
   114    
   124    
       
   125     def path_segments (self, unicode=True) :
       
   126         """
       
   127             Return a series of single-level names describing the path from the root to this node
       
   128         """
       
   129 
       
   130         return self.parent.path_segments(unicode=unicode) + [self.name if unicode else self.fsname]
       
   131 
   115     def exists (self) :
   132     def exists (self) :
   116         """
   133         """
   117             Tests if this node exists on the physical filesystem
   134             Tests if this node exists on the physical filesystem
   118         """
   135         """
   119 
   136 
   130         """
   147         """
   131             Tests if this node represents a normal file on the filesystem
   148             Tests if this node represents a normal file on the filesystem
   132         """
   149         """
   133 
   150 
   134         return os.path.isfile(self.path)
   151         return os.path.isfile(self.path)
       
   152 
       
   153     def test (self) :
       
   154         """
       
   155             Tests that this node exists. Raises an error it not, otherwise, returns the node itself
       
   156         """
       
   157 
       
   158         if not self.exists() :
       
   159             raise Exception("Filesystem node does not exist: %s" % self)
       
   160 
       
   161         return self
   135     
   162     
   136     def path_from (self, node) :
   163     def path_from (self, node) :
   137         """
   164         """
   138             Returns a relative path to this node from the given node
   165             Returns a relative path to this node from the given node.
   139         """
   166 
   140 
   167             This is the same as path_to, but just reversed.
   141         XXX
   168         """
       
   169         
       
   170         return node.path_to(self)
       
   171 
       
   172     def path_to (self, node) :
       
   173         """
       
   174             Returns a relative path from this node to the given node
       
   175         """
       
   176 
       
   177         # get real paths for both
       
   178         from_path = self.nodepath()
       
   179         to_path = node.nodepath()
       
   180         pivot = None
       
   181 
       
   182         # reduce common prefix
       
   183         while from_path[0] == to_path[0] :
       
   184             from_path.pop(0)
       
   185             pivot = to_path.pop(0)
       
   186 
       
   187         # build path
       
   188         return Path(*itertools.chain(reversed(from_path), [pivot], to_path))
   142 
   189 
   143     def stat (self, soft=False) :
   190     def stat (self, soft=False) :
   144         """
   191         """
   145             Returns the os.stat struct for this node.
   192             Returns the os.stat struct for this node.
   146             
   193             
   155             if soft and e.errno == errno.ENOENT :
   202             if soft and e.errno == errno.ENOENT :
   156                 return None
   203                 return None
   157 
   204 
   158             else :
   205             else :
   159                 raise
   206                 raise
   160 
   207     
       
   208     # alias str/unicode
       
   209     str = path
       
   210     unicode = unicodepath
       
   211     
       
   212     def __repr__ (self) :
       
   213         """
       
   214             Returns a str representing this dir
       
   215         """
       
   216 
       
   217         return "Node(%r, %r)" % (self.parent.path, self.fsname)
       
   218     
       
   219     def __cmp__ (self, other) :
       
   220         """
       
   221             Comparisons between Nodes
       
   222         """
       
   223 
       
   224         return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name))
       
   225 
       
   226 class Path (object) :
       
   227     """
       
   228         A Path is a sequence of Nodes that form a path through the node tree.
       
   229 
       
   230         Each node must either be the parent or the child of the following node.
       
   231     """
       
   232 
       
   233     def __init__ (self, *nodes) :
       
   234         """
       
   235             Initialize with the given node path
       
   236         """
       
   237 
       
   238         self.nodes = nodes
       
   239     
       
   240     def subpath (self, *nodes) :
       
   241         """
       
   242             Returns a new path with the given node(s) appended
       
   243         """
       
   244 
       
   245         return Path(*itertools.chain(self.nodes, nodes))
       
   246     
       
   247     def path_segments (self, unicode=True) :
       
   248         """
       
   249             Yields a series of physical path segments for this path
       
   250         """
       
   251 
       
   252         prev = None
       
   253 
       
   254         for node in self.nodes :
       
   255             if not prev :
       
   256                 # ignore
       
   257                 pass
       
   258 
       
   259             elif prev.parent and prev.parent == node :
       
   260                 # up a level
       
   261                 yield u'..' if unicode else '..'
       
   262             
       
   263             else :
       
   264                 # down a level
       
   265                 yield node.name if unicode else node.fsname
       
   266             
       
   267             # chained together
       
   268             prev = node
       
   269 
       
   270     def __iter__ (self) :
       
   271         """
       
   272             Iterate over the nodes
       
   273         """
       
   274 
       
   275         return iter(self.nodes)
       
   276     
       
   277     def __unicode__ (self) :
       
   278         """
       
   279             Returns the unicode human-readable path
       
   280         """
       
   281 
       
   282         return os.path.join(*self.path_segments(unicode=True))
       
   283     
   161     def __str__ (self) :
   284     def __str__ (self) :
   162         """
   285         """
   163             Returns the raw filesystem path
   286             Returns the binary machine-readable path
   164         """
   287         """
   165 
   288 
   166         return self.path
   289         return os.path.join(*self.path_segments(unicode=False))
   167 
       
   168     def __unicode__ (self) :
       
   169         """
       
   170             Returns the human-readable path
       
   171         """
       
   172 
       
   173         return self.unicodepath
       
   174     
   290     
   175     def __repr__ (self) :
   291     def __repr__ (self) :
   176         """
   292         return "Path(%s)" % ', '.join(repr(segment) for segment in self.path_segments(unicode=False))
   177             Returns a str representing this dir
       
   178         """
       
   179 
       
   180         return "Node(%r, %r)" % (self.parent.path, self.fsname)
       
   181     
       
   182     def __cmp__ (self) :
       
   183         """
       
   184             Comparisons between Nodes
       
   185         """
       
   186 
       
   187         return cmp((self.parent, self.name), (other.parent, other.name))
       
   188 
   293 
   189 class File (Node) :
   294 class File (Node) :
   190     """
   295     """
   191         A file. Simple, eh?
   296         A file. Simple, eh?
   192     """
   297     """
   216             Tests if this file's extension is part of the recognized list of extensions
   321             Tests if this file's extension is part of the recognized list of extensions
   217         """
   322         """
   218         
   323         
   219         return (self.fileext.lower() in ext_list)
   324         return (self.fileext.lower() in ext_list)
   220     
   325     
       
   326     def test (self) :
       
   327         """
       
   328             Tests that this file exists as a file. Raises an error it not, otherwise, returns itself
       
   329         """
       
   330 
       
   331         if not self.is_file() :
       
   332             raise Exception("File does not exist: %s" % self)
       
   333 
       
   334         return self
       
   335 
   221     def open (self, mode='r', encoding=None, errors=None, bufsize=None) :
   336     def open (self, mode='r', encoding=None, errors=None, bufsize=None) :
   222         """
   337         """
   223             Wrapper for open/codecs.open
   338             Wrapper for open/codecs.open.
   224         """
   339 
       
   340             Raises an error if read_only mode is set and mode contains any of 'wa+'
       
   341         """
       
   342 
       
   343         if self.config.read_only and any((c in mode) for c in 'wa+') :
       
   344             raise Exception("Unable to open file for %s due to read_only mode: %s" % (mode, self))
   225 
   345 
   226         if encoding :
   346         if encoding :
   227             return codecs.open(self.path, mode, encoding, errors, bufsize)
   347             return codecs.open(self.path, mode, encoding, errors, bufsize)
   228 
   348 
   229         else :
   349         else :
   230             return open(self.path, mode, bufsize)
   350             return open(self.path, mode, bufsize)
       
   351 
       
   352     def copy_from (self, file) :
       
   353         """
       
   354             Replace this file with a copy of the given file with default permissions.
       
   355 
       
   356             Raises an error if read_only mode is set.
       
   357 
       
   358             XXX: accept mode
       
   359         """
       
   360 
       
   361         if self.config.read_only :
       
   362             raise Exception("Not copying file as read_only mode is set: %s -> %s" % (file, self))
       
   363         
       
   364         # perform the copy
       
   365         shutil.copyfile(file.path, self.path)
   231 
   366 
   232 class Directory (Node) :
   367 class Directory (Node) :
   233     """
   368     """
   234         A directory is a node that contains other nodes.
   369         A directory is a node that contains other nodes.
   235     """
   370     """
   239    
   374    
   240     def subdir (self, name, create=False) :
   375     def subdir (self, name, create=False) :
   241         """
   376         """
   242             Returns a Directory object representing the name underneath this dir.
   377             Returns a Directory object representing the name underneath this dir.
   243 
   378 
   244             If the create option is given, the directory will be created if it does not exist.
   379             If the create option is given, the directory will be created if it does not exist. Note that this will
   245         """
   380             raise an error if read_only mode is set
   246 
   381         """
   247         dir = Directory(self, name=name)
   382 
       
   383         subdir = Directory(self, name=name)
   248         
   384         
   249         # try and mkdir?
   385         # try and mkdir?
   250         if create and not dir.is_dir() :
   386         if create and not subdir.is_dir() :
   251             dir.mkdir()
   387             # create it!
       
   388             subdir.mkdir()
       
   389 
       
   390         return subdir
   252     
   391     
   253     def subfile (self, name) :
   392     def subfile (self, name) :
   254         """
   393         """
   255             Returns a File object representing the name underneath this dir
   394             Returns a File object representing the name underneath this dir
   256         """
   395         """
   257 
   396 
   258         return Directory(self, name=name)
   397         return Directory(self, name=name)
   259 
   398 
       
   399     def test (self) :
       
   400         """
       
   401             Tests that this dir exists as a dir. Raises an error it not, otherwise, returns itself
       
   402         """
       
   403 
       
   404         if not self.is_dir() :
       
   405             raise Exception("Directory does not exist: %s" % self)
       
   406 
       
   407         return self
       
   408 
   260     def mkdir (self) :
   409     def mkdir (self) :
   261         """
   410         """
   262             Create this directory with default permissions
   411             Create this directory with default permissions.
       
   412 
       
   413             This will fail if read_only mode is set
   263             
   414             
   264             XXX: mode argument
   415             XXX: mode argument
   265         """
   416         """
       
   417         
       
   418         if self.config.read_only :
       
   419             # forbidden
       
   420             raise Exception("Unable to create dir due to read_only mode: %s" % self)
   266         
   421         
   267         # do it
   422         # do it
   268         os.mkdir(self.path)
   423         os.mkdir(self.path)
   269 
   424 
   270     def listdir (self, skip_dotfiles=True) :
   425     def listdir (self, skip_dotfiles=True) :
   346 class Root (Directory) :
   501 class Root (Directory) :
   347     """
   502     """
   348         A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path.
   503         A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path.
   349     """
   504     """
   350 
   505 
   351     def __init__ (self, fspath, config) :
   506     # XXX: config needs a default
       
   507     def __init__ (self, fspath, config=None) :
   352         """
   508         """
   353             Construct the directory tree root at the given 'real' path, which must be a raw str
   509             Construct the directory tree root at the given 'real' path, which must be a raw str
   354         """
   510         """
   355 
   511 
   356         # abuse Node's concept of a "name" a bit
   512         # abuse Node's concept of a "name" a bit
   357         super(Root, self).__init__(None, fspath, config=config)
   513         super(Root, self).__init__(None, fspath, config=config)
   358 
   514 
       
   515     def nodepath (self) :
       
   516         """
       
   517             Just return ourself
       
   518         """
       
   519         
       
   520         return [self]
       
   521 
   359     @property
   522     @property
   360     def path (self) :
   523     def path (self) :
   361         """
   524         """
   362             Returns the raw path
   525             Returns the raw path
   363         """
   526         """
   378             Returns an empty string representing this dir
   541             Returns an empty string representing this dir
   379         """
   542         """
   380 
   543 
   381         return ''
   544         return ''
   382 
   545 
       
   546     def path_segments (self, unicode=True) :
       
   547         """
       
   548             No path segments
       
   549         """
       
   550 
       
   551         return []
       
   552 
   383     def __repr__ (self) :
   553     def __repr__ (self) :
   384         """
   554         """
   385             Override Node.__repr__ to not use self.parent.path
   555             Override Node.__repr__ to not use self.parent.path
   386         """
   556         """
   387 
   557