"""The path module contains utilities for working with OS paths."""
from contextlib import contextmanager
import fnmatch
import os
from pathlib import Path
import typing as t
from typing import Iterable
from .types import LsFilter, LsFilterable, LsFilterFn, StrPath
[docs]
class Ls:
"""
Directory listing iterable that iterates over its contents and returns them as ``Path`` objects.
Args:
path: Directory to list.
recursive: Whether to recurse into subdirectories. Defaults to ``False``.
only_files: Limit results to files only. Mutually exclusive with ``only_dirs``.
only_dirs: Limit results to directories only. Mutually exclusive with ``only_files``.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True`` and path matches ``only_files`` or ``only_dirs`` (if set). If path is a
directory and is not included, its contents are still eligible for inclusion if they
match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
def __init__(
self,
path: StrPath = ".",
*,
recursive: bool = False,
only_files: bool = False,
only_dirs: bool = False,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
):
if only_files and only_dirs:
raise ValueError("only_files and only_dirs cannot both be true")
include_filters: t.List[t.Callable[[Path], bool]] = []
exclude_filters: t.List[t.Callable[[Path], bool]] = []
if include:
if isinstance(include, Iterable) and not isinstance(include, (str, bytes)):
includes = include
else:
includes = [include]
# When creating the include filters, need to also take into account the only_* filter
# settings so that an include filter will only match if both are true.
include_filters.extend(
_make_ls_filter(only_files=only_files, only_dirs=only_dirs, filterable=incl)
for incl in includes
)
elif only_files or only_dirs:
# If no include filters are given, but one of the only_* filters is, then we'll add it.
# Otherwise, when include is given, the only_* filters are taken into account for each
# include filter.
include_filters.append(_make_ls_filter(only_files=only_files, only_dirs=only_dirs))
if exclude:
if isinstance(exclude, Iterable) and not isinstance(exclude, (str, bytes)):
excludes = exclude
else:
excludes = [exclude]
exclude_filters.extend(_make_ls_filter(filterable=excl) for excl in excludes)
self.path = path
self.recursive = recursive
self.include_filters = include_filters
self.exclude_filters = exclude_filters
def __repr__(self) -> str:
return f"{self.__class__.__name__}(path={self.path!r}, recursive={self.recursive})"
def __str__(self) -> str:
"""Return file system representation of path."""
return str(self.path)
# Make class PathLike for os.fspath compatibility.
__fspath__ = __str__
def __iter__(self) -> t.Iterator[Path]:
"""Iterate over :attr:`path` and yield its contents."""
yield from _ls(
self.path,
recursive=self.recursive,
include_filters=self.include_filters,
exclude_filters=self.exclude_filters,
)
def _ls(
path: StrPath = ".",
*,
recursive: bool = False,
include_filters: t.Optional[t.List[LsFilterFn]] = None,
exclude_filters: t.Optional[t.List[LsFilterFn]] = None,
) -> t.Generator[Path, None, None]:
scanner = os.scandir(Path(path))
recurse_into: t.List[str] = []
with scanner:
while True:
try:
try:
entry = next(scanner)
except StopIteration:
break
except OSError: # pragma: no cover
return
entry_path = Path(entry.path)
excluded = exclude_filters and any(
is_excluded(entry_path) for is_excluded in exclude_filters
)
if not excluded and (
not include_filters
or any(is_included(entry_path) for is_included in include_filters)
):
yield entry_path
if recursive and not excluded and entry_path.is_dir() and not entry_path.is_symlink():
recurse_into.append(entry.path)
for subdir in recurse_into:
yield from _ls(
subdir,
recursive=recursive,
include_filters=include_filters,
exclude_filters=exclude_filters,
)
def _make_ls_filter(
only_files: bool = False, only_dirs: bool = False, filterable: t.Optional[LsFilterable] = None
) -> LsFilterFn:
filter_fn: t.Optional[LsFilterFn] = None
if filterable:
filter_fn = _make_ls_filterable_fn(filterable)
def _ls_filter(path: Path) -> bool:
if only_files and path.is_dir():
return False
elif only_dirs and path.is_file():
return False
elif filter_fn:
return filter_fn(path)
else:
return True
return _ls_filter
def _make_ls_filterable_fn(filterable: LsFilterable) -> LsFilterFn:
_ls_filterable_fn: LsFilterFn
if isinstance(filterable, str):
def _ls_filterable_fn(path: Path) -> bool:
return fnmatch.fnmatch(path, filterable) # type: ignore
elif isinstance(filterable, t.Pattern):
def _ls_filterable_fn(path: Path) -> bool:
return bool(filterable.match(str(path))) # type: ignore
elif callable(filterable):
def _ls_filterable_fn(path: Path) -> bool:
return filterable(path) # type: ignore
else:
raise TypeError(
f"ls filter must be one of str, re.compile() or callable, not {type(filterable)!r}"
)
return _ls_filterable_fn
[docs]
@contextmanager
def cd(path: StrPath) -> t.Iterator[None]:
"""
Context manager that changes the working directory on enter and restores it on exit.
Args:
path: Directory to change to.
"""
orig_cwd = os.getcwd()
if path:
os.chdir(path)
try:
yield
finally:
if path:
os.chdir(orig_cwd)
[docs]
def cwd() -> Path:
"""Return current working directory as ``Path`` object."""
return Path.cwd()
[docs]
def homedir():
"""Return current user's home directory as ``Path`` object."""
return Path.home()
[docs]
def ls(
path: StrPath = ".",
*,
recursive: bool = False,
only_files: bool = False,
only_dirs: bool = False,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that lists directory contents as ``Path`` objects.
Args:
path: Directory to list.
recursive: Whether to recurse into subdirectories. Defaults to ``False``.
only_files: Limit results to files only. Mutually exclusive with ``only_dirs``.
only_dirs: Limit results to directories only. Mutually exclusive with ``only_files``.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True`` and path matches ``only_files`` or ``only_dirs`` (if set). If path is a
directory and is not included, its contents are still eligible for inclusion if they
match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return Ls(
path,
recursive=recursive,
only_files=only_files,
only_dirs=only_dirs,
include=include,
exclude=exclude,
)
[docs]
def lsfiles(
path: StrPath = ".",
*,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that only lists files in directory as ``Path`` objects.
See Also:
This function is not recursive and will only yield the top-level contents of a directory.
Use :func:`.walkfiles` to recursively yield all files from a directory.
Args:
path: Directory to list.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True``. If path is a directory and is not included, its contents are still eligible
for inclusion if they match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return ls(path, only_files=True, include=include, exclude=exclude)
[docs]
def lsdirs(
path: StrPath = ".",
*,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that only lists directories in directory as ``Path`` objects.
See Also:
This function is not recursive and will only yield the top-level contents of a directory.
Use :func:`.walkdirs` to recursively yield all directories from a directory.
Args:
path: Directory to list.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True``. If path is a directory and is not included, its contents are still eligible
for inclusion if they match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return ls(path, only_dirs=True, include=include, exclude=exclude)
[docs]
def reljoin(*paths: StrPath) -> str:
"""
Like ``os.path.join`` except that all paths are treated as relative to the previous one so that
an absolute path in the middle will extend the existing path instead of becoming the new root
path.
Args:
*paths: Paths to join together.
"""
path = os.sep.join(str(Path(path)) for path in paths)
return os.path.normpath(path)
[docs]
def walk(
path: StrPath = ".",
*,
only_files: bool = False,
only_dirs: bool = False,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that recursively lists all directory contents as ``Path`` objects.
See Also:
This function is recursive and will list all contents of a directory. Use :func:`.ls` to
list only the top-level contents of a directory.
Args:
path: Directory to walk.
only_files: Limit results to files only. Mutually exclusive with ``only_dirs``.
only_dirs: Limit results to directories only. Mutually exclusive with ``only_files``.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True`` and path matches ``only_files`` or ``only_dirs`` (if set). If path is a
directory and is not included, its contents are still eligible for inclusion if they
match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return ls(
path,
recursive=True,
only_files=only_files,
only_dirs=only_dirs,
include=include,
exclude=exclude,
)
[docs]
def walkfiles(
path: StrPath = ".",
*,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that recursively lists only files in directory as ``Path`` objects.
See Also:
This function is recursive and will list all files in a directory. Use :func:`.lsfiles` to
list only the top-level files in a directory.
Args:
path: Directory to walk.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True``. If path is a directory and is not included, its contents are still eligible
for inclusion if they match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return walk(path, only_files=True, include=include, exclude=exclude)
[docs]
def walkdirs(
path: StrPath = ".",
*,
include: t.Optional[LsFilter] = None,
exclude: t.Optional[LsFilter] = None,
) -> Ls:
"""
Return iterable that recursively lists only directories in directory as ``Path`` objects.
See Also:
This function is recursive and will list all directories in a directory. Use
:func:`.lsfiles` to list only the top-level directories in a directory.
Args:
path: Directory to walk.
include: Include paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is included if any of the filters return
``True``. If path is a directory and is not included, its contents are still eligible
for inclusion if they match one of the include filters.
exclude: Exclude paths by filtering on a glob-pattern string, compiled regex, callable, or
iterable containing any of those types. Path is not yielded if any of the filters return
``True``. If the path is a directory and is excluded, then all of its contents will be
excluded.
"""
return walk(path, only_dirs=True, include=include, exclude=exclude)