Source code for shelmet.path

"""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)