diff --git a/IPython/external/path/_path.py b/IPython/external/path/_path.py index 676d01e..aa666ef 100644 --- a/IPython/external/path/_path.py +++ b/IPython/external/path/_path.py @@ -1,56 +1,149 @@ +# +# Copyright (c) 2010 Mikhail Gusarov +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + """ path.py - An object representing a path to a file or directory. -Example: +Original author: + Jason Orendorff + +Current maintainer: + Jason R. Coombs -from IPython.external.path import path -d = path('/home/guido/bin') -for f in d.files('*.py'): - f.chmod(0755) +Contributors: + Mikhail Gusarov + Marc Abramowitz + Jason R. Coombs + Jason Chu + Vojislav Stojkovic -This module requires Python 2.5 or later. +Example:: + from path import path + d = path('/home/guido/bin') + for f in d.files('*.py'): + f.chmod(0755) -URL: http://pypi.python.org/pypi/path.py -Author: Jason Orendorff (and others - see the url!) -Date: 9 Mar 2007 +path.py requires Python 2.5 or later. """ +from __future__ import with_statement + +import sys +import warnings +import os +import fnmatch +import glob +import shutil +import codecs +import hashlib +import errno +import tempfile +import functools +import operator +import re + +try: + import win32security +except ImportError: + pass -# TODO -# - Tree-walking functions don't avoid symlink loops. Matt Harrison -# sent me a patch for this. -# - Bug in write_text(). It doesn't support Universal newline mode. -# - Better error message in listdir() when self isn't a -# directory. (On Windows, the error message really sucks.) -# - Make sure everything has a good docstring. -# - Add methods for regex find and replace. -# - guess_content_type() method? -# - Perhaps support arguments to touch(). +try: + import pwd +except ImportError: + pass -from __future__ import generators +################################ +# Monkey patchy python 3 support +try: + basestring +except NameError: + basestring = str + +try: + unicode +except NameError: + unicode = str + +try: + os.getcwdu +except AttributeError: + os.getcwdu = os.getcwd + +if sys.version < '3': + def u(x): + return codecs.unicode_escape_decode(x)[0] +else: + def u(x): + return x -import sys, warnings, os, fnmatch, glob, shutil, codecs -from hashlib import md5 +o777 = 511 +o766 = 502 +o666 = 438 +o554 = 364 +################################ -__version__ = '2.2' +__version__ = '4.3' __all__ = ['path'] -# Platform-specific support for path.owner -if os.name == 'nt': - try: - import win32security - except ImportError: - win32security = None -else: - try: - import pwd - except ImportError: - pwd = None - class TreeWalkWarning(Warning): pass + +def simple_cache(func): + """ + Save results for the 'using_module' classmethod. + When Python 3.2 is available, use functools.lru_cache instead. + """ + saved_results = {} + + def wrapper(cls, module): + if module in saved_results: + return saved_results[module] + saved_results[module] = func(cls, module) + return saved_results[module] + return wrapper + + +class ClassProperty(property): + def __get__(self, cls, owner): + return self.fget.__get__(None, owner)() + + +class multimethod(object): + """ + Acts like a classmethod when invoked from the class and like an + instancemethod when invoked from the instance. + """ + def __init__(self, func): + self.func = func + + def __get__(self, instance, owner): + return ( + functools.partial(self.func, owner) if instance is None + else functools.partial(self.func, owner, instance) + ) + + class path(unicode): """ Represents a filesystem path. @@ -58,26 +151,45 @@ class path(unicode): counterparts in os.path. """ + module = os.path + "The path module to use for path operations." + + def __init__(self, other=''): + if other is None: + raise TypeError("Invalid initial value for path: None") + + @classmethod + @simple_cache + def using_module(cls, module): + subclass_name = cls.__name__ + '_' + module.__name__ + bases = (cls,) + ns = {'module': module} + return type(subclass_name, bases, ns) + + @ClassProperty + @classmethod + def _next_class(cls): + """ + What class should be used to construct new instances from this class + """ + return cls + # --- Special Python methods. def __repr__(self): - return 'path(%s)' % unicode.__repr__(self) + return '%s(%s)' % (type(self).__name__, super(path, self).__repr__()) # Adding a path and a string yields a path. def __add__(self, more): try: - resultStr = unicode.__add__(self, more) - except TypeError: #Python bug - resultStr = NotImplemented - if resultStr is NotImplemented: - return resultStr - return self.__class__(resultStr) + return self._next_class(super(path, self).__add__(more)) + except TypeError: # Python bug + return NotImplemented def __radd__(self, other): - if isinstance(other, basestring): - return self.__class__(other.__add__(self)) - else: + if not isinstance(other, basestring): return NotImplemented + return self._next_class(other.__add__(self)) # The / operator joins paths. def __div__(self, rel): @@ -86,28 +198,50 @@ class path(unicode): Join two path components, adding a separator character if needed. """ - return self.__class__(os.path.join(self, rel)) + return self._next_class(self.module.join(self, rel)) # Make the / operator work even when true division is enabled. __truediv__ = __div__ + def __enter__(self): + self._old_dir = self.getcwd() + os.chdir(self) + return self + + def __exit__(self, *_): + os.chdir(self._old_dir) + + @classmethod def getcwd(cls): """ Return the current working directory as a path object. """ return cls(os.getcwdu()) - getcwd = classmethod(getcwd) - + # # --- Operations on path strings. - def isabs(s): return os.path.isabs(s) - def abspath(self): return self.__class__(os.path.abspath(self)) - def normcase(self): return self.__class__(os.path.normcase(self)) - def normpath(self): return self.__class__(os.path.normpath(self)) - def realpath(self): return self.__class__(os.path.realpath(self)) - def expanduser(self): return self.__class__(os.path.expanduser(self)) - def expandvars(self): return self.__class__(os.path.expandvars(self)) - def dirname(self): return self.__class__(os.path.dirname(self)) - def basename(s): return os.path.basename(s) + def abspath(self): + return self._next_class(self.module.abspath(self)) + + def normcase(self): + return self._next_class(self.module.normcase(self)) + + def normpath(self): + return self._next_class(self.module.normpath(self)) + + def realpath(self): + return self._next_class(self.module.realpath(self)) + + def expanduser(self): + return self._next_class(self.module.expanduser(self)) + + def expandvars(self): + return self._next_class(self.module.expandvars(self)) + + def dirname(self): + return self._next_class(self.module.dirname(self)) + + def basename(self): + return self._next_class(self.module.basename(self)) def expand(self): """ Clean up a filename by calling expandvars(), @@ -118,23 +252,36 @@ class path(unicode): """ return self.expandvars().expanduser().normpath() - def _get_namebase(self): - base, ext = os.path.splitext(self.name) + @property + def namebase(self): + """ The same as path.name, but with one file extension stripped off. + + For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', + but path('/home/guido/python.tar.gz').namebase == 'python.tar' + """ + base, ext = self.module.splitext(self.name) return base - def _get_ext(self): - f, ext = os.path.splitext(unicode(self)) + @property + def ext(self): + """ The file extension, for example '.py'. """ + f, ext = self.module.splitext(self) return ext - def _get_drive(self): - drive, r = os.path.splitdrive(self) - return self.__class__(drive) + @property + def drive(self): + """ The drive specifier, for example 'C:'. + This is always empty on systems that don't use drive specifiers. + """ + drive, r = self.module.splitdrive(self) + return self._next_class(drive) parent = property( dirname, None, None, """ This path's parent directory, as a new path object. - For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') + For example, + path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') """) name = property( @@ -144,28 +291,10 @@ class path(unicode): For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' """) - namebase = property( - _get_namebase, None, None, - """ The same as path.name, but with one file extension stripped off. - - For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', - but path('/home/guido/python.tar.gz').namebase == 'python.tar' - """) - - ext = property( - _get_ext, None, None, - """ The file extension, for example '.py'. """) - - drive = property( - _get_drive, None, None, - """ The drive specifier, for example 'C:'. - This is always empty on systems that don't use drive specifiers. - """) - def splitpath(self): """ p.splitpath() -> Return (p.parent, p.name). """ - parent, child = os.path.split(self) - return self.__class__(parent), child + parent, child = self.module.split(self) + return self._next_class(parent), child def splitdrive(self): """ p.splitdrive() -> Return (p.drive, ). @@ -174,8 +303,8 @@ class path(unicode): no drive specifier, p.drive is empty, so the return value is simply (path(''), p). This is always the case on Unix. """ - drive, rel = os.path.splitdrive(self) - return self.__class__(drive), rel + drive, rel = self.module.splitdrive(self) + return self._next_class(drive), rel def splitext(self): """ p.splitext() -> Return (p.stripext(), p.ext). @@ -187,8 +316,8 @@ class path(unicode): last path segment. This has the property that if (a, b) == p.splitext(), then a + b == p. """ - filename, ext = os.path.splitext(self) - return self.__class__(filename), ext + filename, ext = self.module.splitext(self) + return self._next_class(filename), ext def stripext(self): """ p.stripext() -> Remove one file extension from the path. @@ -198,36 +327,39 @@ class path(unicode): """ return self.splitext()[0] - if hasattr(os.path, 'splitunc'): - def splitunc(self): - unc, rest = os.path.splitunc(self) - return self.__class__(unc), rest - - def _get_uncshare(self): - unc, r = os.path.splitunc(self) - return self.__class__(unc) + def splitunc(self): + unc, rest = self.module.splitunc(self) + return self._next_class(unc), rest - uncshare = property( - _get_uncshare, None, None, - """ The UNC mount point for this path. - This is empty for paths on local drives. """) + @property + def uncshare(self): + """ + The UNC mount point for this path. + This is empty for paths on local drives. + """ + unc, r = self.module.splitunc(self) + return self._next_class(unc) - def joinpath(self, *args): - """ Join two or more path components, adding a separator - character (os.sep) if needed. Returns a new path - object. + @multimethod + def joinpath(cls, first, *others): + """ + Join first to zero or more path components, adding a separator + character (first.module.sep) if needed. Returns a new instance of + first._next_class. """ - return self.__class__(os.path.join(self, *args)) + if not isinstance(first, cls): + first = cls(first) + return first._next_class(first.module.join(first, *others)) def splitall(self): r""" Return a list of the path components in this path. The first item in the list will be a path. Its value will be either os.curdir, os.pardir, empty, or the root directory of - this path (for example, '/' or 'C:\\'). The other items in + this path (for example, ``'/'`` or ``'C:\\'``). The other items in the list will be strings. - path.path.joinpath(*result) will yield the original path. + ``path.path.joinpath(*result)`` will yield the original path. """ parts = [] loc = self @@ -241,11 +373,11 @@ class path(unicode): parts.reverse() return parts - def relpath(self): + def relpath(self, start='.'): """ Return this path as a relative path, - based from the current working directory. + based from start, which defaults to the current working directory. """ - cwd = self.__class__(os.getcwdu()) + cwd = self._next_class(start) return cwd.relpathto(self) def relpathto(self, dest): @@ -256,20 +388,20 @@ class path(unicode): dest.abspath(). """ origin = self.abspath() - dest = self.__class__(dest).abspath() + dest = self._next_class(dest).abspath() orig_list = origin.normcase().splitall() # Don't normcase dest! We want to preserve the case. dest_list = dest.splitall() - if orig_list[0] != os.path.normcase(dest_list[0]): + if orig_list[0] != self.module.normcase(dest_list[0]): # Can't get here from there. return dest # Find the location where the two paths start to differ. i = 0 for start_seg, dest_seg in zip(orig_list, dest_list): - if start_seg != os.path.normcase(dest_seg): + if start_seg != self.module.normcase(dest_seg): break i += 1 @@ -283,8 +415,8 @@ class path(unicode): # If they happen to be identical, use os.curdir. relpath = os.curdir else: - relpath = os.path.join(*segments) - return self.__class__(relpath) + relpath = self.module.join(*segments) + return self._next_class(relpath) # --- Listing, searching, walking, and matching @@ -313,7 +445,7 @@ class path(unicode): With the optional 'pattern' argument, this only lists directories whose names match the given pattern. For - example, d.dirs('build-*'). + example, ``d.dirs('build-*')``. """ return [p for p in self.listdir(pattern) if p.isdir()] @@ -325,9 +457,9 @@ class path(unicode): With the optional 'pattern' argument, this only lists files whose names match the given pattern. For example, - d.files('*.pyc'). + ``d.files('*.pyc')``. """ - + return [p for p in self.listdir(pattern) if p.isfile()] def walk(self, pattern=None, errors='strict'): @@ -388,7 +520,7 @@ class path(unicode): With the optional 'pattern' argument, this yields only directories whose names match the given pattern. For - example, mydir.walkdirs('*test') yields only directories + example, ``mydir.walkdirs('*test')`` yields only directories with names ending in 'test'. The errors= keyword argument controls behavior when an @@ -424,7 +556,7 @@ class path(unicode): The optional argument, pattern, limits the results to files with names that match the pattern. For example, - mydir.walkfiles('*.tmp') yields only files with the .tmp + ``mydir.walkfiles('*.tmp')`` yields only files with the .tmp extension. """ if errors not in ('strict', 'warn', 'ignore'): @@ -471,7 +603,7 @@ class path(unicode): """ Return True if self.name matches the given pattern. pattern - A filename pattern with wildcards, - for example '*.py'. + for example ``'*.py'``. """ return fnmatch.fnmatch(self.name, pattern) @@ -483,23 +615,40 @@ class path(unicode): For example, path('/users').glob('*/bin/*') returns a list of all the files users have in their bin directories. """ - cls = self.__class__ - return [cls(s) for s in glob.glob(unicode(self / pattern))] - + cls = self._next_class + return [cls(s) for s in glob.glob(self / pattern)] + # # --- Reading or writing an entire file at once. - def open(self, mode='r'): + def open(self, *args, **kwargs): """ Open this file. Return a file object. """ - return open(self, mode) + return open(self, *args, **kwargs) def bytes(self): """ Open this file, read all bytes, return them as a string. """ - f = self.open('rb') - try: + with self.open('rb') as f: return f.read() - finally: - f.close() + + def chunks(self, size, *args, **kwargs): + """ Returns a generator yielding chunks of the file, so it can + be read piece by piece with a simple for loop. + + Any argument you pass after `size` will be passed to `open()`. + + :example: + + >>> for chunk in path("file.txt").chunk(8192): + ... print(chunk) + + This will read the file by chunks of 8192 bytes. + """ + with open(self, *args, **kwargs) as f: + while True: + d = f.read(size) + if not d: + break + yield d def write_bytes(self, bytes, append=False): """ Open this file and write the given bytes to it. @@ -511,17 +660,14 @@ class path(unicode): mode = 'ab' else: mode = 'wb' - f = self.open(mode) - try: + with self.open(mode) as f: f.write(bytes) - finally: - f.close() def text(self, encoding=None, errors='strict'): r""" Open this file, read it in, return the content as a string. - This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' - are automatically translated to '\n'. + This method uses 'U' mode, so '\r\n' and '\r' are automatically + translated to '\n'. Optional arguments: @@ -534,27 +680,22 @@ class path(unicode): """ if encoding is None: # 8-bit - f = self.open('U') - try: + with self.open('U') as f: return f.read() - finally: - f.close() else: # Unicode - f = codecs.open(self, 'r', encoding, errors) - # (Note - Can't use 'U' mode here, since codecs.open - # doesn't support 'U' mode, even in Python 2.3.) - try: + with codecs.open(self, 'r', encoding, errors) as f: + # (Note - Can't use 'U' mode here, since codecs.open + # doesn't support 'U' mode.) t = f.read() - finally: - f.close() - return (t.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - - def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): + return (t.replace(u('\r\n'), u('\n')) + .replace(u('\r\x85'), u('\n')) + .replace(u('\r'), u('\n')) + .replace(u('\x85'), u('\n')) + .replace(u('\u2028'), u('\n'))) + + def write_text(self, text, encoding=None, errors='strict', + linesep=os.linesep, append=False): r""" Write the given text to this file. The default behavior is to overwrite any existing file; @@ -622,12 +763,12 @@ class path(unicode): if linesep is not None: # Convert all standard end-of-line sequences to # ordinary newline characters. - text = (text.replace(u'\r\n', u'\n') - .replace(u'\r\x85', u'\n') - .replace(u'\r', u'\n') - .replace(u'\x85', u'\n') - .replace(u'\u2028', u'\n')) - text = text.replace(u'\n', linesep) + text = (text.replace(u('\r\n'), u('\n')) + .replace(u('\r\x85'), u('\n')) + .replace(u('\r'), u('\n')) + .replace(u('\x85'), u('\n')) + .replace(u('\u2028'), u('\n'))) + text = text.replace(u('\n'), linesep) if encoding is None: encoding = sys.getdefaultencoding() bytes = text.encode(encoding, errors) @@ -658,14 +799,11 @@ class path(unicode): translated to '\n'. If false, newline characters are stripped off. Default is True. - This uses 'U' mode in Python 2.3 and later. + This uses 'U' mode. """ if encoding is None and retain: - f = self.open('U') - try: + with self.open('U') as f: return f.readlines() - finally: - f.close() else: return self.text(encoding, errors).splitlines(retain) @@ -707,18 +845,17 @@ class path(unicode): mode = 'ab' else: mode = 'wb' - f = self.open(mode) - try: + with self.open(mode) as f: for line in lines: isUnicode = isinstance(line, unicode) if linesep is not None: # Strip off any existing line-end and add the # specified linesep string. if isUnicode: - if line[-2:] in (u'\r\n', u'\x0d\x85'): + if line[-2:] in (u('\r\n'), u('\x0d\x85')): line = line[:-2] - elif line[-1:] in (u'\r', u'\n', - u'\x85', u'\u2028'): + elif line[-1:] in (u('\r'), u('\n'), + u('\x85'), u('\u2028')): line = line[:-1] else: if line[-2:] == '\r\n': @@ -731,57 +868,91 @@ class path(unicode): encoding = sys.getdefaultencoding() line = line.encode(encoding, errors) f.write(line) - finally: - f.close() def read_md5(self): """ Calculate the md5 hash for this file. This reads through the entire file. """ - f = self.open('rb') - try: - m = md5() - while True: - d = f.read(8192) - if not d: - break - m.update(d) - finally: - f.close() - return m.digest() + return self.read_hash('md5') + + def _hash(self, hash_name): + """ Returns a hash object for the file at the current path. + + `hash_name` should be a hash algo name such as 'md5' or 'sha1' + that's available in the `hashlib` module. + """ + m = hashlib.new(hash_name) + for chunk in self.chunks(8192): + m.update(chunk) + return m + + def read_hash(self, hash_name): + """ Calculate given hash for this file. + + List of supported hashes can be obtained from hashlib package. This + reads the entire file. + """ + return self._hash(hash_name).digest() + + def read_hexhash(self, hash_name): + """ Calculate given hash for this file, returning hexdigest. + + List of supported hashes can be obtained from hashlib package. This + reads the entire file. + """ + return self._hash(hash_name).hexdigest() # --- Methods for querying the filesystem. - # N.B. We can't assign the functions directly, because they may on some - # platforms be implemented in C, and compiled functions don't get bound. - # See gh-737 for discussion of this. + # N.B. On some platforms, the os.path functions may be implemented in C + # (e.g. isdir on Windows, Python 3.2.2), and compiled functions don't get + # bound. Playing it safe and wrapping them all in method calls. - def exists(s): return os.path.exists(s) - def isdir(s): return os.path.isdir(s) - def isfile(s): return os.path.isfile(s) - def islink(s): return os.path.islink(s) - def ismount(s): return os.path.ismount(s) + def isabs(self): + return self.module.isabs(self) - if hasattr(os.path, 'samefile'): - def samefile(s, o): return os.path.samefile(s, o) + def exists(self): + return self.module.exists(self) + + def isdir(self): + return self.module.isdir(self) + + def isfile(self): + return self.module.isfile(self) + + def islink(self): + return self.module.islink(self) + + def ismount(self): + return self.module.ismount(self) + + def samefile(self, other): + return self.module.samefile(self, other) + + def getatime(self): + return self.module.getatime(self) - def getatime(s): return os.path.getatime(s) atime = property( getatime, None, None, """ Last access time of the file. """) - def getmtime(s): return os.path.getmtime(s) + def getmtime(self): + return self.module.getmtime(self) + mtime = property( getmtime, None, None, """ Last-modified time of the file. """) - if hasattr(os.path, 'getctime'): - def getctime(s): return os.path.getctime(s) - ctime = property( - getctime, None, None, - """ Creation time of the file. """) + def getctime(self): + return self.module.getctime(self) + + ctime = property( + getctime, None, None, + """ Creation time of the file. """) + + def getsize(self): + return self.module.getsize(self) - def getsize(s): return os.path.getsize(s) size = property( getsize, None, None, """ Size of the file, in bytes. """) @@ -802,27 +973,36 @@ class path(unicode): """ Like path.stat(), but do not follow symbolic links. """ return os.lstat(self) - def get_owner(self): - r""" Return the name of the owner of this file or directory. + def __get_owner_windows(self): + r""" + Return the name of the owner of this file or directory. Follow + symbolic links. - This follows symbolic links. + Return a name of the form ur'DOMAIN\User Name'; may be a group. + """ + desc = win32security.GetFileSecurity( + self, win32security.OWNER_SECURITY_INFORMATION) + sid = desc.GetSecurityDescriptorOwner() + account, domain, typecode = win32security.LookupAccountSid(None, sid) + return domain + u('\\') + account - On Windows, this returns a name of the form ur'DOMAIN\User Name'. - On Windows, a group can own a file or directory. + def __get_owner_unix(self): """ - if os.name == 'nt': - if win32security is None: - raise Exception("path.owner requires win32all to be installed") - desc = win32security.GetFileSecurity( - self, win32security.OWNER_SECURITY_INFORMATION) - sid = desc.GetSecurityDescriptorOwner() - account, domain, typecode = win32security.LookupAccountSid(None, sid) - return domain + u'\\' + account - else: - if pwd is None: - raise NotImplementedError("path.owner is not implemented on this platform.") - st = self.stat() - return pwd.getpwuid(st.st_uid).pw_name + Return the name of the owner of this file or directory. Follow + symbolic links. + """ + st = self.stat() + return pwd.getpwuid(st.st_uid).pw_name + + def __get_owner_not_implemented(self): + raise NotImplementedError("Ownership not available on this platform.") + + if 'win32security' in globals(): + get_owner = __get_owner_windows + elif 'pwd' in globals(): + get_owner = __get_owner_unix + else: + get_owner = __get_owner_not_implemented owner = property( get_owner, None, None, @@ -837,41 +1017,85 @@ class path(unicode): def pathconf(self, name): return os.pathconf(self, name) - + # # --- Modifying operations on files and directories def utime(self, times): """ Set the access and modified times of this file. """ os.utime(self, times) + return self def chmod(self, mode): os.chmod(self, mode) + return self if hasattr(os, 'chown'): - def chown(self, uid, gid): + def chown(self, uid=-1, gid=-1): os.chown(self, uid, gid) + return self def rename(self, new): os.rename(self, new) + return self._next_class(new) def renames(self, new): os.renames(self, new) + return self._next_class(new) - + # # --- Create/delete operations on directories - def mkdir(self, mode=0o777): + def mkdir(self, mode=o777): os.mkdir(self, mode) + return self + + def mkdir_p(self, mode=o777): + try: + self.mkdir(mode) + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.EEXIST: + raise + return self - def makedirs(self, mode=0o777): + def makedirs(self, mode=o777): os.makedirs(self, mode) + return self + + def makedirs_p(self, mode=o777): + try: + self.makedirs(mode) + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.EEXIST: + raise + return self def rmdir(self): os.rmdir(self) + return self + + def rmdir_p(self): + try: + self.rmdir() + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: + raise + return self def removedirs(self): os.removedirs(self) + return self + def removedirs_p(self): + try: + self.removedirs() + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST: + raise + return self # --- Modifying operations on files @@ -879,16 +1103,31 @@ class path(unicode): """ Set the access/modified times of this file to the current time. Create the file if it does not exist. """ - fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) + fd = os.open(self, os.O_WRONLY | os.O_CREAT, o666) os.close(fd) os.utime(self, None) + return self def remove(self): os.remove(self) + return self + + def remove_p(self): + try: + self.unlink() + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOENT: + raise + return self def unlink(self): os.unlink(self) + return self + def unlink_p(self): + self.remove_p() + return self # --- Links @@ -896,11 +1135,13 @@ class path(unicode): def link(self, newpath): """ Create a hard link at 'newpath', pointing to this file. """ os.link(self, newpath) + return self._next_class(newpath) if hasattr(os, 'symlink'): def symlink(self, newlink): """ Create a symbolic link at 'newlink', pointing here. """ os.symlink(self, newlink) + return self._next_class(newlink) if hasattr(os, 'readlink'): def readlink(self): @@ -908,7 +1149,7 @@ class path(unicode): The result may be an absolute or a relative path. """ - return self.__class__(os.readlink(self)) + return self._next_class(os.readlink(self)) def readlinkabs(self): """ Return the path to which this symbolic link points. @@ -921,7 +1162,7 @@ class path(unicode): else: return (self.parent / p).abspath() - + # # --- High-level functions from shutil copyfile = shutil.copyfile @@ -934,7 +1175,21 @@ class path(unicode): move = shutil.move rmtree = shutil.rmtree + def rmtree_p(self): + try: + self.rmtree() + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOENT: + raise + return self + + def chdir(self): + os.chdir(self) + + cd = chdir + # # --- Special stuff from os if hasattr(os, 'chroot'): @@ -944,4 +1199,69 @@ class path(unicode): if hasattr(os, 'startfile'): def startfile(self): os.startfile(self) + return self + +class tempdir(path): + """ + A temporary directory via tempfile.mkdtemp, and constructed with the + same parameters that you can use as a context manager. + + Example: + + with tempdir() as d: + # do stuff with the path object "d" + + # here the directory is deleted automatically + """ + + @ClassProperty + @classmethod + def _next_class(cls): + return path + + def __new__(cls, *args, **kwargs): + dirname = tempfile.mkdtemp(*args, **kwargs) + return super(tempdir, cls).__new__(cls, dirname) + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + if not exc_value: + self.rmtree() + + +def _permission_mask(mode): + """ + Convert a Unix chmod symbolic mode like 'ugo+rwx' to a function + suitable for applying to a mask to affect that change. + + >>> mask = _permission_mask('ugo+rwx') + >>> oct(mask(o554)) + 'o777' + + >>> oct(_permission_mask('gw-x')(o777)) + 'o766' + """ + parsed = re.match('(?P[ugo]+)(?P[-+])(?P[rwx]+)$', mode) + if not parsed: + raise ValueError("Unrecognized symbolic mode", mode) + spec_map = dict(r=4, w=2, x=1) + spec = reduce(operator.or_, [spec_map[perm] + for perm in parsed.group('what')]) + # now apply spec to each in who + shift_map = dict(u=6, g=3, o=0) + mask = reduce(operator.or_, [spec << shift_map[subj] + for subj in parsed.group('who')]) + + op = parsed.group('op') + # if op is -, invert the mask + if op == '-': + mask ^= o777 + + op_map = {'+': operator.or_, '-': operator.and_} + return functools.partial(op_map[op], mask)