terom@51: """ terom@51: Filesystem path handling terom@51: """ terom@51: terom@55: import os, os.path, errno terom@60: import codecs, shutil terom@60: import itertools terom@60: terom@60: from utils import lazy_load terom@51: terom@51: class Node (object) : terom@51: """ terom@51: A filesystem object is basically just complicated representation of a path. terom@51: terom@51: On the plus side, it has a parent node and can handle unicode/binary paths. terom@51: """ terom@51: terom@51: # the binary name terom@51: fsname = None terom@51: terom@51: # the unicode name terom@51: name = None terom@51: terom@51: def decode_fsname (self, fsname) : terom@51: """ terom@51: Decode the given raw byte string representing a filesystem name into an user-readable unicode name. terom@51: terom@51: XXX: currently just hardcoded as utf-8 terom@51: """ terom@51: terom@51: return fsname.decode('utf-8', 'replace') terom@51: terom@51: def encode_name (self, name) : terom@51: """ terom@51: Returns a suitable fsname for the given unicode name or strict ASCII str terom@51: terom@51: XXX: currently just hardcoded as utf-8 terom@51: """ terom@51: terom@51: # this should fail for non-ASCII str terom@51: return name.encode('utf-8') terom@51: terom@51: @classmethod terom@51: def from_node (cls, node) : terom@51: """ terom@51: Construct from a Node object terom@51: """ terom@51: terom@51: return cls(node.parent, node.fsname, node.name, node.config) terom@51: terom@51: def __init__ (self, parent, fsname=None, name=None, config=None) : terom@51: """ terom@51: Initialize the node with a parent and both name/fsname. terom@51: terom@51: If not given, fsname is encoded from name, or name decoded from fsname, using encode/decode_name. terom@51: terom@51: If parent is given, but both fsname and name are None, then this node will be equal to the parent. terom@51: """ terom@51: terom@51: assert not fsname or isinstance(fsname, str) terom@51: terom@51: if parent and not fsname and not name : terom@51: # no name given -> we're the same as parent terom@51: self.parent, self.config, self.fsname, self.name = parent.parent, parent.config, parent.fsname, parent.name terom@51: terom@51: else : terom@51: # store terom@51: self.parent = parent terom@51: terom@51: # config, either as given, or copy from parent terom@51: if config : terom@51: self.config = config terom@76: terom@76: elif parent : # XXX: else : terom@51: self.config = parent.config terom@51: terom@51: # fsname terom@51: if fsname : terom@51: self.fsname = fsname terom@51: terom@51: else : terom@51: self.fsname = self.encode_name(name) terom@51: terom@51: # name terom@51: if name : terom@51: self.name = name terom@51: terom@51: else : terom@51: self.name = self.decode_fsname(fsname) terom@51: terom@51: def subnode (self, name) : terom@51: """ terom@51: Returns a Node object representing the given name behind this node. terom@51: terom@51: The name should either be a plain ASCII string or unicode object. terom@51: """ terom@51: terom@51: return Node(self, name=name) terom@51: terom@60: def nodepath (self) : terom@60: """ terom@60: Returns the path of nodes from this node to the root node, inclusive terom@60: """ terom@60: terom@60: return self.parent.nodepath() + [self] terom@60: terom@60: @lazy_load terom@51: def path (self) : terom@51: """ terom@60: Return the machine-readable root-path for this node terom@51: """ terom@51: terom@51: # build using parent path and our fsname terom@51: return os.path.join(self.parent.path, self.fsname) terom@51: terom@60: @lazy_load terom@51: def unicodepath (self) : terom@51: """ terom@60: Return the human-readable root-path for this node terom@51: """ terom@51: terom@51: # build using parent unicodepath and our name terom@51: return os.path.join(self.parent.path, self.name) terom@51: terom@60: def path_segments (self, unicode=True) : terom@60: """ terom@60: Return a series of single-level names describing the path from the root to this node terom@60: """ terom@60: terom@60: return self.parent.path_segments(unicode=unicode) + [self.name if unicode else self.fsname] terom@60: terom@51: def exists (self) : terom@51: """ terom@51: Tests if this node exists on the physical filesystem terom@51: """ terom@51: terom@51: return os.path.exists(self.path) terom@51: terom@51: def is_dir (self) : terom@51: """ terom@51: Tests if this node represents a directory on the filesystem terom@51: """ terom@51: terom@51: return os.path.isdir(self.path) terom@51: terom@51: def is_file (self) : terom@51: """ terom@51: Tests if this node represents a normal file on the filesystem terom@51: """ terom@51: terom@51: return os.path.isfile(self.path) terom@60: terom@60: def test (self) : terom@60: """ terom@60: Tests that this node exists. Raises an error it not, otherwise, returns the node itself terom@60: """ terom@60: terom@60: if not self.exists() : terom@60: raise Exception("Filesystem node does not exist: %s" % self) terom@60: terom@60: return self terom@51: terom@55: def path_from (self, node) : terom@55: """ terom@60: Returns a relative path to this node from the given node. terom@60: terom@60: This is the same as path_to, but just reversed. terom@60: """ terom@60: terom@60: return node.path_to(self) terom@60: terom@60: def path_to (self, node) : terom@60: """ terom@60: Returns a relative path from this node to the given node terom@55: """ terom@55: terom@60: # get real paths for both terom@60: from_path = self.nodepath() terom@60: to_path = node.nodepath() terom@60: pivot = None terom@60: terom@60: # reduce common prefix terom@60: while from_path[0] == to_path[0] : terom@60: from_path.pop(0) terom@60: pivot = to_path.pop(0) terom@60: terom@60: # build path terom@60: return Path(*itertools.chain(reversed(from_path), [pivot], to_path)) terom@55: terom@55: def stat (self, soft=False) : terom@55: """ terom@55: Returns the os.stat struct for this node. terom@55: terom@55: If `soft` is given, returns None if this node doesn't exist terom@55: """ terom@55: terom@55: try : terom@55: return os.stat(self.path) terom@55: terom@55: except OSError, e : terom@55: # trap ENOENT for soft terom@55: if soft and e.errno == errno.ENOENT : terom@55: return None terom@55: terom@55: else : terom@55: raise terom@60: terom@60: # alias str/unicode terom@76: __str__ = path terom@76: __unicode__ = unicodepath terom@51: terom@51: def __repr__ (self) : terom@51: """ terom@51: Returns a str representing this dir terom@51: """ terom@51: terom@51: return "Node(%r, %r)" % (self.parent.path, self.fsname) terom@51: terom@60: def __cmp__ (self, other) : terom@51: """ terom@51: Comparisons between Nodes terom@51: """ terom@51: terom@60: return cmp((self.parent, self.name), (other.parent if self.parent else None, other.name)) terom@60: terom@60: class Path (object) : terom@60: """ terom@60: A Path is a sequence of Nodes that form a path through the node tree. terom@60: terom@60: Each node must either be the parent or the child of the following node. terom@60: """ terom@60: terom@60: def __init__ (self, *nodes) : terom@60: """ terom@60: Initialize with the given node path terom@60: """ terom@60: terom@60: self.nodes = nodes terom@60: terom@60: def subpath (self, *nodes) : terom@60: """ terom@60: Returns a new path with the given node(s) appended terom@60: """ terom@60: terom@60: return Path(*itertools.chain(self.nodes, nodes)) terom@60: terom@60: def path_segments (self, unicode=True) : terom@60: """ terom@60: Yields a series of physical path segments for this path terom@60: """ terom@60: terom@60: prev = None terom@60: terom@60: for node in self.nodes : terom@60: if not prev : terom@60: # ignore terom@60: pass terom@60: terom@60: elif prev.parent and prev.parent == node : terom@60: # up a level terom@60: yield u'..' if unicode else '..' terom@60: terom@60: else : terom@60: # down a level terom@60: yield node.name if unicode else node.fsname terom@60: terom@60: # chained together terom@60: prev = node terom@60: terom@60: def __iter__ (self) : terom@60: """ terom@60: Iterate over the nodes terom@60: """ terom@60: terom@60: return iter(self.nodes) terom@60: terom@60: def __unicode__ (self) : terom@60: """ terom@60: Returns the unicode human-readable path terom@60: """ terom@60: terom@60: return os.path.join(*self.path_segments(unicode=True)) terom@60: terom@60: def __str__ (self) : terom@60: """ terom@60: Returns the binary machine-readable path terom@60: """ terom@60: terom@60: return os.path.join(*self.path_segments(unicode=False)) terom@60: terom@60: def __repr__ (self) : terom@60: return "Path(%s)" % ', '.join(repr(segment) for segment in self.path_segments(unicode=False)) terom@51: terom@51: class File (Node) : terom@51: """ terom@51: A file. Simple, eh? terom@51: """ terom@51: terom@51: @property terom@51: def basename (self) : terom@51: """ terom@51: Returns the "base" part of this file's name, i.e. the filename without the extension terom@51: """ terom@51: terom@51: basename, _ = os.path.splitext(self.name) terom@51: terom@51: return basename terom@51: terom@51: @property terom@51: def fileext (self) : terom@51: """ terom@51: Returns the file extension part of the file's name, without any leading dot terom@51: """ terom@51: terom@51: _, fileext = os.path.splitext(self.name) terom@51: terom@51: return fileext.rstrip('.') terom@51: terom@51: def matchext (self, ext_list) : terom@51: """ terom@51: Tests if this file's extension is part of the recognized list of extensions terom@51: """ terom@51: terom@51: return (self.fileext.lower() in ext_list) terom@55: terom@60: def test (self) : terom@60: """ terom@60: Tests that this file exists as a file. Raises an error it not, otherwise, returns itself terom@60: """ terom@60: terom@60: if not self.is_file() : terom@60: raise Exception("File does not exist: %s" % self) terom@60: terom@60: return self terom@60: terom@55: def open (self, mode='r', encoding=None, errors=None, bufsize=None) : terom@55: """ terom@60: Wrapper for open/codecs.open. terom@60: terom@60: Raises an error if read_only mode is set and mode contains any of 'wa+' terom@55: """ terom@55: terom@60: if self.config.read_only and any((c in mode) for c in 'wa+') : terom@60: raise Exception("Unable to open file for %s due to read_only mode: %s" % (mode, self)) terom@60: terom@55: if encoding : terom@55: return codecs.open(self.path, mode, encoding, errors, bufsize) terom@55: terom@55: else : terom@55: return open(self.path, mode, bufsize) terom@51: terom@68: def open_write (self, encoding=None, errors=None, bufsize=None) : terom@68: """ terom@68: Open for write using open('w'). terom@68: """ terom@68: terom@68: return self.open('w', encoding, errors, bufsize) terom@68: terom@60: def copy_from (self, file) : terom@60: """ terom@60: Replace this file with a copy of the given file with default permissions. terom@60: terom@60: Raises an error if read_only mode is set. terom@60: terom@60: XXX: accept mode terom@60: """ terom@60: terom@60: if self.config.read_only : terom@60: raise Exception("Not copying file as read_only mode is set: %s -> %s" % (file, self)) terom@60: terom@60: # perform the copy terom@60: shutil.copyfile(file.path, self.path) terom@60: terom@51: class Directory (Node) : terom@51: """ terom@51: A directory is a node that contains other nodes. terom@51: """ terom@51: terom@51: # a list of (test_func, node_type) tuples for use by children() to build subnodes with terom@51: NODE_TYPES = None terom@51: terom@55: def subdir (self, name, create=False) : terom@51: """ terom@55: Returns a Directory object representing the name underneath this dir. terom@55: terom@60: If the create option is given, the directory will be created if it does not exist. Note that this will terom@60: raise an error if read_only mode is set terom@51: """ terom@51: terom@60: subdir = Directory(self, name=name) terom@68: terom@60: if create and not subdir.is_dir() : terom@60: # create it! terom@60: subdir.mkdir() terom@60: terom@68: return dir terom@51: terom@68: def test_subdir (self, name, create=False) : terom@68: """ terom@68: Test for the presence of a subdir with the given name, and possibly return it, or None. terom@68: terom@68: Returns a (exists, created, dir) tuple. terom@68: terom@68: XXX: ugly, not used terom@68: """ terom@68: terom@68: subdir = Directory(self, name=name) terom@68: terom@68: # already exists? terom@68: if subdir.is_dir() : terom@68: if create : terom@68: # create it! terom@68: subdir.mkdir() terom@68: terom@68: # didn't exist, did create terom@68: return True, True, subdir terom@68: terom@68: else : terom@68: # doesn't exist, don't create terom@68: return False, False, subdir terom@68: terom@68: else : terom@68: # already existing terom@68: return True, False, subdir terom@68: terom@68: terom@51: def subfile (self, name) : terom@51: """ terom@51: Returns a File object representing the name underneath this dir terom@51: """ terom@51: terom@51: return Directory(self, name=name) terom@51: terom@60: def test (self) : terom@60: """ terom@60: Tests that this dir exists as a dir. Raises an error it not, otherwise, returns itself terom@60: """ terom@60: terom@60: if not self.is_dir() : terom@60: raise Exception("Directory does not exist: %s" % self) terom@60: terom@60: return self terom@60: terom@55: def mkdir (self) : terom@55: """ terom@60: Create this directory with default permissions. terom@60: terom@60: This will fail if read_only mode is set terom@55: terom@55: XXX: mode argument terom@55: """ terom@55: terom@60: if self.config.read_only : terom@60: # forbidden terom@60: raise Exception("Unable to create dir due to read_only mode: %s" % self) terom@60: terom@55: # do it terom@55: os.mkdir(self.path) terom@55: terom@51: def listdir (self, skip_dotfiles=True) : terom@51: """ terom@51: Yield a series of raw fsnames for nodes in this dir terom@51: """ terom@51: terom@51: # expressed terom@51: return (fsname for fsname in os.listdir(self.path) if not (skip_dotfiles and fsname.startswith('.'))) terom@51: terom@55: def subnodes (self, skip_dotfiles=True, sort=True) : terom@51: """ terom@55: Yield a series of Nodes contained in this dir. terom@55: terom@55: If skip_dotfiles is given, nodes that begin with a . are omitted. terom@55: terom@55: If `sort` is given, the returned nodes will be in sorted order. terom@51: """ terom@51: terom@55: iter = (Node(self, fsname) for fsname in self.listdir(skip_dotfiles)) terom@55: terom@55: if sort : terom@55: return sorted(iter) terom@55: terom@55: else : terom@55: return iter terom@51: terom@51: def __iter__ (self) : terom@51: """ terom@51: Iterating over a Directory yields sub-Nodes. terom@51: terom@51: Dotfiles are skipped. terom@51: """ terom@51: terom@55: return self.subnodes() terom@51: terom@51: @property terom@51: def root_path (self) : terom@51: """ terom@51: Build and return a relative path to the root of this dir tree terom@55: terom@55: XXX: move to node terom@51: """ terom@51: terom@51: # build using parent root_path terom@51: return os.path.join('..', self.parent.root_path) terom@51: terom@51: def children (self) : terom@51: """ terom@51: Yield a series of Node subclasses representing the items in this dir. terom@51: terom@51: This uses self.NODE_TYPES to figure out what kind of sub-node object to build. This should be a list of terom@51: (test_func, node_type) terom@51: terom@51: tuples, of which the first is a function that takes a Node as it's sole argument, and returns a boolean. terom@51: For the first test_func which returns True, a Node-subclass object is constructed using node_type.from_node. terom@55: terom@55: XXX: never used terom@51: """ terom@51: terom@51: for node in self : terom@51: # figure out what type to use terom@51: for test_func, node_type in self.NODE_TYPES : terom@51: if test_func(node) : terom@51: # matches, build terom@51: yield node_type.from_node(node) terom@51: terom@51: else : terom@51: # unknown file type! terom@51: raise Exception("unrecongized type of file: %s" % node); terom@51: terom@51: # assign default Directory.NODE_TYPES terom@51: Directory.NODE_TYPES = [ terom@51: (Node.is_dir, Directory), terom@51: (Node.is_file, File), terom@51: ] terom@51: terom@51: terom@51: class Root (Directory) : terom@51: """ terom@51: A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path. terom@51: """ terom@51: terom@60: # XXX: config needs a default terom@60: def __init__ (self, fspath, config=None) : terom@51: """ terom@51: Construct the directory tree root at the given 'real' path, which must be a raw str terom@51: """ terom@51: terom@51: # abuse Node's concept of a "name" a bit terom@76: super(Root, self).__init__(None, fspath) terom@76: terom@76: # store our config terom@76: self.config = config terom@51: terom@60: def nodepath (self) : terom@60: """ terom@60: Just return ourself terom@60: """ terom@60: terom@60: return [self] terom@60: terom@51: @property terom@51: def path (self) : terom@51: """ terom@51: Returns the raw path terom@51: """ terom@51: terom@51: return self.fsname terom@51: terom@51: @property terom@51: def unicodepath (self) : terom@51: """ terom@51: Returns the raw decoded path terom@51: """ terom@51: terom@51: return self.name terom@51: terom@51: @property terom@51: def root_path (self) : terom@51: """ terom@51: Returns an empty string representing this dir terom@51: """ terom@51: terom@51: return '' terom@51: terom@60: def path_segments (self, unicode=True) : terom@60: """ terom@60: No path segments terom@60: """ terom@60: terom@60: return [] terom@60: terom@51: def __repr__ (self) : terom@51: """ terom@51: Override Node.__repr__ to not use self.parent.path terom@51: """ terom@51: terom@51: return "Root(%r)" % self.fsname terom@51: