Source code for cement.utils.fs

"""Common File System Utilities."""

import os
import shutil
import tempfile
from datetime import datetime
from pathlib import Path as _Path
from types import TracebackType
from typing import Any


[docs] class Tmp: """ Provides creation and cleanup of a separate temporary directory, and file. Keyword Args: cleanup (bool): Whether or not to delete the temporary directory and file on exit (when used with the ``with`` operator). suffix (str): The suffix that the directory and file will end with. Default: *no suffix* prefix (str): The prefix that the directory and file will start with. Default: *no prefix* dir (str): The parent directory path that the temp directory and file will be created in. Default: *system default temporary path* Example: .. code-block:: python from cement.utils import fs with fs.Tmp() as tmp: # do something with a temporary directory os.listdir(tmp.dir) # do something with a temporary file with open(tmp.file, 'w') as f: f.write('some data') """ dir: str file: str def __init__(self, **kwargs: str) -> None: self.cleanup = kwargs.get('cleanup', True) suffix = kwargs.get('suffix', '') prefix = kwargs.get('prefix', 'tmp') dir = kwargs.get('dir', None) self.dir = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dir) _, self.file = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
[docs] def remove(self) -> None: """ Remove the temporary directory (and file) if it exists, and ``self.cleanup`` is ``True``. """ if self.cleanup is True: if _Path(self.dir).exists(): shutil.rmtree(self.dir) if _Path(self.file).exists(): os.remove(self.file)
def __enter__(self): # type: ignore return self def __exit__(self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __exc_traceback: TracebackType | None) -> None: self.remove()
[docs] def abspath(path: str, strip_trailing_slash: bool = True) -> str: """ Return an absolute path, while also expanding the ``~`` user directory shortcut. Args: path (str): The original path to expand. Returns: str: The fully expanded, absolute path to the given ``path`` Example: .. code-block:: python from cement.utils import fs fs.abspath('~/some/path') fs.abspath('./some.file') """ return os.path.abspath(os.path.expanduser(path)) # boundary: D-14 (CR-01/CR-02)
[docs] def join(*args: str, **kwargs: Any) -> str: """ Return a complete, joined path, by first calling ``abspath()`` on the first item to ensure the final path is complete. Args: paths (list): A list of paths to join together. Returns: list: The complete and absolute joined path. Example: .. code-block:: python from cement.utils import fs fs.join('~/some/path', 'some/other/relevant/paht') """ # NOTE: ``**kwargs`` is retained on the public signature for backward # compatibility but is unused by the body. The original implementation # forwarded kwargs to the stdlib path-join (which only accepts # positional args), so any non-empty kwargs would have raised # TypeError. No in-tree caller passes kwargs; downstream callers # passing kwargs were already broken pre-migration. paths = list(args) first_path = abspath(paths.pop(0)) p = _Path(first_path) for part in paths: p = p / part return str(p)
[docs] def join_exists(*paths: str) -> tuple[str, bool]: """ Wrapper around :func:`join` plus an existence check via :class:`pathlib.Path`. Args: paths (list): List of paths to join, and then return ``True`` if that path exists, or ``False`` if it does not. Returns: tuple: First item is the fully joined absolute path, and the second is ``bool`` (whether that path exists or not). """ path = join(*paths) return (path, _Path(path).exists())
[docs] def ensure_dir_exists(path: str) -> None: """ Ensure the directory ``path`` exists, and if not create it. Args: path (str): The filesystem path of a directory. Raises: AssertionError: If the directory ``path`` exists, but is not a directory. Returns: None """ path = abspath(path) p = _Path(path) if p.exists() and not p.is_dir(): raise AssertionError(f'Path `{path}` exists but is not a directory!') elif not p.exists(): p.mkdir(parents=True)
[docs] def ensure_parent_dir_exists(path: str) -> None: """ Ensure the parent directory of ``path`` (file, or directory) exists, and if not create it. Args: path (str): The filesystem path of a file or directory. Returns: None """ parent_dir = str(_Path(abspath(path)).parent) return ensure_dir_exists(parent_dir)
[docs] def backup(path: str, suffix: str = '.bak', **kwargs: Any) -> str | None: """ Rename a file or directory safely without overwriting an existing backup of the same name. Args: path (str): The path to the file or directory to make a backup of. suffix (str): The suffix to rename files with. Keyword Args: timestamp(bool): whether to add a timestamp to the backup suffix timestamp_format(str): Date-Time format in python datetime.datetime notation. default '%Y-%m-%d-%H:%M:%S' Returns: str: The new path of backed up file/directory Example: .. code-block:: python from cement.utils import fs fs.backup('/path/to/original/file') """ count = -1 new_path = None path = abspath(path) if kwargs.get('timestamp', False) is True: timestamp_format = kwargs.get('timestamp_format', '%Y-%m-%d-%H:%M:%S') timestamp = datetime.now().strftime(timestamp_format) suffix = '-'.join((suffix, timestamp)) p = _Path(path) while True: if p.exists(): if count == -1: new_path = f"{path}{suffix}" else: new_path = f"{path}{suffix}.{count}" if _Path(new_path).exists(): count += 1 continue else: if p.is_file(): shutil.copy(path, new_path) elif p.is_dir(): shutil.copytree(path, new_path) break else: break # pragma: nocover # defensive: unreachable return new_path
# Kinda dirty, but should resolve issues on Windows per #183 if 'HOME' in os.environ: HOME_DIR = abspath(os.environ['HOME']) else: HOME_DIR = abspath('~') # pragma: nocover # platform-specific