2018-12-18 06:10:00 +00:00
|
|
|
import glob
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
2020-07-08 05:25:44 +00:00
|
|
|
from voussoirkit import winglob
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
class PathclassException(Exception):
|
|
|
|
pass
|
|
|
|
|
2020-02-20 00:22:22 +00:00
|
|
|
class Exists(PathclassException):
|
|
|
|
pass
|
2018-12-18 06:10:00 +00:00
|
|
|
|
2020-02-20 00:22:22 +00:00
|
|
|
class NotExists(PathclassException):
|
2018-12-18 06:10:00 +00:00
|
|
|
pass
|
|
|
|
|
2020-02-20 00:22:22 +00:00
|
|
|
class NotDirectory(PathclassException):
|
|
|
|
pass
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
class NotFile(PathclassException):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2020-01-28 05:33:09 +00:00
|
|
|
class Extension:
|
|
|
|
def __init__(self, ext):
|
|
|
|
if isinstance(ext, Extension):
|
|
|
|
ext = ext.ext
|
|
|
|
ext = self.prep(ext)
|
|
|
|
self.ext = ext
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def prep(ext):
|
|
|
|
return os.path.normcase(ext).lstrip('.')
|
|
|
|
|
|
|
|
def __bool__(self):
|
|
|
|
return bool(self.ext)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
2020-02-06 19:52:18 +00:00
|
|
|
if isinstance(other, Extension):
|
|
|
|
return self.ext == other.ext
|
2020-01-28 05:33:09 +00:00
|
|
|
other = self.prep(other)
|
|
|
|
return self.ext == other
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.ext)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f'Extension({repr(self.ext)})'
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.ext
|
|
|
|
|
|
|
|
@property
|
|
|
|
def no_dot(self):
|
|
|
|
return self.ext
|
|
|
|
|
|
|
|
@property
|
|
|
|
def with_dot(self):
|
|
|
|
if self.ext == '':
|
|
|
|
return ''
|
|
|
|
return '.' + self.ext
|
|
|
|
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
class Path:
|
|
|
|
'''
|
|
|
|
I started to use pathlib.Path, but it was too much of a pain.
|
|
|
|
'''
|
2019-08-03 08:01:00 +00:00
|
|
|
def __init__(self, path, force_sep=None):
|
|
|
|
self.force_sep = force_sep
|
|
|
|
self.sep = force_sep or os.sep
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
if isinstance(path, Path):
|
|
|
|
self.absolute_path = path.absolute_path
|
|
|
|
else:
|
|
|
|
path = path.strip()
|
2020-01-21 05:29:33 +00:00
|
|
|
if re.search('^[A-Za-z]:$', path):
|
2018-12-18 06:10:00 +00:00
|
|
|
# Bare Windows drive letter.
|
2019-08-03 08:01:00 +00:00
|
|
|
path += self.sep
|
2018-12-18 06:10:00 +00:00
|
|
|
path = normalize_sep(path)
|
|
|
|
path = os.path.normpath(path)
|
|
|
|
path = os.path.abspath(path)
|
|
|
|
self.absolute_path = path
|
|
|
|
|
2020-01-21 05:49:14 +00:00
|
|
|
self.absolute_path = normalize_sep(self.absolute_path, self.sep)
|
2019-08-03 08:01:00 +00:00
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
def __contains__(self, other):
|
2020-01-21 05:52:40 +00:00
|
|
|
other = self.spawn(other)
|
2019-08-01 15:46:37 +00:00
|
|
|
|
|
|
|
self_norm = self.normcase
|
2019-08-03 08:01:00 +00:00
|
|
|
if not self_norm.endswith(self.sep):
|
|
|
|
self_norm += self.sep
|
|
|
|
return other.normcase.startswith(self_norm)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if not hasattr(other, 'absolute_path'):
|
|
|
|
return False
|
2020-09-02 15:52:13 +00:00
|
|
|
# Compare by normcase so that Windows's case-insensitive filenames
|
|
|
|
# behave correctly.
|
2018-12-18 06:10:00 +00:00
|
|
|
return self.normcase == other.normcase
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.normcase)
|
|
|
|
|
2020-07-21 01:03:48 +00:00
|
|
|
def __lt__(self, other):
|
2020-09-02 15:52:13 +00:00
|
|
|
# Sort by normcase so that Windows's case-insensitive filenames sort
|
|
|
|
# alphabetically regardless of case.
|
|
|
|
return self.normcase < other.normcase
|
2020-07-21 01:03:48 +00:00
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return '{c}({path})'.format(c=self.__class__.__name__, path=repr(self.absolute_path))
|
|
|
|
|
2020-02-20 00:22:22 +00:00
|
|
|
def assert_exists(self):
|
|
|
|
if not self.exists:
|
|
|
|
raise NotExists(self)
|
|
|
|
|
|
|
|
def assert_not_exists(self):
|
|
|
|
if self.exists:
|
|
|
|
raise Exists(self)
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
def assert_is_file(self):
|
|
|
|
if not self.is_file:
|
|
|
|
raise NotFile(self)
|
|
|
|
|
|
|
|
def assert_is_directory(self):
|
|
|
|
if not self.is_dir:
|
|
|
|
raise NotDirectory(self)
|
|
|
|
|
2019-08-01 15:47:27 +00:00
|
|
|
def add_extension(self, extension):
|
2020-01-28 05:33:09 +00:00
|
|
|
extension = Extension(extension)
|
2019-08-01 15:47:27 +00:00
|
|
|
if extension == '':
|
|
|
|
return self
|
2020-01-28 05:33:09 +00:00
|
|
|
return self.parent.with_child(self.basename + extension.with_dot)
|
2019-08-01 15:47:27 +00:00
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
@property
|
|
|
|
def basename(self):
|
|
|
|
return os.path.basename(self.absolute_path)
|
|
|
|
|
|
|
|
def correct_case(self):
|
|
|
|
self.absolute_path = get_path_casing(self.absolute_path)
|
2019-08-03 08:01:00 +00:00
|
|
|
self.absolute_path = self.absolute_path.replace('/', self.sep).replace('\\', self.sep)
|
2018-12-18 06:10:00 +00:00
|
|
|
return self.absolute_path
|
|
|
|
|
|
|
|
@property
|
|
|
|
def depth(self):
|
2019-08-03 08:01:00 +00:00
|
|
|
return len(self.absolute_path.rstrip(self.sep).split(self.sep))
|
2018-12-18 06:10:00 +00:00
|
|
|
|
2019-08-06 00:34:17 +00:00
|
|
|
@property
|
|
|
|
def dot_extension(self):
|
2020-01-28 05:33:09 +00:00
|
|
|
return self.extension.with_dot
|
2019-08-06 00:34:17 +00:00
|
|
|
|
2020-03-13 00:45:42 +00:00
|
|
|
@property
|
|
|
|
def drive(self):
|
|
|
|
return os.path.splitdrive(self.absolute_path)[0]
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
@property
|
|
|
|
def exists(self):
|
|
|
|
return os.path.exists(self.absolute_path)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def extension(self):
|
2020-01-28 05:33:09 +00:00
|
|
|
return Extension(os.path.splitext(self.absolute_path)[1])
|
2018-12-18 06:10:00 +00:00
|
|
|
|
2020-07-08 05:25:44 +00:00
|
|
|
def glob(self, pattern):
|
|
|
|
if '/' in pattern or '\\' in pattern:
|
|
|
|
# If the user wants to glob names in a different path, they should
|
|
|
|
# create a Pathclass for that directory first and do it normally.
|
|
|
|
raise TypeError('glob pattern should not have path separators')
|
2020-07-17 04:28:40 +00:00
|
|
|
pattern = self.with_child(pattern)
|
|
|
|
children = winglob.glob(pattern.absolute_path)
|
2020-07-08 05:25:44 +00:00
|
|
|
children = [self.with_child(child) for child in children]
|
|
|
|
return children
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
@property
|
|
|
|
def is_dir(self):
|
|
|
|
return os.path.isdir(self.absolute_path)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_file(self):
|
|
|
|
return os.path.isfile(self.absolute_path)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_link(self):
|
|
|
|
return os.path.islink(self.absolute_path)
|
|
|
|
|
|
|
|
def join(self, subpath):
|
|
|
|
if not isinstance(subpath, str):
|
|
|
|
raise TypeError('subpath must be a string')
|
2020-01-21 05:52:40 +00:00
|
|
|
path = os.path.join(self.absolute_path, subpath)
|
|
|
|
return self.spawn(path)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
def listdir(self):
|
|
|
|
children = os.listdir(self.absolute_path)
|
|
|
|
children = [self.with_child(child) for child in children]
|
|
|
|
return children
|
|
|
|
|
|
|
|
@property
|
|
|
|
def normcase(self):
|
2019-08-03 08:01:00 +00:00
|
|
|
norm = os.path.normcase(self.absolute_path)
|
|
|
|
norm = norm.replace('/', self.sep).replace('\\', self.sep)
|
|
|
|
return norm
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def parent(self):
|
|
|
|
parent = os.path.dirname(self.absolute_path)
|
2020-01-21 05:52:40 +00:00
|
|
|
return self.spawn(parent)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def relative_path(self):
|
|
|
|
return self.relative_to(os.getcwd())
|
|
|
|
|
2019-08-01 15:58:13 +00:00
|
|
|
def relative_to(self, other, simple=False):
|
2020-01-21 05:52:40 +00:00
|
|
|
other = self.spawn(other)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
if self == other:
|
|
|
|
return '.'
|
|
|
|
|
2019-08-01 15:47:04 +00:00
|
|
|
self.correct_case()
|
|
|
|
other.correct_case()
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
if self in other:
|
2019-02-18 07:43:33 +00:00
|
|
|
relative = self.absolute_path
|
|
|
|
relative = relative.replace(other.absolute_path, '', 1)
|
2019-08-03 08:01:00 +00:00
|
|
|
relative = relative.lstrip(self.sep)
|
2019-08-01 15:58:13 +00:00
|
|
|
if not simple:
|
2019-08-03 08:01:00 +00:00
|
|
|
relative = '.' + self.sep + relative
|
2019-02-18 07:43:33 +00:00
|
|
|
return relative
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
common = common_path([other.absolute_path, self.absolute_path], fallback=None)
|
2019-02-18 07:43:33 +00:00
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
if common is None:
|
|
|
|
return self.absolute_path
|
2019-02-18 07:43:33 +00:00
|
|
|
|
2020-01-21 05:52:40 +00:00
|
|
|
common = self.spawn(common)
|
2018-12-18 06:10:00 +00:00
|
|
|
backsteps = other.depth - common.depth
|
2019-08-03 08:01:00 +00:00
|
|
|
backsteps = self.sep.join('..' for x in range(backsteps))
|
2019-08-01 15:57:18 +00:00
|
|
|
common = common.absolute_path
|
2019-08-03 08:01:00 +00:00
|
|
|
if not common.endswith(self.sep):
|
|
|
|
common += self.sep
|
2019-08-01 15:57:18 +00:00
|
|
|
unique = self.absolute_path.replace(common, '', 1)
|
2019-08-03 08:01:00 +00:00
|
|
|
relative_path = os.path.join(backsteps, unique)
|
|
|
|
relative_path = relative_path.replace('/', self.sep).replace('\\', self.sep)
|
|
|
|
return relative_path
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
def replace_extension(self, extension):
|
2020-01-28 05:33:09 +00:00
|
|
|
extension = Extension(extension)
|
2019-08-03 08:01:00 +00:00
|
|
|
base = os.path.splitext(self.basename)[0]
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
if extension == '':
|
2019-08-03 08:01:00 +00:00
|
|
|
return self.parent.with_child(base)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
2020-01-28 05:33:09 +00:00
|
|
|
return self.parent.with_child(base + extension.with_dot)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def size(self):
|
2020-08-29 00:31:31 +00:00
|
|
|
self.assert_exists()
|
2018-12-18 06:10:00 +00:00
|
|
|
if self.is_file:
|
|
|
|
return os.path.getsize(self.absolute_path)
|
2020-08-29 00:31:31 +00:00
|
|
|
elif self.is_dir:
|
2020-03-13 01:18:59 +00:00
|
|
|
return sum(file.size for file in self.walk() if file.is_file)
|
2018-12-18 06:10:00 +00:00
|
|
|
|
2020-01-21 05:52:40 +00:00
|
|
|
def spawn(self, path):
|
|
|
|
return self.__class__(path, force_sep=self.force_sep)
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
@property
|
|
|
|
def stat(self):
|
|
|
|
return os.stat(self.absolute_path)
|
|
|
|
|
2020-03-03 01:46:19 +00:00
|
|
|
def touch(self):
|
|
|
|
try:
|
|
|
|
os.utime(self.absolute_path)
|
|
|
|
except FileNotFoundError:
|
|
|
|
open(self.absolute_path, 'a').close()
|
|
|
|
|
2019-06-12 05:45:04 +00:00
|
|
|
def walk(self):
|
|
|
|
directories = []
|
|
|
|
for child in self.listdir():
|
|
|
|
if child.is_dir:
|
|
|
|
directories.append(child)
|
|
|
|
else:
|
|
|
|
yield child
|
|
|
|
|
|
|
|
for directory in directories:
|
|
|
|
yield directory
|
|
|
|
yield from directory.walk()
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
def with_child(self, basename):
|
|
|
|
return self.join(os.path.basename(basename))
|
|
|
|
|
|
|
|
|
|
|
|
def common_path(paths, fallback):
|
|
|
|
'''
|
|
|
|
Given a list of file paths, determine the deepest path which all
|
|
|
|
have in common.
|
|
|
|
'''
|
|
|
|
if isinstance(paths, (str, Path)):
|
|
|
|
raise TypeError('`paths` must be a collection')
|
2020-01-21 05:52:40 +00:00
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
paths = [Path(f) for f in paths]
|
|
|
|
|
|
|
|
if len(paths) == 0:
|
|
|
|
raise ValueError('Empty list')
|
|
|
|
|
|
|
|
if hasattr(paths, 'pop'):
|
|
|
|
model = paths.pop()
|
|
|
|
else:
|
|
|
|
model = paths[0]
|
|
|
|
paths = paths[1:]
|
|
|
|
|
|
|
|
while True:
|
|
|
|
if all(f in model for f in paths):
|
|
|
|
return model
|
|
|
|
parent = model.parent
|
|
|
|
if parent == model:
|
|
|
|
# We just processed the root, and now we're stuck at the root.
|
|
|
|
# Which means there was no common path.
|
|
|
|
return fallback
|
|
|
|
model = parent
|
|
|
|
|
|
|
|
def get_path_casing(path):
|
|
|
|
'''
|
|
|
|
Take what is perhaps incorrectly cased input and get the path's actual
|
|
|
|
casing according to the filesystem.
|
|
|
|
|
|
|
|
Thank you:
|
|
|
|
Ethan Furman http://stackoverflow.com/a/7133137/5430534
|
|
|
|
xvorsx http://stackoverflow.com/a/14742779/5430534
|
|
|
|
'''
|
|
|
|
if not isinstance(path, Path):
|
|
|
|
path = Path(path)
|
|
|
|
|
|
|
|
# Nonexistent paths don't glob correctly. If the input is a nonexistent
|
|
|
|
# subpath of an existing path, we have to glob the existing portion first,
|
|
|
|
# and then attach the fake portion again at the end.
|
|
|
|
input_path = path
|
|
|
|
while not path.exists:
|
|
|
|
parent = path.parent
|
|
|
|
if path == parent:
|
|
|
|
# We're stuck at a fake root.
|
|
|
|
return input_path.absolute_path
|
|
|
|
path = parent
|
|
|
|
|
|
|
|
path = path.absolute_path
|
|
|
|
|
|
|
|
(drive, subpath) = os.path.splitdrive(path)
|
|
|
|
drive = drive.upper()
|
|
|
|
subpath = subpath.lstrip(os.sep)
|
|
|
|
|
|
|
|
pattern = [glob_patternize(piece) for piece in subpath.split(os.sep)]
|
|
|
|
pattern = os.sep.join(pattern)
|
|
|
|
pattern = drive + os.sep + pattern
|
|
|
|
|
|
|
|
try:
|
|
|
|
cased = glob.glob(pattern)[0]
|
|
|
|
except IndexError:
|
|
|
|
return input_path.absolute_path
|
|
|
|
|
|
|
|
imaginary_portion = input_path.absolute_path
|
|
|
|
imaginary_portion = imaginary_portion[len(cased):]
|
|
|
|
imaginary_portion = imaginary_portion.lstrip(os.sep)
|
|
|
|
cased = os.path.join(cased, imaginary_portion)
|
|
|
|
cased = cased.rstrip(os.sep)
|
2020-02-01 04:53:29 +00:00
|
|
|
if os.sep not in cased:
|
2018-12-18 06:10:00 +00:00
|
|
|
cased += os.sep
|
|
|
|
return cased
|
|
|
|
|
|
|
|
def glob_patternize(piece):
|
|
|
|
'''
|
|
|
|
Create a pattern like "[u]ser" from "user", forcing glob to look up the
|
|
|
|
correct path name, while guaranteeing that the only result will be the correct path.
|
|
|
|
|
|
|
|
Special cases are:
|
|
|
|
`!`
|
|
|
|
because in glob syntax, [!x] tells glob to look for paths that don't contain
|
|
|
|
"x", and [!] is invalid syntax.
|
|
|
|
`[`, `]`
|
|
|
|
because this starts a glob capture group
|
|
|
|
|
|
|
|
so we pick the first non-special character to put in the brackets.
|
|
|
|
If the path consists entirely of these special characters, then the
|
|
|
|
casing doesn't need to be corrected anyway.
|
|
|
|
'''
|
|
|
|
piece = glob.escape(piece)
|
|
|
|
for character in piece:
|
|
|
|
if character not in '![]':
|
2020-02-18 08:42:33 +00:00
|
|
|
replacement = f'[{character}]'
|
2018-12-18 06:10:00 +00:00
|
|
|
piece = piece.replace(character, replacement, 1)
|
|
|
|
break
|
|
|
|
return piece
|
|
|
|
|
2020-01-21 05:49:14 +00:00
|
|
|
def normalize_sep(path, sep=None):
|
|
|
|
sep = sep or os.sep
|
|
|
|
path = path.replace('/', sep)
|
|
|
|
path = path.replace('\\', sep)
|
|
|
|
|
2018-12-18 06:10:00 +00:00
|
|
|
return path
|
|
|
|
|
|
|
|
def system_root():
|
|
|
|
return os.path.abspath(os.sep)
|