--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/degal/filesystem.py Thu Jun 04 11:22:52 2009 +0300
@@ -0,0 +1,322 @@
+"""
+ Filesystem path handling
+"""
+
+import os, os.path
+
+class Node (object) :
+ """
+ A filesystem object is basically just complicated representation of a path.
+
+ On the plus side, it has a parent node and can handle unicode/binary paths.
+ """
+
+ # the binary name
+ fsname = None
+
+ # the unicode name
+ name = None
+
+ def decode_fsname (self, fsname) :
+ """
+ Decode the given raw byte string representing a filesystem name into an user-readable unicode name.
+
+ XXX: currently just hardcoded as utf-8
+ """
+
+ return fsname.decode('utf-8', 'replace')
+
+ def encode_name (self, name) :
+ """
+ Returns a suitable fsname for the given unicode name or strict ASCII str
+
+ XXX: currently just hardcoded as utf-8
+ """
+
+ # 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.
+ """
+
+ assert not fsname or isinstance(fsname, str)
+
+ if parent and not fsname and not name :
+ # no name given -> we're the same as parent
+ self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name
+
+ else :
+ # store
+ self.parent = parent
+
+ # config, either as given, or copy from parent
+ if config :
+ self.config = config
+
+ else :
+ self.config = parent.config
+
+ # fsname
+ if fsname :
+ self.fsname = fsname
+
+ else :
+ self.fsname = self.encode_name(name)
+
+ # name
+ if name :
+ self.name = name
+
+ else :
+ self.name = self.decode_fsname(fsname)
+
+ def subnode (self, name) :
+ """
+ Returns a Node object representing the given name behind this node.
+
+ The name should either be a plain ASCII string or unicode object.
+ """
+
+ return Node(self, name=name)
+
+ @property
+ def path (self) :
+ """
+ Build and return the real filesystem path for this node
+ """
+
+ # build using parent path and our fsname
+ return os.path.join(self.parent.path, self.fsname)
+
+ @property
+ def unicodepath (self) :
+ """
+ Build and return the fake unicode filesystem path for this node
+ """
+
+ # build using parent unicodepath and our name
+ return os.path.join(self.parent.path, self.name)
+
+ def exists (self) :
+ """
+ Tests if this node exists on the physical filesystem
+ """
+
+ return os.path.exists(self.path)
+
+ def is_dir (self) :
+ """
+ Tests if this node represents a directory on the filesystem
+ """
+
+ return os.path.isdir(self.path)
+
+ def is_file (self) :
+ """
+ Tests if this node represents a normal file on the filesystem
+ """
+
+ return os.path.isfile(self.path)
+
+ def __str__ (self) :
+ """
+ Returns the raw filesystem path
+ """
+
+ return self.path
+
+ def __unicode__ (self) :
+ """
+ Returns the human-readable path
+ """
+
+ return self.unicodepath
+
+ def __repr__ (self) :
+ """
+ Returns a str representing this dir
+ """
+
+ return "Node(%r, %r)" % (self.parent.path, self.fsname)
+
+ def __cmp__ (self) :
+ """
+ Comparisons between Nodes
+ """
+
+ return cmp((self.parent, self.name), (other.parent, other.name))
+
+class File (Node) :
+ """
+ A file. Simple, eh?
+ """
+
+ @property
+ def basename (self) :
+ """
+ Returns the "base" part of this file's name, i.e. the filename without the extension
+ """
+
+ basename, _ = os.path.splitext(self.name)
+
+ return basename
+
+ @property
+ def fileext (self) :
+ """
+ Returns the file extension part of the file's name, without any leading dot
+ """
+
+ _, fileext = os.path.splitext(self.name)
+
+ return fileext.rstrip('.')
+
+ def matchext (self, ext_list) :
+ """
+ Tests if this file's extension is part of the recognized list of extensions
+ """
+
+ return (self.fileext.lower() in ext_list)
+
+class Directory (Node) :
+ """
+ A directory is a node that contains other nodes.
+ """
+
+ # a list of (test_func, node_type) tuples for use by children() to build subnodes with
+ NODE_TYPES = None
+
+ def subdir (self, name) :
+ """
+ Returns a Directory object representing the name underneath this dir
+ """
+
+ return Directory(self, name=name)
+
+ def subfile (self, name) :
+ """
+ Returns a File object representing the name underneath this dir
+ """
+
+ return Directory(self, name=name)
+
+ def listdir (self, skip_dotfiles=True) :
+ """
+ Yield a series of raw fsnames for nodes in this dir
+ """
+
+ # expressed
+ return (fsname for fsname in os.listdir(self.path) if not (skip_dotfiles and fsname.startswith('.')))
+
+ def child_nodes (self, skip_dotfiles=True) :
+ """
+ Yield a series of nodes contained in this dir
+ """
+
+ return (Node(self, fsname) for fsname in self.listdir(skip_dotfiles))
+
+ def __iter__ (self) :
+ """
+ Iterating over a Directory yields sub-Nodes.
+
+ Dotfiles are skipped.
+ """
+
+ return self.childnodes()
+
+ @property
+ def root_path (self) :
+ """
+ Build and return a relative path to the root of this dir tree
+ """
+
+ # build using parent root_path
+ return os.path.join('..', self.parent.root_path)
+
+ def children (self) :
+ """
+ Yield a series of Node subclasses representing the items in this dir.
+
+ This uses self.NODE_TYPES to figure out what kind of sub-node object to build. This should be a list of
+ (test_func, node_type)
+
+ tuples, of which the first is a function that takes a Node as it's sole argument, and returns a boolean.
+ For the first test_func which returns True, a Node-subclass object is constructed using node_type.from_node.
+ """
+
+ for node in self :
+ # figure out what type to use
+ for test_func, node_type in self.NODE_TYPES :
+ if test_func(node) :
+ # matches, build
+ yield node_type.from_node(node)
+
+ else :
+ # unknown file type!
+ raise Exception("unrecongized type of file: %s" % node);
+
+# assign default Directory.NODE_TYPES
+Directory.NODE_TYPES = [
+ (Node.is_dir, Directory),
+ (Node.is_file, File),
+]
+
+
+class Root (Directory) :
+ """
+ A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path.
+ """
+
+ def __init__ (self, fspath, config) :
+ """
+ Construct the directory tree root at the given 'real' path, which must be a raw str
+ """
+
+ # abuse Node's concept of a "name" a bit
+ super(Root, self).__init__(None, fspath, config=config)
+
+ @property
+ def path (self) :
+ """
+ Returns the raw path
+ """
+
+ return self.fsname
+
+ @property
+ def unicodepath (self) :
+ """
+ Returns the raw decoded path
+ """
+
+ return self.name
+
+ @property
+ def root_path (self) :
+ """
+ Returns an empty string representing this dir
+ """
+
+ return ''
+
+ def __repr__ (self) :
+ """
+ Override Node.__repr__ to not use self.parent.path
+ """
+
+ return "Root(%r)" % self.fsname
+