--- a/degal/filesystem.py Fri Jun 05 19:30:53 2009 +0300
+++ b/degal/filesystem.py Fri Jun 05 21:46:43 2009 +0300
@@ -3,7 +3,10 @@
"""
import os, os.path, errno
-import codecs
+import codecs, shutil
+import itertools
+
+from utils import lazy_load
class Node (object) :
"""
@@ -94,24 +97,38 @@
return Node(self, name=name)
- @property
+ def nodepath (self) :
+ """
+ Returns the path of nodes from this node to the root node, inclusive
+ """
+
+ return self.parent.nodepath() + [self]
+
+ @lazy_load
def path (self) :
"""
- Build and return the real filesystem path for this node
+ Return the machine-readable root-path for this node
"""
# build using parent path and our fsname
return os.path.join(self.parent.path, self.fsname)
- @property
+ @lazy_load
def unicodepath (self) :
"""
- Build and return the fake unicode filesystem path for this node
+ Return the human-readable root-path for this node
"""
# build using parent unicodepath and our name
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 self.parent.path_segments(unicode=unicode) + [self.name if unicode else self.fsname]
+
def exists (self) :
"""
Tests if this node exists on the physical filesystem
@@ -132,13 +149,43 @@
"""
return os.path.isfile(self.path)
+
+ def test (self) :
+ """
+ Tests that this node exists. Raises an error it not, otherwise, returns the node itself
+ """
+
+ if not self.exists() :
+ raise Exception("Filesystem node does not exist: %s" % self)
+
+ return self
def path_from (self, node) :
"""
- Returns a relative path to this node from the given node
+ Returns a relative path to this node from the given node.
+
+ This is the same as path_to, but just reversed.
+ """
+
+ return node.path_to(self)
+
+ def path_to (self, node) :
+ """
+ Returns a relative path from this node to the given node
"""
- XXX
+ # 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) :
"""
@@ -157,20 +204,10 @@
else :
raise
-
- def __str__ (self) :
- """
- Returns the raw filesystem path
- """
-
- return self.path
-
- def __unicode__ (self) :
- """
- Returns the human-readable path
- """
-
- return self.unicodepath
+
+ # alias str/unicode
+ str = path
+ unicode = unicodepath
def __repr__ (self) :
"""
@@ -179,12 +216,80 @@
return "Node(%r, %r)" % (self.parent.path, self.fsname)
- def __cmp__ (self) :
+ def __cmp__ (self, other) :
"""
Comparisons between Nodes
"""
- return cmp((self.parent, self.name), (other.parent, other.name))
+ 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.
+
+ Each node must either be the parent or the child of the following node.
+ """
+
+ def __init__ (self, *nodes) :
+ """
+ Initialize with the given node path
+ """
+
+ self.nodes = nodes
+
+ def subpath (self, *nodes) :
+ """
+ Returns a new path with the given node(s) appended
+ """
+
+ return Path(*itertools.chain(self.nodes, nodes))
+
+ def path_segments (self, unicode=True) :
+ """
+ Yields a series of physical path segments for this path
+ """
+
+ prev = None
+
+ for node in self.nodes :
+ if not prev :
+ # ignore
+ pass
+
+ elif prev.parent and prev.parent == node :
+ # up a level
+ yield u'..' if unicode else '..'
+
+ else :
+ # down a level
+ yield node.name if unicode else node.fsname
+
+ # chained together
+ prev = node
+
+ def __iter__ (self) :
+ """
+ Iterate over the nodes
+ """
+
+ return iter(self.nodes)
+
+ def __unicode__ (self) :
+ """
+ Returns the unicode human-readable path
+ """
+
+ return os.path.join(*self.path_segments(unicode=True))
+
+ def __str__ (self) :
+ """
+ Returns the binary machine-readable path
+ """
+
+ return os.path.join(*self.path_segments(unicode=False))
+
+ def __repr__ (self) :
+ return "Path(%s)" % ', '.join(repr(segment) for segment in self.path_segments(unicode=False))
class File (Node) :
"""
@@ -218,17 +323,47 @@
return (self.fileext.lower() in ext_list)
+ def test (self) :
+ """
+ Tests that this file exists as a file. Raises an error it not, otherwise, returns itself
+ """
+
+ if not self.is_file() :
+ raise Exception("File does not exist: %s" % self)
+
+ return self
+
def open (self, mode='r', encoding=None, errors=None, bufsize=None) :
"""
- Wrapper for open/codecs.open
+ Wrapper for open/codecs.open.
+
+ Raises an error if read_only mode is set and mode contains any of 'wa+'
"""
+ if self.config.read_only and any((c in mode) for c in 'wa+') :
+ raise Exception("Unable to open file for %s due to read_only mode: %s" % (mode, self))
+
if encoding :
return codecs.open(self.path, mode, encoding, errors, bufsize)
else :
return open(self.path, mode, bufsize)
+ def copy_from (self, file) :
+ """
+ Replace this file with a copy of the given file with default permissions.
+
+ Raises an error if read_only mode is set.
+
+ XXX: accept mode
+ """
+
+ if self.config.read_only :
+ raise Exception("Not copying file as read_only mode is set: %s -> %s" % (file, self))
+
+ # perform the copy
+ shutil.copyfile(file.path, self.path)
+
class Directory (Node) :
"""
A directory is a node that contains other nodes.
@@ -241,14 +376,18 @@
"""
Returns a Directory object representing the name underneath this dir.
- If the create option is given, the directory will be created if it does not exist.
+ If the create option is given, the directory will be created if it does not exist. Note that this will
+ raise an error if read_only mode is set
"""
- dir = Directory(self, name=name)
+ subdir = Directory(self, name=name)
# try and mkdir?
- if create and not dir.is_dir() :
- dir.mkdir()
+ if create and not subdir.is_dir() :
+ # create it!
+ subdir.mkdir()
+
+ return subdir
def subfile (self, name) :
"""
@@ -257,13 +396,29 @@
return Directory(self, name=name)
+ def test (self) :
+ """
+ Tests that this dir exists as a dir. Raises an error it not, otherwise, returns itself
+ """
+
+ if not self.is_dir() :
+ raise Exception("Directory does not exist: %s" % self)
+
+ return self
+
def mkdir (self) :
"""
- Create this directory with default permissions
+ Create this directory with default permissions.
+
+ This will fail if read_only mode is set
XXX: mode argument
"""
+ if self.config.read_only :
+ # forbidden
+ raise Exception("Unable to create dir due to read_only mode: %s" % self)
+
# do it
os.mkdir(self.path)
@@ -348,7 +503,8 @@
A special Directory that overrides the Node methods to anchor the recursion/etc at some 'real' filesystem path.
"""
- def __init__ (self, fspath, config) :
+ # XXX: config needs a default
+ def __init__ (self, fspath, config=None) :
"""
Construct the directory tree root at the given 'real' path, which must be a raw str
"""
@@ -356,6 +512,13 @@
# abuse Node's concept of a "name" a bit
super(Root, self).__init__(None, fspath, config=config)
+ def nodepath (self) :
+ """
+ Just return ourself
+ """
+
+ return [self]
+
@property
def path (self) :
"""
@@ -380,6 +543,13 @@
return ''
+ def path_segments (self, unicode=True) :
+ """
+ No path segments
+ """
+
+ return []
+
def __repr__ (self) :
"""
Override Node.__repr__ to not use self.parent.path