mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-08-14 10:36:52 -07:00
Update jaraco-windows to 3.9.2
Also updates: - importlib-metadata-0.7 - jaraco-windows - jaraco.classes-1.5 - jaraco.collections-1.6.0 - jaraco.functools-1.20 - jaraco.structures-1.1.2 - jaraco.text-1.10.1 - jaraco.ui-1.6 - more-itertools-4.3.0 - path.py-11.5.0 - six-1.12.0
This commit is contained in:
parent
aa769627bd
commit
8d43b8ea39
92 changed files with 7515 additions and 996 deletions
577
libs/path.py
577
libs/path.py
|
@ -1,25 +1,3 @@
|
|||
#
|
||||
# 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.
|
||||
|
||||
|
@ -29,8 +7,18 @@ Example::
|
|||
|
||||
from path import Path
|
||||
d = Path('/home/guido/bin')
|
||||
|
||||
# Globbing
|
||||
for f in d.files('*.py'):
|
||||
f.chmod(0o755)
|
||||
|
||||
# Changing the working directory:
|
||||
with Path("somewhere"):
|
||||
# cwd in now `somewhere`
|
||||
...
|
||||
|
||||
# Concatenate paths with /
|
||||
foo_txt = Path("bar") / "foo.txt"
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
@ -41,7 +29,6 @@ import os
|
|||
import fnmatch
|
||||
import glob
|
||||
import shutil
|
||||
import codecs
|
||||
import hashlib
|
||||
import errno
|
||||
import tempfile
|
||||
|
@ -50,8 +37,10 @@ import operator
|
|||
import re
|
||||
import contextlib
|
||||
import io
|
||||
from distutils import dir_util
|
||||
import importlib
|
||||
import itertools
|
||||
import platform
|
||||
import ntpath
|
||||
|
||||
try:
|
||||
import win32security
|
||||
|
@ -77,22 +66,17 @@ string_types = str,
|
|||
text_type = str
|
||||
getcwdu = os.getcwd
|
||||
|
||||
def surrogate_escape(error):
|
||||
"""
|
||||
Simulate the Python 3 ``surrogateescape`` handler, but for Python 2 only.
|
||||
"""
|
||||
chars = error.object[error.start:error.end]
|
||||
assert len(chars) == 1
|
||||
val = ord(chars)
|
||||
val += 0xdc00
|
||||
return __builtin__.unichr(val), error.end
|
||||
|
||||
if PY2:
|
||||
import __builtin__
|
||||
string_types = __builtin__.basestring,
|
||||
text_type = __builtin__.unicode
|
||||
getcwdu = os.getcwdu
|
||||
codecs.register_error('surrogateescape', surrogate_escape)
|
||||
map = itertools.imap
|
||||
filter = itertools.ifilter
|
||||
FileNotFoundError = OSError
|
||||
itertools.filterfalse = itertools.ifilterfalse
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def io_error_compat():
|
||||
|
@ -107,7 +91,8 @@ def io_error_compat():
|
|||
|
||||
##############################################################################
|
||||
|
||||
__all__ = ['Path', 'CaseInsensitivePattern']
|
||||
|
||||
__all__ = ['Path', 'TempDir', 'CaseInsensitivePattern']
|
||||
|
||||
|
||||
LINESEPS = ['\r\n', '\r', '\n']
|
||||
|
@ -119,8 +104,8 @@ U_NL_END = re.compile(r'(?:{0})$'.format(U_NEWLINE.pattern))
|
|||
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
__version__ = pkg_resources.require('path.py')[0].version
|
||||
import importlib_metadata
|
||||
__version__ = importlib_metadata.version('path.py')
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
|
||||
|
@ -131,7 +116,7 @@ class TreeWalkWarning(Warning):
|
|||
|
||||
# from jaraco.functools
|
||||
def compose(*funcs):
|
||||
compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs))
|
||||
compose_two = lambda f1, f2: lambda *args, **kwargs: f1(f2(*args, **kwargs)) # noqa
|
||||
return functools.reduce(compose_two, funcs)
|
||||
|
||||
|
||||
|
@ -170,6 +155,60 @@ class multimethod(object):
|
|||
)
|
||||
|
||||
|
||||
class matchers(object):
|
||||
# TODO: make this class a module
|
||||
|
||||
@staticmethod
|
||||
def load(param):
|
||||
"""
|
||||
If the supplied parameter is a string, assum it's a simple
|
||||
pattern.
|
||||
"""
|
||||
return (
|
||||
matchers.Pattern(param) if isinstance(param, string_types)
|
||||
else param if param is not None
|
||||
else matchers.Null()
|
||||
)
|
||||
|
||||
class Base(object):
|
||||
pass
|
||||
|
||||
class Null(Base):
|
||||
def __call__(self, path):
|
||||
return True
|
||||
|
||||
class Pattern(Base):
|
||||
def __init__(self, pattern):
|
||||
self.pattern = pattern
|
||||
|
||||
def get_pattern(self, normcase):
|
||||
try:
|
||||
return self._pattern
|
||||
except AttributeError:
|
||||
pass
|
||||
self._pattern = normcase(self.pattern)
|
||||
return self._pattern
|
||||
|
||||
def __call__(self, path):
|
||||
normcase = getattr(self, 'normcase', path.module.normcase)
|
||||
pattern = self.get_pattern(normcase)
|
||||
return fnmatch.fnmatchcase(normcase(path.name), pattern)
|
||||
|
||||
class CaseInsensitive(Pattern):
|
||||
"""
|
||||
A Pattern with a ``'normcase'`` property, suitable for passing to
|
||||
:meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`,
|
||||
:meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive.
|
||||
|
||||
For example, to get all files ending in .py, .Py, .pY, or .PY in the
|
||||
current directory::
|
||||
|
||||
from path import Path, matchers
|
||||
Path('.').files(matchers.CaseInsensitive('*.py'))
|
||||
"""
|
||||
normcase = staticmethod(ntpath.normcase)
|
||||
|
||||
|
||||
class Path(text_type):
|
||||
"""
|
||||
Represents a filesystem path.
|
||||
|
@ -214,16 +253,6 @@ class Path(text_type):
|
|||
"""
|
||||
return cls
|
||||
|
||||
@classmethod
|
||||
def _always_unicode(cls, path):
|
||||
"""
|
||||
Ensure the path as retrieved from a Python API, such as :func:`os.listdir`,
|
||||
is a proper Unicode string.
|
||||
"""
|
||||
if PY3 or isinstance(path, text_type):
|
||||
return path
|
||||
return path.decode(sys.getfilesystemencoding(), 'surrogateescape')
|
||||
|
||||
# --- Special Python methods.
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -277,6 +306,9 @@ class Path(text_type):
|
|||
def __exit__(self, *_):
|
||||
os.chdir(self._old_dir)
|
||||
|
||||
def __fspath__(self):
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def getcwd(cls):
|
||||
""" Return the current working directory as a path object.
|
||||
|
@ -330,23 +362,45 @@ class Path(text_type):
|
|||
return self.expandvars().expanduser().normpath()
|
||||
|
||||
@property
|
||||
def namebase(self):
|
||||
def stem(self):
|
||||
""" The same as :meth:`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'``.
|
||||
>>> Path('/home/guido/python.tar.gz').stem
|
||||
'python.tar'
|
||||
"""
|
||||
base, ext = self.module.splitext(self.name)
|
||||
return base
|
||||
|
||||
@property
|
||||
def namebase(self):
|
||||
warnings.warn("Use .stem instead of .namebase", DeprecationWarning)
|
||||
return self.stem
|
||||
|
||||
@property
|
||||
def ext(self):
|
||||
""" The file extension, for example ``'.py'``. """
|
||||
f, ext = self.module.splitext(self)
|
||||
return ext
|
||||
|
||||
def with_suffix(self, suffix):
|
||||
""" Return a new path with the file suffix changed (or added, if none)
|
||||
|
||||
>>> Path('/home/guido/python.tar.gz').with_suffix(".foo")
|
||||
Path('/home/guido/python.tar.foo')
|
||||
|
||||
>>> Path('python').with_suffix('.zip')
|
||||
Path('python.zip')
|
||||
|
||||
>>> Path('filename.ext').with_suffix('zip')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Invalid suffix 'zip'
|
||||
"""
|
||||
if not suffix.startswith('.'):
|
||||
raise ValueError("Invalid suffix {suffix!r}".format(**locals()))
|
||||
|
||||
return self.stripext() + suffix
|
||||
|
||||
@property
|
||||
def drive(self):
|
||||
""" The drive specifier, for example ``'C:'``.
|
||||
|
@ -437,8 +491,9 @@ class Path(text_type):
|
|||
@multimethod
|
||||
def joinpath(cls, first, *others):
|
||||
"""
|
||||
Join first to zero or more :class:`Path` components, adding a separator
|
||||
character (:samp:`{first}.module.sep`) if needed. Returns a new instance of
|
||||
Join first to zero or more :class:`Path` components,
|
||||
adding a separator character (:samp:`{first}.module.sep`)
|
||||
if needed. Returns a new instance of
|
||||
:samp:`{first}._next_class`.
|
||||
|
||||
.. seealso:: :func:`os.path.join`
|
||||
|
@ -516,7 +571,7 @@ class Path(text_type):
|
|||
|
||||
# --- Listing, searching, walking, and matching
|
||||
|
||||
def listdir(self, pattern=None):
|
||||
def listdir(self, match=None):
|
||||
""" D.listdir() -> List of items in this directory.
|
||||
|
||||
Use :meth:`files` or :meth:`dirs` instead if you want a listing
|
||||
|
@ -524,46 +579,39 @@ class Path(text_type):
|
|||
|
||||
The elements of the list are Path objects.
|
||||
|
||||
With the optional `pattern` argument, this only lists
|
||||
items whose names match the given pattern.
|
||||
With the optional `match` argument, a callable,
|
||||
only return items whose names match the given pattern.
|
||||
|
||||
.. seealso:: :meth:`files`, :meth:`dirs`
|
||||
"""
|
||||
if pattern is None:
|
||||
pattern = '*'
|
||||
return [
|
||||
self / child
|
||||
for child in map(self._always_unicode, os.listdir(self))
|
||||
if self._next_class(child).fnmatch(pattern)
|
||||
]
|
||||
match = matchers.load(match)
|
||||
return list(filter(match, (
|
||||
self / child for child in os.listdir(self)
|
||||
)))
|
||||
|
||||
def dirs(self, pattern=None):
|
||||
def dirs(self, *args, **kwargs):
|
||||
""" D.dirs() -> List of this directory's subdirectories.
|
||||
|
||||
The elements of the list are Path objects.
|
||||
This does not walk recursively into subdirectories
|
||||
(but see :meth:`walkdirs`).
|
||||
|
||||
With the optional `pattern` argument, this only lists
|
||||
directories whose names match the given pattern. For
|
||||
example, ``d.dirs('build-*')``.
|
||||
Accepts parameters to :meth:`listdir`.
|
||||
"""
|
||||
return [p for p in self.listdir(pattern) if p.isdir()]
|
||||
return [p for p in self.listdir(*args, **kwargs) if p.isdir()]
|
||||
|
||||
def files(self, pattern=None):
|
||||
def files(self, *args, **kwargs):
|
||||
""" D.files() -> List of the files in this directory.
|
||||
|
||||
The elements of the list are Path objects.
|
||||
This does not walk into subdirectories (see :meth:`walkfiles`).
|
||||
|
||||
With the optional `pattern` argument, this only lists files
|
||||
whose names match the given pattern. For example,
|
||||
``d.files('*.pyc')``.
|
||||
Accepts parameters to :meth:`listdir`.
|
||||
"""
|
||||
|
||||
return [p for p in self.listdir(pattern) if p.isfile()]
|
||||
return [p for p in self.listdir(*args, **kwargs) if p.isfile()]
|
||||
|
||||
def walk(self, pattern=None, errors='strict'):
|
||||
def walk(self, match=None, errors='strict'):
|
||||
""" D.walk() -> iterator over files and subdirs, recursively.
|
||||
|
||||
The iterator yields Path objects naming each child item of
|
||||
|
@ -593,6 +641,8 @@ class Path(text_type):
|
|||
raise ValueError("invalid errors parameter")
|
||||
errors = vars(Handlers).get(errors, errors)
|
||||
|
||||
match = matchers.load(match)
|
||||
|
||||
try:
|
||||
childList = self.listdir()
|
||||
except Exception:
|
||||
|
@ -603,7 +653,7 @@ class Path(text_type):
|
|||
return
|
||||
|
||||
for child in childList:
|
||||
if pattern is None or child.fnmatch(pattern):
|
||||
if match(child):
|
||||
yield child
|
||||
try:
|
||||
isdir = child.isdir()
|
||||
|
@ -615,92 +665,26 @@ class Path(text_type):
|
|||
isdir = False
|
||||
|
||||
if isdir:
|
||||
for item in child.walk(pattern, errors):
|
||||
for item in child.walk(errors=errors, match=match):
|
||||
yield item
|
||||
|
||||
def walkdirs(self, pattern=None, errors='strict'):
|
||||
def walkdirs(self, *args, **kwargs):
|
||||
""" D.walkdirs() -> iterator over subdirs, recursively.
|
||||
|
||||
With the optional `pattern` argument, this yields only
|
||||
directories whose names match the given pattern. For
|
||||
example, ``mydir.walkdirs('*test')`` yields only directories
|
||||
with names ending in ``'test'``.
|
||||
|
||||
The `errors=` keyword argument controls behavior when an
|
||||
error occurs. The default is ``'strict'``, which causes an
|
||||
exception. The other allowed values are ``'warn'`` (which
|
||||
reports the error via :func:`warnings.warn()`), and ``'ignore'``.
|
||||
"""
|
||||
if errors not in ('strict', 'warn', 'ignore'):
|
||||
raise ValueError("invalid errors parameter")
|
||||
return (
|
||||
item
|
||||
for item in self.walk(*args, **kwargs)
|
||||
if item.isdir()
|
||||
)
|
||||
|
||||
try:
|
||||
dirs = self.dirs()
|
||||
except Exception:
|
||||
if errors == 'ignore':
|
||||
return
|
||||
elif errors == 'warn':
|
||||
warnings.warn(
|
||||
"Unable to list directory '%s': %s"
|
||||
% (self, sys.exc_info()[1]),
|
||||
TreeWalkWarning)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
for child in dirs:
|
||||
if pattern is None or child.fnmatch(pattern):
|
||||
yield child
|
||||
for subsubdir in child.walkdirs(pattern, errors):
|
||||
yield subsubdir
|
||||
|
||||
def walkfiles(self, pattern=None, errors='strict'):
|
||||
def walkfiles(self, *args, **kwargs):
|
||||
""" D.walkfiles() -> iterator over files in D, recursively.
|
||||
|
||||
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``
|
||||
extension.
|
||||
"""
|
||||
if errors not in ('strict', 'warn', 'ignore'):
|
||||
raise ValueError("invalid errors parameter")
|
||||
|
||||
try:
|
||||
childList = self.listdir()
|
||||
except Exception:
|
||||
if errors == 'ignore':
|
||||
return
|
||||
elif errors == 'warn':
|
||||
warnings.warn(
|
||||
"Unable to list directory '%s': %s"
|
||||
% (self, sys.exc_info()[1]),
|
||||
TreeWalkWarning)
|
||||
return
|
||||
else:
|
||||
raise
|
||||
|
||||
for child in childList:
|
||||
try:
|
||||
isfile = child.isfile()
|
||||
isdir = not isfile and child.isdir()
|
||||
except:
|
||||
if errors == 'ignore':
|
||||
continue
|
||||
elif errors == 'warn':
|
||||
warnings.warn(
|
||||
"Unable to access '%s': %s"
|
||||
% (self, sys.exc_info()[1]),
|
||||
TreeWalkWarning)
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
if isfile:
|
||||
if pattern is None or child.fnmatch(pattern):
|
||||
yield child
|
||||
elif isdir:
|
||||
for f in child.walkfiles(pattern, errors):
|
||||
yield f
|
||||
return (
|
||||
item
|
||||
for item in self.walk(*args, **kwargs)
|
||||
if item.isfile()
|
||||
)
|
||||
|
||||
def fnmatch(self, pattern, normcase=None):
|
||||
""" Return ``True`` if `self.name` matches the given `pattern`.
|
||||
|
@ -710,8 +694,8 @@ class Path(text_type):
|
|||
attribute, it is applied to the name and path prior to comparison.
|
||||
|
||||
`normcase` - (optional) A function used to normalize the pattern and
|
||||
filename before matching. Defaults to :meth:`self.module`, which defaults
|
||||
to :meth:`os.path.normcase`.
|
||||
filename before matching. Defaults to :meth:`self.module`, which
|
||||
defaults to :meth:`os.path.normcase`.
|
||||
|
||||
.. seealso:: :func:`fnmatch.fnmatch`
|
||||
"""
|
||||
|
@ -730,10 +714,32 @@ class Path(text_type):
|
|||
of all the files users have in their :file:`bin` directories.
|
||||
|
||||
.. seealso:: :func:`glob.glob`
|
||||
|
||||
.. note:: Glob is **not** recursive, even when using ``**``.
|
||||
To do recursive globbing see :func:`walk`,
|
||||
:func:`walkdirs` or :func:`walkfiles`.
|
||||
"""
|
||||
cls = self._next_class
|
||||
return [cls(s) for s in glob.glob(self / pattern)]
|
||||
|
||||
def iglob(self, pattern):
|
||||
""" Return an iterator of Path objects that match the pattern.
|
||||
|
||||
`pattern` - a path relative to this directory, with wildcards.
|
||||
|
||||
For example, ``Path('/users').iglob('*/bin/*')`` returns an
|
||||
iterator of all the files users have in their :file:`bin`
|
||||
directories.
|
||||
|
||||
.. seealso:: :func:`glob.iglob`
|
||||
|
||||
.. note:: Glob is **not** recursive, even when using ``**``.
|
||||
To do recursive globbing see :func:`walk`,
|
||||
:func:`walkdirs` or :func:`walkfiles`.
|
||||
"""
|
||||
cls = self._next_class
|
||||
return (cls(s) for s in glob.iglob(self / pattern))
|
||||
|
||||
#
|
||||
# --- Reading or writing an entire file at once.
|
||||
|
||||
|
@ -882,15 +888,9 @@ class Path(text_type):
|
|||
translated to ``'\n'``. If ``False``, newline characters are
|
||||
stripped off. Default is ``True``.
|
||||
|
||||
This uses ``'U'`` mode.
|
||||
|
||||
.. seealso:: :meth:`text`
|
||||
"""
|
||||
if encoding is None and retain:
|
||||
with self.open('U') as f:
|
||||
return f.readlines()
|
||||
else:
|
||||
return self.text(encoding, errors).splitlines(retain)
|
||||
return self.text(encoding, errors).splitlines(retain)
|
||||
|
||||
def write_lines(self, lines, encoding=None, errors='strict',
|
||||
linesep=os.linesep, append=False):
|
||||
|
@ -931,14 +931,15 @@ class Path(text_type):
|
|||
to read the file later.
|
||||
"""
|
||||
with self.open('ab' if append else 'wb') as f:
|
||||
for l in lines:
|
||||
isUnicode = isinstance(l, text_type)
|
||||
for line in lines:
|
||||
isUnicode = isinstance(line, text_type)
|
||||
if linesep is not None:
|
||||
pattern = U_NL_END if isUnicode else NL_END
|
||||
l = pattern.sub('', l) + linesep
|
||||
line = pattern.sub('', line) + linesep
|
||||
if isUnicode:
|
||||
l = l.encode(encoding or sys.getdefaultencoding(), errors)
|
||||
f.write(l)
|
||||
line = line.encode(
|
||||
encoding or sys.getdefaultencoding(), errors)
|
||||
f.write(line)
|
||||
|
||||
def read_md5(self):
|
||||
""" Calculate the md5 hash for this file.
|
||||
|
@ -952,8 +953,8 @@ class Path(text_type):
|
|||
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 :mod:`hashlib` module.
|
||||
`hash_name` should be a hash algo name (such as ``'md5'``
|
||||
or ``'sha1'``) that's available in the :mod:`hashlib` module.
|
||||
"""
|
||||
m = hashlib.new(hash_name)
|
||||
for chunk in self.chunks(8192, mode="rb"):
|
||||
|
@ -1176,7 +1177,8 @@ class Path(text_type):
|
|||
gid = grp.getgrnam(gid).gr_gid
|
||||
os.chown(self, uid, gid)
|
||||
else:
|
||||
raise NotImplementedError("Ownership not available on this platform.")
|
||||
msg = "Ownership not available on this platform."
|
||||
raise NotImplementedError(msg)
|
||||
return self
|
||||
|
||||
def rename(self, new):
|
||||
|
@ -1236,7 +1238,8 @@ class Path(text_type):
|
|||
self.rmdir()
|
||||
except OSError:
|
||||
_, e, _ = sys.exc_info()
|
||||
if e.errno != errno.ENOTEMPTY and e.errno != errno.EEXIST:
|
||||
bypass_codes = errno.ENOTEMPTY, errno.EEXIST, errno.ENOENT
|
||||
if e.errno not in bypass_codes:
|
||||
raise
|
||||
return self
|
||||
|
||||
|
@ -1277,9 +1280,8 @@ class Path(text_type):
|
|||
file does not exist. """
|
||||
try:
|
||||
self.unlink()
|
||||
except OSError:
|
||||
_, e, _ = sys.exc_info()
|
||||
if e.errno != errno.ENOENT:
|
||||
except FileNotFoundError as exc:
|
||||
if PY2 and exc.errno != errno.ENOENT:
|
||||
raise
|
||||
return self
|
||||
|
||||
|
@ -1306,11 +1308,16 @@ class Path(text_type):
|
|||
return self._next_class(newpath)
|
||||
|
||||
if hasattr(os, 'symlink'):
|
||||
def symlink(self, newlink):
|
||||
def symlink(self, newlink=None):
|
||||
""" Create a symbolic link at `newlink`, pointing here.
|
||||
|
||||
If newlink is not supplied, the symbolic link will assume
|
||||
the name self.basename(), creating the link in the cwd.
|
||||
|
||||
.. seealso:: :func:`os.symlink`
|
||||
"""
|
||||
if newlink is None:
|
||||
newlink = self.basename()
|
||||
os.symlink(self, newlink)
|
||||
return self._next_class(newlink)
|
||||
|
||||
|
@ -1368,30 +1375,60 @@ class Path(text_type):
|
|||
|
||||
cd = chdir
|
||||
|
||||
def merge_tree(self, dst, symlinks=False, *args, **kwargs):
|
||||
def merge_tree(
|
||||
self, dst, symlinks=False,
|
||||
# *
|
||||
update=False,
|
||||
copy_function=shutil.copy2,
|
||||
ignore=lambda dir, contents: []):
|
||||
"""
|
||||
Copy entire contents of self to dst, overwriting existing
|
||||
contents in dst with those in self.
|
||||
|
||||
If the additional keyword `update` is True, each
|
||||
`src` will only be copied if `dst` does not exist,
|
||||
or `src` is newer than `dst`.
|
||||
Pass ``symlinks=True`` to copy symbolic links as links.
|
||||
|
||||
Note that the technique employed stages the files in a temporary
|
||||
directory first, so this function is not suitable for merging
|
||||
trees with large files, especially if the temporary directory
|
||||
is not capable of storing a copy of the entire source tree.
|
||||
Accepts a ``copy_function``, similar to copytree.
|
||||
|
||||
To avoid overwriting newer files, supply a copy function
|
||||
wrapped in ``only_newer``. For example::
|
||||
|
||||
src.merge_tree(dst, copy_function=only_newer(shutil.copy2))
|
||||
"""
|
||||
update = kwargs.pop('update', False)
|
||||
with tempdir() as _temp_dir:
|
||||
# first copy the tree to a stage directory to support
|
||||
# the parameters and behavior of copytree.
|
||||
stage = _temp_dir / str(hash(self))
|
||||
self.copytree(stage, symlinks, *args, **kwargs)
|
||||
# now copy everything from the stage directory using
|
||||
# the semantics of dir_util.copy_tree
|
||||
dir_util.copy_tree(stage, dst, preserve_symlinks=symlinks,
|
||||
update=update)
|
||||
dst = self._next_class(dst)
|
||||
dst.makedirs_p()
|
||||
|
||||
if update:
|
||||
warnings.warn(
|
||||
"Update is deprecated; "
|
||||
"use copy_function=only_newer(shutil.copy2)",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
copy_function = only_newer(copy_function)
|
||||
|
||||
sources = self.listdir()
|
||||
_ignored = ignore(self, [item.name for item in sources])
|
||||
|
||||
def ignored(item):
|
||||
return item.name in _ignored
|
||||
|
||||
for source in itertools.filterfalse(ignored, sources):
|
||||
dest = dst / source.name
|
||||
if symlinks and source.islink():
|
||||
target = source.readlink()
|
||||
target.symlink(dest)
|
||||
elif source.isdir():
|
||||
source.merge_tree(
|
||||
dest,
|
||||
symlinks=symlinks,
|
||||
update=update,
|
||||
copy_function=copy_function,
|
||||
ignore=ignore,
|
||||
)
|
||||
else:
|
||||
copy_function(source, dest)
|
||||
|
||||
self.copystat(dst)
|
||||
|
||||
#
|
||||
# --- Special stuff from os
|
||||
|
@ -1410,19 +1447,23 @@ class Path(text_type):
|
|||
# in-place re-writing, courtesy of Martijn Pieters
|
||||
# http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/
|
||||
@contextlib.contextmanager
|
||||
def in_place(self, mode='r', buffering=-1, encoding=None, errors=None,
|
||||
newline=None, backup_extension=None):
|
||||
def in_place(
|
||||
self, mode='r', buffering=-1, encoding=None, errors=None,
|
||||
newline=None, backup_extension=None,
|
||||
):
|
||||
"""
|
||||
A context in which a file may be re-written in-place with new content.
|
||||
A context in which a file may be re-written in-place with
|
||||
new content.
|
||||
|
||||
Yields a tuple of :samp:`({readable}, {writable})` file objects, where `writable`
|
||||
replaces `readable`.
|
||||
Yields a tuple of :samp:`({readable}, {writable})` file
|
||||
objects, where `writable` replaces `readable`.
|
||||
|
||||
If an exception occurs, the old file is restored, removing the
|
||||
written data.
|
||||
|
||||
Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only read-only-modes are
|
||||
allowed. A :exc:`ValueError` is raised on invalid modes.
|
||||
Mode *must not* use ``'w'``, ``'a'``, or ``'+'``; only
|
||||
read-only-modes are allowed. A :exc:`ValueError` is raised
|
||||
on invalid modes.
|
||||
|
||||
For example, to add line numbers to a file::
|
||||
|
||||
|
@ -1448,22 +1489,28 @@ class Path(text_type):
|
|||
except os.error:
|
||||
pass
|
||||
os.rename(self, backup_fn)
|
||||
readable = io.open(backup_fn, mode, buffering=buffering,
|
||||
encoding=encoding, errors=errors, newline=newline)
|
||||
readable = io.open(
|
||||
backup_fn, mode, buffering=buffering,
|
||||
encoding=encoding, errors=errors, newline=newline,
|
||||
)
|
||||
try:
|
||||
perm = os.fstat(readable.fileno()).st_mode
|
||||
except OSError:
|
||||
writable = open(self, 'w' + mode.replace('r', ''),
|
||||
writable = open(
|
||||
self, 'w' + mode.replace('r', ''),
|
||||
buffering=buffering, encoding=encoding, errors=errors,
|
||||
newline=newline)
|
||||
newline=newline,
|
||||
)
|
||||
else:
|
||||
os_mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
|
||||
if hasattr(os, 'O_BINARY'):
|
||||
os_mode |= os.O_BINARY
|
||||
fd = os.open(self, os_mode, perm)
|
||||
writable = io.open(fd, "w" + mode.replace('r', ''),
|
||||
writable = io.open(
|
||||
fd, "w" + mode.replace('r', ''),
|
||||
buffering=buffering, encoding=encoding, errors=errors,
|
||||
newline=newline)
|
||||
newline=newline,
|
||||
)
|
||||
try:
|
||||
if hasattr(os, 'chmod'):
|
||||
os.chmod(self, perm)
|
||||
|
@ -1516,6 +1563,23 @@ class Path(text_type):
|
|||
return functools.partial(SpecialResolver, cls)
|
||||
|
||||
|
||||
def only_newer(copy_func):
|
||||
"""
|
||||
Wrap a copy function (like shutil.copy2) to return
|
||||
the dst if it's newer than the source.
|
||||
"""
|
||||
@functools.wraps(copy_func)
|
||||
def wrapper(src, dst, *args, **kwargs):
|
||||
is_newer_dst = (
|
||||
dst.exists()
|
||||
and dst.getmtime() >= src.getmtime()
|
||||
)
|
||||
if is_newer_dst:
|
||||
return dst
|
||||
return copy_func(src, dst, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class SpecialResolver(object):
|
||||
class ResolverScope:
|
||||
def __init__(self, paths, scope):
|
||||
|
@ -1584,14 +1648,15 @@ class Multi:
|
|||
)
|
||||
|
||||
|
||||
class tempdir(Path):
|
||||
class TempDir(Path):
|
||||
"""
|
||||
A temporary directory via :func:`tempfile.mkdtemp`, and constructed with the
|
||||
same parameters that you can use as a context manager.
|
||||
A temporary directory via :func:`tempfile.mkdtemp`, and
|
||||
constructed with the same parameters that you can use
|
||||
as a context manager.
|
||||
|
||||
Example:
|
||||
Example::
|
||||
|
||||
with tempdir() as d:
|
||||
with TempDir() as d:
|
||||
# do stuff with the Path object "d"
|
||||
|
||||
# here the directory is deleted automatically
|
||||
|
@ -1606,19 +1671,27 @@ class tempdir(Path):
|
|||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
dirname = tempfile.mkdtemp(*args, **kwargs)
|
||||
return super(tempdir, cls).__new__(cls, dirname)
|
||||
return super(TempDir, cls).__new__(cls, dirname)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
# TempDir should return a Path version of itself and not itself
|
||||
# so that a second context manager does not create a second
|
||||
# temporary directory, but rather changes CWD to the location
|
||||
# of the temporary directory.
|
||||
return self._next_class(self)
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if not exc_value:
|
||||
self.rmtree()
|
||||
|
||||
|
||||
# For backwards compatibility.
|
||||
tempdir = TempDir
|
||||
|
||||
|
||||
def _multi_permission_mask(mode):
|
||||
"""
|
||||
Support multiple, comma-separated Unix chmod symbolic modes.
|
||||
|
@ -1626,7 +1699,8 @@ def _multi_permission_mask(mode):
|
|||
>>> _multi_permission_mask('a=r,u+w')(0) == 0o644
|
||||
True
|
||||
"""
|
||||
compose = lambda f, g: lambda *args, **kwargs: g(f(*args, **kwargs))
|
||||
def compose(f, g):
|
||||
return lambda *args, **kwargs: g(f(*args, **kwargs))
|
||||
return functools.reduce(compose, map(_permission_mask, mode.split(',')))
|
||||
|
||||
|
||||
|
@ -1692,31 +1766,56 @@ def _permission_mask(mode):
|
|||
return functools.partial(op_map[op], mask)
|
||||
|
||||
|
||||
class CaseInsensitivePattern(text_type):
|
||||
class CaseInsensitivePattern(matchers.CaseInsensitive):
|
||||
def __init__(self, value):
|
||||
warnings.warn(
|
||||
"Use matchers.CaseInsensitive instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super(CaseInsensitivePattern, self).__init__(value)
|
||||
|
||||
|
||||
class FastPath(Path):
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"Use Path, as FastPath no longer holds any advantage",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super(FastPath, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def patch_for_linux_python2():
|
||||
"""
|
||||
A string with a ``'normcase'`` property, suitable for passing to
|
||||
:meth:`listdir`, :meth:`dirs`, :meth:`files`, :meth:`walk`,
|
||||
:meth:`walkdirs`, or :meth:`walkfiles` to match case-insensitive.
|
||||
|
||||
For example, to get all files ending in .py, .Py, .pY, or .PY in the
|
||||
current directory::
|
||||
|
||||
from path import Path, CaseInsensitivePattern as ci
|
||||
Path('.').files(ci('*.py'))
|
||||
As reported in #130, when Linux users create filenames
|
||||
not in the file system encoding, it creates problems on
|
||||
Python 2. This function attempts to patch the os module
|
||||
to make it behave more like that on Python 3.
|
||||
"""
|
||||
if not PY2 or platform.system() != 'Linux':
|
||||
return
|
||||
|
||||
@property
|
||||
def normcase(self):
|
||||
return __import__('ntpath').normcase
|
||||
try:
|
||||
import backports.os
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
########################
|
||||
# Backward-compatibility
|
||||
class path(Path):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
msg = "path is deprecated. Use Path instead."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return Path.__new__(cls, *args, **kwargs)
|
||||
class OS:
|
||||
"""
|
||||
The proxy to the os module
|
||||
"""
|
||||
def __init__(self, wrapped):
|
||||
self._orig = wrapped
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._orig, name)
|
||||
|
||||
def listdir(self, *args, **kwargs):
|
||||
items = self._orig.listdir(*args, **kwargs)
|
||||
return list(map(backports.os.fsdecode, items))
|
||||
|
||||
globals().update(os=OS(os))
|
||||
|
||||
|
||||
__all__ += ['path']
|
||||
########################
|
||||
patch_for_linux_python2()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue