diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml index 5b69c2c..94d18e2 100644 --- a/.github/workflows/test-package.yml +++ b/.github/workflows/test-package.yml @@ -43,9 +43,7 @@ jobs: - name: Run tests run: | - coverage run --append --source=fsutil -m unittest - coverage report --show-missing - coverage xml -o ./coverage.xml + pytest tests --cov=fsutil --cov-report=term-missing --cov-fail-under=90 - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6126ac8..6229a7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.18.0 + rev: v3.19.1 hooks: - id: pyupgrade args: ["--py310-plus"] @@ -14,14 +14,14 @@ repos: - id: fix-future-annotations - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.9.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.12.0 + rev: v1.15.0 hooks: - id: mypy args: [--ignore-missing-imports, --strict] diff --git a/fsutil/__init__.py b/fsutil/__init__.py index 8c8622d..68f15f4 100644 --- a/fsutil/__init__.py +++ b/fsutil/__init__.py @@ -1,30 +1,51 @@ from __future__ import annotations -import glob -import hashlib -import json -import os -import pathlib -import platform -import re -import shutil -import sys -import tarfile -import tempfile -import uuid -import zipfile -from datetime import datetime -from typing import Any, Literal, Union -from collections.abc import Callable, Generator, Iterable -from urllib.parse import urlsplit - -try: - import requests - - requests_installed = True -except ImportError: - requests_installed = False - +from fsutil.archives import ( + create_tar_file, + create_zip_file, + extract_tar_file, + extract_zip_file, +) +from fsutil.checks import ( + assert_dir, + assert_exists, + assert_file, + assert_not_dir, + assert_not_exists, + assert_not_file, + exists, + is_dir, + is_empty, + is_empty_dir, + is_empty_file, + is_file, +) +from fsutil.converters import convert_size_bytes_to_string, convert_size_string_to_bytes +from fsutil.info import ( + get_dir_creation_date, + get_dir_creation_date_formatted, + get_dir_hash, + get_dir_last_modified_date, + get_dir_last_modified_date_formatted, + get_dir_size, + get_dir_size_formatted, + get_file_creation_date, + get_file_creation_date_formatted, + get_file_hash, + get_file_last_modified_date, + get_file_last_modified_date_formatted, + get_file_size, + get_file_size_formatted, +) +from fsutil.io import ( + read_file, + read_file_from_url, + read_file_json, + read_file_lines, + read_file_lines_count, + write_file, + write_file_json, +) from fsutil.metadata import ( __author__, __copyright__, @@ -34,6 +55,54 @@ __title__, __version__, ) +from fsutil.operations import ( + clean_dir, + copy_dir, + copy_dir_content, + copy_file, + create_dir, + create_file, + delete_dir, + delete_dir_content, + delete_dirs, + delete_file, + delete_files, + download_file, + list_dirs, + list_files, + make_dirs, + make_dirs_for_file, + move_dir, + move_file, + remove_dir, + remove_dir_content, + remove_dirs, + remove_file, + remove_files, + rename_dir, + rename_file, + rename_file_basename, + rename_file_extension, + replace_dir, + replace_file, + search_dirs, + search_files, +) +from fsutil.paths import ( + get_file_basename, + get_file_extension, + get_filename, + get_parent_dir, + get_unique_name, + join_filename, + join_filepath, + join_path, + split_filename, + split_filepath, + split_path, + transform_filepath, +) +from fsutil.perms import get_permissions, set_permissions __all__ = [ "__author__", @@ -57,6 +126,7 @@ "copy_file", "create_dir", "create_file", + "create_tar_file", "create_zip_file", "delete_dir", "delete_dir_content", @@ -65,9 +135,11 @@ "delete_files", "download_file", "exists", + "extract_tar_file", "extract_zip_file", "get_dir_creation_date", "get_dir_creation_date_formatted", + "get_dir_hash", "get_dir_last_modified_date", "get_dir_last_modified_date_formatted", "get_dir_size", @@ -121,1399 +193,7 @@ "split_filename", "split_filepath", "split_path", + "transform_filepath", "write_file", "write_file_json", ] - -PathIn = Union[str, pathlib.Path] - -SIZE_UNITS = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] - - -def _require_requests_installed() -> None: - if not requests_installed: - raise ModuleNotFoundError( - "'requests' module is not installed, " - "it can be installed by running: 'pip install requests'" - ) - - -def _get_path(path: PathIn) -> str: - if isinstance(path, str): - return os.path.normpath(path) - return str(path) - - -def assert_dir(path: PathIn) -> None: - """ - Raise an OSError if the given path doesn't exist or it is not a directory. - """ - path = _get_path(path) - if not is_dir(path): - raise OSError(f"Invalid directory path: {path}") - - -def assert_exists(path: PathIn) -> None: - """ - Raise an OSError if the given path doesn't exist. - """ - path = _get_path(path) - if not exists(path): - raise OSError(f"Invalid item path: {path}") - - -def assert_file(path: PathIn) -> None: - """ - Raise an OSError if the given path doesn't exist or it is not a file. - """ - path = _get_path(path) - if not is_file(path): - raise OSError(f"Invalid file path: {path}") - - -def assert_not_dir(path: PathIn) -> None: - """ - Raise an OSError if the given path is an existing directory. - """ - path = _get_path(path) - if is_dir(path): - raise OSError(f"Invalid path, directory already exists: {path}") - - -def assert_not_exists(path: PathIn) -> None: - """ - Raise an OSError if the given path already exists. - """ - path = _get_path(path) - if exists(path): - raise OSError(f"Invalid path, item already exists: {path}") - - -def assert_not_file(path: PathIn) -> None: - """ - Raise an OSError if the given path is an existing file. - """ - path = _get_path(path) - if is_file(path): - raise OSError(f"Invalid path, file already exists: {path}") - - -def _clean_dir_empty_dirs(path: PathIn) -> None: - path = _get_path(path) - for basepath, dirnames, _ in os.walk(path, topdown=False): - for dirname in dirnames: - dirpath = os.path.join(basepath, dirname) - if is_empty_dir(dirpath): - remove_dir(dirpath) - - -def _clean_dir_empty_files(path: PathIn) -> None: - path = _get_path(path) - for basepath, _, filenames in os.walk(path, topdown=False): - for filename in filenames: - filepath = os.path.join(basepath, filename) - if is_empty_file(filepath): - remove_file(filepath) - - -def clean_dir(path: PathIn, *, dirs: bool = True, files: bool = True) -> None: - """ - Clean a directory by removing empty directories and/or empty files. - """ - path = _get_path(path) - assert_dir(path) - if files: - _clean_dir_empty_files(path) - if dirs: - _clean_dir_empty_dirs(path) - - -def convert_size_bytes_to_string(size: int) -> str: - """ - Convert the given size bytes to string using the right unit suffix. - """ - size_num = float(size) - units = SIZE_UNITS - factor = 0 - factor_limit = len(units) - 1 - while (size_num >= 1024) and (factor <= factor_limit): - size_num /= 1024 - factor += 1 - size_units = units[factor] - size_str = f"{size_num:.2f}" if (factor > 1) else f"{size_num:.0f}" - size_str = f"{size_str} {size_units}" - return size_str - - -def convert_size_string_to_bytes(size: str) -> float | int: - """ - Convert the given size string to bytes. - """ - units = [item.lower() for item in SIZE_UNITS] - parts = size.strip().replace(" ", " ").split(" ") - amount = float(parts[0]) - unit = parts[1] - factor = units.index(unit.lower()) - if not factor: - return amount - return int((1024**factor) * amount) - - -def copy_dir( - path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any -) -> None: - """ - Copy the directory at the given path and all its content to dest path. - If overwrite is not allowed and dest path exists, an OSError is raised. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.copytree - """ - path = _get_path(path) - dest = _get_path(dest) - assert_dir(path) - dirname = os.path.basename(os.path.normpath(path)) - dest = os.path.join(dest, dirname) - assert_not_file(dest) - if not overwrite: - assert_not_exists(dest) - copy_dir_content(path, dest, **kwargs) - - -def copy_dir_content(path: PathIn, dest: PathIn, **kwargs: Any) -> None: - """ - Copy the content of the directory at the given path to dest path. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.copytree - """ - path = _get_path(path) - dest = _get_path(dest) - assert_dir(path) - assert_not_file(dest) - make_dirs(dest) - kwargs.setdefault("dirs_exist_ok", True) - shutil.copytree(path, dest, **kwargs) - - -def copy_file( - path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any -) -> None: - """ - Copy the file at the given path and its metadata to dest path. - If overwrite is not allowed and dest path exists, an OSError is raised. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.copy2 - """ - path = _get_path(path) - dest = _get_path(dest) - assert_file(path) - assert_not_dir(dest) - if not overwrite: - assert_not_exists(dest) - make_dirs_for_file(dest) - shutil.copy2(path, dest, **kwargs) - - -def create_dir(path: PathIn, *, overwrite: bool = False) -> None: - """ - Create directory at the given path. - If overwrite is not allowed and path exists, an OSError is raised. - """ - path = _get_path(path) - assert_not_file(path) - if not overwrite: - assert_not_exists(path) - make_dirs(path) - - -def create_file(path: PathIn, content: str = "", *, overwrite: bool = False) -> None: - """ - Create file with the specified content at the given path. - If overwrite is not allowed and path exists, an OSError is raised. - """ - path = _get_path(path) - assert_not_dir(path) - if not overwrite: - assert_not_exists(path) - write_file(path, content) - - -def create_tar_file( - path: PathIn, - content_paths: list[PathIn], - *, - overwrite: bool = True, - compression: str = "", -) -> None: - """ - Create tar file at path compressing directories/files listed in content_paths. - If overwrite is allowed and dest tar already exists, it will be overwritten. - """ - path = _get_path(path) - assert_not_dir(path) - if not overwrite: - assert_not_exists(path) - make_dirs_for_file(path) - - def _write_content_to_tar_file( - file: tarfile.TarFile, content_path: PathIn, basedir: str = "" - ) -> None: - path = _get_path(content_path) - assert_exists(path) - if is_file(path): - filename = get_filename(path) - filepath = join_path(basedir, filename) - file.add(path, filepath) - elif is_dir(path): - for item_name in os.listdir(path): - item_path = join_path(path, item_name) - item_basedir = ( - join_path(basedir, item_name) if is_dir(item_path) else basedir - ) - _write_content_to_tar_file(file, item_path, item_basedir) - - compression = f"w:{compression}" if compression else "w" - with tarfile.open(path, compression) as file: - for content_path in content_paths: - _write_content_to_tar_file(file, content_path) - - -def create_zip_file( - path: PathIn, - content_paths: list[PathIn], - *, - overwrite: bool = True, - compression: int = zipfile.ZIP_DEFLATED, -) -> None: - """ - Create zip file at path compressing directories/files listed in content_paths. - If overwrite is allowed and dest zip already exists, it will be overwritten. - """ - path = _get_path(path) - assert_not_dir(path) - if not overwrite: - assert_not_exists(path) - make_dirs_for_file(path) - - def _write_content_to_zip_file( - file: zipfile.ZipFile, content_path: PathIn, basedir: str = "" - ) -> None: - path = _get_path(content_path) - assert_exists(path) - if is_file(path): - filename = get_filename(path) - filepath = join_path(basedir, filename) - file.write(path, filepath) - elif is_dir(path): - for item_name in os.listdir(path): - item_path = join_path(path, item_name) - item_basedir = ( - join_path(basedir, item_name) if is_dir(item_path) else basedir - ) - _write_content_to_zip_file(file, item_path, item_basedir) - - with zipfile.ZipFile(path, "w", compression) as file: - for content_path in content_paths: - _write_content_to_zip_file(file, content_path) - - -def delete_dir(path: PathIn) -> bool: - """ - Alias for remove_dir. - """ - removed = remove_dir(path) - return removed - - -def delete_dir_content(path: PathIn) -> None: - """ - Alias for remove_dir_content. - """ - remove_dir_content(path) - - -def delete_dirs(*paths: PathIn) -> None: - """ - Alias for remove_dirs. - """ - remove_dirs(*paths) - - -def delete_file(path: PathIn) -> bool: - """ - Alias for remove_file. - """ - removed = remove_file(path) - return removed - - -def delete_files(*paths: PathIn) -> None: - """ - Alias for remove_files. - """ - remove_files(*paths) - - -def download_file( - url: str, - *, - dirpath: PathIn | None = None, - filename: str | None = None, - chunk_size: int = 8192, - **kwargs: Any, -) -> str: - """ - Download a file from url to dirpath. - If dirpath is not provided, the file will be downloaded to a temp directory. - If filename is provided, the file will be named using filename. - It is possible to pass extra request options - (eg. for authentication) using **kwargs. - """ - _require_requests_installed() - # https://stackoverflow.com/a/16696317/2096218 - - kwargs["stream"] = True - with requests.get(url, **kwargs) as response: - response.raise_for_status() - - # build filename - if not filename: - # detect filename from headers - content_disposition = response.headers.get("content-disposition", "") or "" - filename_pattern = r'filename="(.*)"' - filename_match = re.search(filename_pattern, content_disposition) - if filename_match: - filename = filename_match.group(1) - # or detect filename from url - if not filename: - filename = get_filename(url) - # or fallback to a unique name - if not filename: - filename_uuid = str(uuid.uuid4()) - filename = f"download-{filename_uuid}" - - # build filepath - dirpath = dirpath or tempfile.gettempdir() - dirpath = _get_path(dirpath) - filepath = join_path(dirpath, filename) - make_dirs_for_file(filepath) - - # write file to disk - with open(filepath, "wb") as file: - for chunk in response.iter_content(chunk_size=chunk_size): - if chunk: - file.write(chunk) - return filepath - - -def exists(path: PathIn) -> bool: - """ - Check if a directory of a file exists at the given path. - """ - path = _get_path(path) - return os.path.exists(path) - - -def extract_tar_file( - path: PathIn, - dest: PathIn, - *, - autodelete: bool = False, - content_paths: Iterable[tarfile.TarInfo] | None = None, - filter: ( - Callable[[tarfile.TarInfo, str], tarfile.TarInfo | None] - | Literal["fully_trusted", "tar", "data"] - ) - | None = None, -) -> None: - """ - Extract tar file at path to dest path. - If autodelete, the archive will be deleted after extraction. - If content_paths list is defined, - only listed items will be extracted, otherwise all. - """ - path = _get_path(path) - dest = _get_path(dest) - assert_file(path) - assert_not_file(dest) - make_dirs(dest) - with tarfile.TarFile(path, "r") as file: - if sys.version_info < (3, 12): - file.extractall(dest, members=content_paths) - else: - # https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter - file.extractall( - dest, - members=content_paths, - numeric_owner=False, - filter=(filter or "data"), - ) - if autodelete: - remove_file(path) - - -def extract_zip_file( - path: PathIn, - dest: PathIn, - *, - autodelete: bool = False, - content_paths: Iterable[str | zipfile.ZipInfo] | None = None, -) -> None: - """ - Extract zip file at path to dest path. - If autodelete, the archive will be deleted after extraction. - If content_paths list is defined, - only listed items will be extracted, otherwise all. - """ - path = _get_path(path) - dest = _get_path(dest) - assert_file(path) - assert_not_file(dest) - make_dirs(dest) - with zipfile.ZipFile(path, "r") as file: - file.extractall(dest, members=content_paths) - if autodelete: - remove_file(path) - - -def _filter_paths( - basepath: str, - relpaths: list[str], - *, - predicate: Callable[[str], bool] | None = None, -) -> list[str]: - """ - Filter paths relative to basepath according to the optional predicate function. - If predicate is defined, paths are filtered using it, - otherwise all paths will be listed. - """ - paths = [] - for relpath in relpaths: - abspath = os.path.join(basepath, relpath) - if predicate is None or predicate(abspath): - paths.append(abspath) - paths.sort() - return paths - - -def get_dir_creation_date(path: PathIn) -> datetime: - """ - Get the directory creation date. - """ - path = _get_path(path) - assert_dir(path) - creation_timestamp = os.path.getctime(path) - creation_date = datetime.fromtimestamp(creation_timestamp) - return creation_date - - -def get_dir_creation_date_formatted( - path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" -) -> str: - """ - Get the directory creation date formatted using the given format. - """ - path = _get_path(path) - date = get_dir_creation_date(path) - return date.strftime(format) - - -def get_dir_hash(path: PathIn, *, func: str = "md5") -> str: - """ - Get the hash of the directory at the given path using - the specified algorithm function (md5 by default). - """ - path = _get_path(path) - assert_dir(path) - hash = hashlib.new(func) - files = search_files(path) - for file in sorted(files): - file_hash = get_file_hash(file, func=func) - file_hash_b = bytes(file_hash, "utf-8") - hash.update(file_hash_b) - hash_hex = hash.hexdigest() - return hash_hex - - -def get_dir_last_modified_date(path: PathIn) -> datetime: - """ - Get the directory last modification date. - """ - path = _get_path(path) - assert_dir(path) - last_modified_timestamp = os.path.getmtime(path) - for basepath, dirnames, filenames in os.walk(path): - for dirname in dirnames: - dirpath = os.path.join(basepath, dirname) - last_modified_timestamp = max( - last_modified_timestamp, os.path.getmtime(dirpath) - ) - for filename in filenames: - filepath = os.path.join(basepath, filename) - last_modified_timestamp = max( - last_modified_timestamp, os.path.getmtime(filepath) - ) - last_modified_date = datetime.fromtimestamp(last_modified_timestamp) - return last_modified_date - - -def get_dir_last_modified_date_formatted( - path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" -) -> str: - """ - Get the directory last modification date formatted using the given format. - """ - path = _get_path(path) - date = get_dir_last_modified_date(path) - return date.strftime(format) - - -def get_dir_size(path: PathIn) -> int: - """ - Get the directory size in bytes. - """ - path = _get_path(path) - assert_dir(path) - size = 0 - for basepath, _, filenames in os.walk(path): - for filename in filenames: - filepath = os.path.join(basepath, filename) - if not os.path.islink(filepath): - size += get_file_size(filepath) - return size - - -def get_dir_size_formatted(path: PathIn) -> str: - """ - Get the directory size formatted using the right unit suffix. - """ - size = get_dir_size(path) - size_formatted = convert_size_bytes_to_string(size) - return size_formatted - - -def get_file_basename(path: PathIn) -> str: - """ - Get the file basename from the given path/url. - """ - path = _get_path(path) - basename, _ = split_filename(path) - return basename - - -def get_file_creation_date(path: PathIn) -> datetime: - """ - Get the file creation date. - """ - path = _get_path(path) - assert_file(path) - creation_timestamp = os.path.getctime(path) - creation_date = datetime.fromtimestamp(creation_timestamp) - return creation_date - - -def get_file_creation_date_formatted( - path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" -) -> str: - """ - Get the file creation date formatted using the given format. - """ - path = _get_path(path) - date = get_file_creation_date(path) - return date.strftime(format) - - -def get_file_extension(path: PathIn) -> str: - """ - Get the file extension from the given path/url. - """ - path = _get_path(path) - _, extension = split_filename(path) - return extension - - -def get_file_hash(path: PathIn, *, func: str = "md5") -> str: - """ - Get the hash of the file at the given path using - the specified algorithm function (md5 by default). - """ - path = _get_path(path) - assert_file(path) - hash = hashlib.new(func) - with open(path, "rb") as file: - for chunk in iter(lambda: file.read(4096), b""): - hash.update(chunk) - hash_hex = hash.hexdigest() - return hash_hex - - -def get_file_last_modified_date(path: PathIn) -> datetime: - """ - Get the file last modification date. - """ - path = _get_path(path) - assert_file(path) - last_modified_timestamp = os.path.getmtime(path) - last_modified_date = datetime.fromtimestamp(last_modified_timestamp) - return last_modified_date - - -def get_file_last_modified_date_formatted( - path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" -) -> str: - """ - Get the file last modification date formatted using the given format. - """ - path = _get_path(path) - date = get_file_last_modified_date(path) - return date.strftime(format) - - -def get_file_size(path: PathIn) -> int: - """ - Get the directory size in bytes. - """ - path = _get_path(path) - assert_file(path) - # size = os.stat(path).st_size - size = os.path.getsize(path) - return size - - -def get_file_size_formatted(path: PathIn) -> str: - """ - Get the directory size formatted using the right unit suffix. - """ - path = _get_path(path) - size = get_file_size(path) - size_formatted = convert_size_bytes_to_string(size) - return size_formatted - - -def get_filename(path: PathIn) -> str: - """ - Get the filename from the given path/url. - """ - path = _get_path(path) - filepath = urlsplit(path).path - filename = os.path.basename(filepath) - return filename - - -def get_parent_dir(path: PathIn, *, levels: int = 1) -> str: - """ - Get the parent directory for the given path going up N levels. - """ - path = _get_path(path) - return join_path(path, *([os.pardir] * max(1, levels))) - - -def get_permissions(path: PathIn) -> int: - """ - Get the file/directory permissions. - """ - path = _get_path(path) - assert_exists(path) - st_mode = os.stat(path).st_mode - permissions = int(str(oct(st_mode & 0o777))[2:]) - return permissions - - -def get_unique_name( - path: PathIn, - *, - prefix: str = "", - suffix: str = "", - extension: str = "", - separator: str = "-", -) -> str: - """ - Get a unique name for a directory/file at the given directory path. - """ - path = _get_path(path) - assert_dir(path) - name = "" - while True: - if prefix: - name += f"{prefix}{separator}" - uid = uuid.uuid4() - name += f"{uid}" - if suffix: - name += f"{separator}{suffix}" - if extension: - extension = extension.lstrip(".").lower() - name += f".{extension}" - if exists(join_path(path, name)): - continue - break - return name - - -def is_dir(path: PathIn) -> bool: - """ - Determine whether the specified path represents an existing directory. - """ - path = _get_path(path) - return os.path.isdir(path) - - -def is_empty(path: PathIn) -> bool: - """ - Determine whether the specified path represents an empty directory or an empty file. - """ - path = _get_path(path) - assert_exists(path) - if is_dir(path): - return is_empty_dir(path) - return is_empty_file(path) - - -def is_empty_dir(path: PathIn) -> bool: - """ - Determine whether the specified path represents an empty directory. - """ - path = _get_path(path) - assert_dir(path) - return len(os.listdir(path)) == 0 - - -def is_empty_file(path: PathIn) -> bool: - """ - Determine whether the specified path represents an empty file. - """ - path = _get_path(path) - return get_file_size(path) == 0 - - -def is_file(path: PathIn) -> bool: - """ - Determine whether the specified path represents an existing file. - """ - path = _get_path(path) - return os.path.isfile(path) - - -def join_filename(basename: str, extension: str) -> str: - """ - Create a filename joining the file basename and the extension. - """ - basename = basename.rstrip(".").strip() - extension = extension.replace(".", "").strip() - if basename and extension: - filename = f"{basename}.{extension}" - return filename - return basename or extension - - -def join_filepath(dirpath: PathIn, filename: str) -> str: - """ - Create a filepath joining the directory path and the filename. - """ - dirpath = _get_path(dirpath) - return join_path(dirpath, filename) - - -def join_path(path: PathIn, *paths: PathIn) -> str: - """ - Create a path joining path and paths. - If path is __file__ (or a .py file), the resulting path will be relative - to the directory path of the module in which it's used. - """ - path = _get_path(path) - basepath = path - if get_file_extension(path) in ["py", "pyc", "pyo"]: - basepath = os.path.dirname(os.path.realpath(path)) - paths_str = [_get_path(path).lstrip("/\\") for path in paths] - return os.path.normpath(os.path.join(basepath, *paths_str)) - - -def list_dirs(path: PathIn) -> list[str]: - """ - List all directories contained at the given directory path. - """ - path = _get_path(path) - return _filter_paths(path, os.listdir(path), predicate=is_dir) - - -def list_files(path: PathIn) -> list[str]: - """ - List all files contained at the given directory path. - """ - path = _get_path(path) - return _filter_paths(path, os.listdir(path), predicate=is_file) - - -def make_dirs(path: PathIn) -> None: - """ - Create the directories needed to ensure that the given path exists. - If a file already exists at the given path an OSError is raised. - """ - path = _get_path(path) - if is_dir(path): - return - assert_not_file(path) - os.makedirs(path, exist_ok=True) - - -def make_dirs_for_file(path: PathIn) -> None: - """ - Create the directories needed to ensure that the given path exists. - If a directory already exists at the given path an OSError is raised. - """ - path = _get_path(path) - if is_file(path): - return - assert_not_dir(path) - dirpath, _ = split_filepath(path) - if dirpath: - make_dirs(dirpath) - - -def move_dir( - path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any -) -> None: - """ - Move an existing dir from path to dest directory. - If overwrite is not allowed and dest path exists, an OSError is raised. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.move - """ - path = _get_path(path) - dest = _get_path(dest) - assert_dir(path) - assert_not_file(dest) - if not overwrite: - assert_not_exists(dest) - make_dirs(dest) - shutil.move(path, dest, **kwargs) - - -def move_file( - path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any -) -> None: - """ - Move an existing file from path to dest directory. - If overwrite is not allowed and dest path exists, an OSError is raised. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.move - """ - path = _get_path(path) - dest = _get_path(dest) - assert_file(path) - assert_not_file(dest) - dest = os.path.join(dest, get_filename(path)) - assert_not_dir(dest) - if not overwrite: - assert_not_exists(dest) - make_dirs_for_file(dest) - shutil.move(path, dest, **kwargs) - - -def read_file(path: PathIn, *, encoding: str = "utf-8") -> str: - """ - Read the content of the file at the given path using the specified encoding. - """ - path = _get_path(path) - assert_file(path) - content = "" - with open(path, encoding=encoding) as file: - content = file.read() - return content - - -def read_file_from_url(url: str, **kwargs: Any) -> str: - """ - Read the content of the file at the given url. - """ - _require_requests_installed() - response = requests.get(url, **kwargs) - response.raise_for_status() - content = response.text - return content - - -def read_file_json(path: PathIn, **kwargs: Any) -> Any: - """ - Read and decode a json encoded file at the given path. - """ - path = _get_path(path) - content = read_file(path) - data = json.loads(content, **kwargs) - return data - - -def _read_file_lines_in_range( - path: PathIn, - *, - line_start: int = 0, - line_end: int = -1, - encoding: str = "utf-8", -) -> Generator[str]: - path = _get_path(path) - line_start_negative = line_start < 0 - line_end_negative = line_end < 0 - if line_start_negative or line_end_negative: - # pre-calculate lines count only if using negative line indexes - lines_count = read_file_lines_count(path) - # normalize negative indexes - if line_start_negative: - line_start = max(0, line_start + lines_count) - if line_end_negative: - line_end = min(line_end + lines_count, lines_count - 1) - with open(path, "rb") as file: - file.seek(0) - line_index = 0 - for line in file: - if line_index >= line_start and line_index <= line_end: - yield line.decode(encoding) - line_index += 1 - - -def read_file_lines( - path: PathIn, - *, - line_start: int = 0, - line_end: int = -1, - strip_white: bool = True, - skip_empty: bool = True, - encoding: str = "utf-8", -) -> list[str]: - """ - Read file content lines. - It is possible to specify the line indexes (negative indexes too), - very useful especially when reading large files. - """ - path = _get_path(path) - assert_file(path) - if line_start == 0 and line_end == -1: - content = read_file(path, encoding=encoding) - lines = content.splitlines() - else: - lines = list( - _read_file_lines_in_range( - path, - line_start=line_start, - line_end=line_end, - encoding=encoding, - ) - ) - if strip_white: - lines = [line.strip() for line in lines] - if skip_empty: - lines = [line for line in lines if line] - return lines - - -def read_file_lines_count(path: PathIn) -> int: - """ - Read file lines count. - """ - path = _get_path(path) - assert_file(path) - lines_count = 0 - with open(path, "rb") as file: - file.seek(0) - lines_count = sum(1 for line in file) - return lines_count - - -def remove_dir(path: PathIn, **kwargs: Any) -> bool: - """ - Remove a directory at the given path and all its content. - If the directory is removed with success returns True, otherwise False. - More informations about kwargs supported options here: - https://docs.python.org/3/library/shutil.html#shutil.rmtree - """ - path = _get_path(path) - if not exists(path): - return False - assert_dir(path) - shutil.rmtree(path, **kwargs) - return not exists(path) - - -def remove_dir_content(path: PathIn) -> None: - """ - Removes all directory content (both sub-directories and files). - """ - path = _get_path(path) - assert_dir(path) - remove_dirs(*list_dirs(path)) - remove_files(*list_files(path)) - - -def remove_dirs(*paths: PathIn) -> None: - """ - Remove multiple directories at the given paths and all their content. - """ - for path in paths: - remove_dir(path) - - -def remove_file(path: PathIn) -> bool: - """ - Remove a file at the given path. - If the file is removed with success returns True, otherwise False. - """ - path = _get_path(path) - if not exists(path): - return False - assert_file(path) - os.remove(path) - return not exists(path) - - -def remove_files(*paths: PathIn) -> None: - """ - Remove multiple files at the given paths. - """ - for path in paths: - remove_file(path) - - -def rename_dir(path: PathIn, name: str) -> None: - """ - Rename a directory with the given name. - If a directory or a file with the given name already exists, an OSError is raised. - """ - path = _get_path(path) - assert_dir(path) - comps = list(os.path.split(path)) - comps[-1] = name - dest = os.path.join(*comps) - assert_not_exists(dest) - os.rename(path, dest) - - -def rename_file(path: PathIn, name: str) -> None: - """ - Rename a file with the given name. - If a directory or a file with the given name already exists, an OSError is raised. - """ - path = _get_path(path) - assert_file(path) - dirpath, filename = split_filepath(path) - dest = join_filepath(dirpath, name) - assert_not_exists(dest) - os.rename(path, dest) - - -def rename_file_basename(path: PathIn, basename: str) -> None: - """ - Rename a file basename with the given basename. - """ - path = _get_path(path) - extension = get_file_extension(path) - filename = join_filename(basename, extension) - rename_file(path, filename) - - -def rename_file_extension(path: PathIn, extension: str) -> None: - """ - Rename a file extension with the given extension. - """ - path = _get_path(path) - basename = get_file_basename(path) - filename = join_filename(basename, extension) - rename_file(path, filename) - - -def replace_dir(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None: - """ - Replace directory at the specified path with the directory located at src. - If autodelete, the src directory will be removed at the end of the operation. - Optimized for large files. - """ - path = _get_path(path) - src = _get_path(src) - assert_not_file(path) - assert_dir(src) - - if path == src: - return - - make_dirs(path) - - dirpath, dirname = split_filepath(path) - # safe temporary name to avoid clashes with existing files/directories - temp_dirname = get_unique_name(dirpath) - temp_dest = join_path(dirpath, temp_dirname) - copy_dir_content(src, temp_dest) - - if exists(path): - temp_dirname = get_unique_name(dirpath) - temp_path = join_path(dirpath, temp_dirname) - rename_dir(path=path, name=temp_dirname) - rename_dir(path=temp_dest, name=dirname) - remove_dir(path=temp_path) - else: - rename_dir(path=temp_dest, name=dirname) - - if autodelete: - remove_dir(path=src) - - -def replace_file(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None: - """ - Replace file at the specified path with the file located at src. - If autodelete, the src file will be removed at the end of the operation. - Optimized for large files. - """ - path = _get_path(path) - src = _get_path(src) - assert_not_dir(path) - assert_file(src) - if path == src: - return - - make_dirs_for_file(path) - - dirpath, filename = split_filepath(path) - _, extension = split_filename(filename) - # safe temporary name to avoid clashes with existing files/directories - temp_filename = get_unique_name(dirpath, extension=extension) - temp_dest = join_path(dirpath, temp_filename) - copy_file(path=src, dest=temp_dest, overwrite=False) - - if exists(path): - temp_filename = get_unique_name(dirpath, extension=extension) - temp_path = join_path(dirpath, temp_filename) - rename_file(path=path, name=temp_filename) - rename_file(path=temp_dest, name=filename) - remove_file(path=temp_path) - else: - rename_file(path=temp_dest, name=filename) - - if autodelete: - remove_file(path=src) - - -def _search_paths(path: PathIn, pattern: str) -> list[str]: - """ - Search all paths relative to path matching the given pattern. - """ - path = _get_path(path) - assert_dir(path) - pathname = os.path.join(path, pattern) - paths = glob.glob(pathname, recursive=True) - return paths - - -def search_dirs(path: PathIn, pattern: str = "**/*") -> list[str]: - """ - Search for directories at path matching the given pattern. - """ - path = _get_path(path) - return _filter_paths(path, _search_paths(path, pattern), predicate=is_dir) - - -def search_files(path: PathIn, pattern: str = "**/*.*") -> list[str]: - """ - Search for files at path matching the given pattern. - """ - path = _get_path(path) - return _filter_paths(path, _search_paths(path, pattern), predicate=is_file) - - -def set_permissions(path: PathIn, value: int) -> None: - """ - Set the file/directory permissions. - """ - path = _get_path(path) - assert_exists(path) - permissions = int(str(value), 8) & 0o777 - os.chmod(path, permissions) - - -def split_filename(path: PathIn) -> tuple[str, str]: - """ - Split a filename and returns its basename and extension. - """ - path = _get_path(path) - filename = get_filename(path) - basename, extension = os.path.splitext(filename) - extension = extension.replace(".", "").strip() - return (basename, extension) - - -def split_filepath(path: PathIn) -> tuple[str, str]: - """ - Split a filepath and returns its directory-path and filename. - """ - path = _get_path(path) - dirpath = os.path.dirname(path) - filename = get_filename(path) - return (dirpath, filename) - - -def split_path(path: PathIn) -> list[str]: - """ - Split a path and returns its path-names. - """ - path = _get_path(path) - head, tail = os.path.split(path) - names = head.split(os.sep) + [tail] - names = list(filter(None, names)) - return names - - -def transform_filepath( - path: PathIn, - *, - dirpath: str | Callable[[str], str] | None = None, - basename: str | Callable[[str], str] | None = None, - extension: str | Callable[[str], str] | None = None, -) -> str: - """ - Trasform a filepath by applying the provided optional changes. - - :param path: The path. - :type path: PathIn - :param dirpath: The new dirpath or a callable. - :type dirpath: str | Callable[[str], str] | None - :param basename: The new basename or a callable. - :type basename: str | Callable[[str], str] | None - :param extension: The new extension or a callable. - :type extension: str | Callable[[str], str] | None - - :returns: The filepath with the applied changes. - :rtype: str - """ - - def _get_value( - new_value: str | Callable[[str], str] | None, - old_value: str, - ) -> str: - value = old_value - if new_value is not None: - if callable(new_value): - value = new_value(old_value) - elif isinstance(new_value, str): - value = new_value - else: - value = old_value - return value - - if all([dirpath is None, basename is None, extension is None]): - raise ValueError( - "Invalid arguments: at least one of " - "'dirpath', 'basename' or 'extension' is required." - ) - old_dirpath, old_filename = split_filepath(path) - old_basename, old_extension = split_filename(old_filename) - new_dirpath = _get_value(dirpath, old_dirpath) - new_basename = _get_value(basename, old_basename) - new_extension = _get_value(extension, old_extension) - if not any([new_dirpath, new_basename, new_extension]): - raise ValueError( - "Invalid arguments: at least one of " - "'dirpath', 'basename' or 'extension' is required." - ) - new_filename = join_filename(new_basename, new_extension) - new_filepath = join_filepath(new_dirpath, new_filename) - return new_filepath - - -def _write_file_atomic( - path: PathIn, - content: str, - *, - append: bool = False, - encoding: str = "utf-8", -) -> None: - path = _get_path(path) - mode = "a" if append else "w" - if append: - content = read_file(path, encoding=encoding) + content - dirpath, _ = split_filepath(path) - auto_delete_temp_file = False if platform.system() == "Windows" else True - try: - with tempfile.NamedTemporaryFile( - mode=mode, - dir=dirpath, - delete=auto_delete_temp_file, - # delete_on_close=False, # supported since Python >= 3.12 - encoding=encoding, - ) as file: - file.write(content) - file.flush() - os.fsync(file.fileno()) - temp_path = file.name - permissions = get_permissions(path) if exists(path) else None - os.replace(temp_path, path) - if permissions: - set_permissions(path, permissions) - except FileNotFoundError: - # success - the NamedTemporaryFile has not been able - # to remove the temp file on __exit__ because the temp file - # has replaced atomically the file at path. - pass - finally: - # attempt for fixing #121 (on Windows destroys created file on exit) - # manually delete the temporary file if still exists - if temp_path and exists(temp_path): - remove_file(temp_path) - - -def _write_file_non_atomic( - path: PathIn, - content: str, - *, - append: bool = False, - encoding: str = "utf-8", -) -> None: - mode = "a" if append else "w" - with open(path, mode, encoding=encoding) as file: - file.write(content) - - -def write_file( - path: PathIn, - content: str, - *, - append: bool = False, - encoding: str = "utf-8", - atomic: bool = False, -) -> None: - """ - Write file with the specified content at the given path. - """ - path = _get_path(path) - assert_not_dir(path) - make_dirs_for_file(path) - write_file_func = _write_file_atomic if atomic else _write_file_non_atomic - write_file_func( - path, - content, - append=append, - encoding=encoding, - ) - - -def write_file_json( - path: PathIn, - data: Any, - encoding: str = "utf-8", - atomic: bool = False, - **kwargs: Any, -) -> None: - """ - Write a json file at the given path with the specified data encoded in json format. - """ - path = _get_path(path) - - def default_encoder(obj: Any) -> Any: - if isinstance(obj, datetime): - return obj.isoformat() - elif isinstance(obj, set): - return list(obj) - return str(obj) - - kwargs.setdefault("default", default_encoder) - content = json.dumps(data, **kwargs) - write_file( - path, - content, - append=False, - encoding=encoding, - atomic=atomic, - ) diff --git a/fsutil/archives.py b/fsutil/archives.py new file mode 100644 index 0000000..c7531cd --- /dev/null +++ b/fsutil/archives.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import os +import sys +import tarfile +import zipfile +from collections.abc import Callable, Iterable +from typing import Literal + +from fsutil.args import get_path as _get_path +from fsutil.checks import ( + assert_exists, + assert_file, + assert_not_dir, + assert_not_exists, + assert_not_file, + is_dir, + is_file, +) +from fsutil.operations import make_dirs, make_dirs_for_file, remove_file +from fsutil.paths import get_filename, join_path +from fsutil.types import PathIn + + +def create_tar_file( + path: PathIn, + content_paths: list[PathIn], + *, + overwrite: bool = True, + compression: str = "", # literal: gz, bz2, xz +) -> None: + """ + Create tar file at path compressing directories/files listed in content_paths. + If overwrite is allowed and dest tar already exists, it will be overwritten. + """ + path = _get_path(path) + assert_not_dir(path) + if not overwrite: + assert_not_exists(path) + make_dirs_for_file(path) + + def _write_content_to_tar_file( + file: tarfile.TarFile, content_path: PathIn, basedir: str = "" + ) -> None: + path = _get_path(content_path) + assert_exists(path) + if is_file(path): + filename = get_filename(path) + filepath = join_path(basedir, filename) + file.add(path, filepath) + elif is_dir(path): + for item_name in os.listdir(path): + item_path = join_path(path, item_name) + item_basedir = ( + join_path(basedir, item_name) if is_dir(item_path) else basedir + ) + _write_content_to_tar_file(file, item_path, item_basedir) + + mode = f"w:{compression}" if compression else "w" + with tarfile.open(path, mode=mode) as file: # type: ignore + for content_path in content_paths: + _write_content_to_tar_file(file, content_path) + + +def create_zip_file( + path: PathIn, + content_paths: list[PathIn], + *, + overwrite: bool = True, + compression: int = zipfile.ZIP_DEFLATED, +) -> None: + """ + Create zip file at path compressing directories/files listed in content_paths. + If overwrite is allowed and dest zip already exists, it will be overwritten. + """ + path = _get_path(path) + assert_not_dir(path) + if not overwrite: + assert_not_exists(path) + make_dirs_for_file(path) + + def _write_content_to_zip_file( + file: zipfile.ZipFile, content_path: PathIn, basedir: str = "" + ) -> None: + path = _get_path(content_path) + assert_exists(path) + if is_file(path): + filename = get_filename(path) + filepath = join_path(basedir, filename) + file.write(path, filepath) + elif is_dir(path): + for item_name in os.listdir(path): + item_path = join_path(path, item_name) + item_basedir = ( + join_path(basedir, item_name) if is_dir(item_path) else basedir + ) + _write_content_to_zip_file(file, item_path, item_basedir) + + with zipfile.ZipFile(path, "w", compression) as file: + for content_path in content_paths: + _write_content_to_zip_file(file, content_path) + + +def extract_tar_file( + path: PathIn, + dest: PathIn, + *, + autodelete: bool = False, + content_paths: Iterable[tarfile.TarInfo] | None = None, + filter: ( + Callable[[tarfile.TarInfo, str], tarfile.TarInfo | None] + | Literal["fully_trusted", "tar", "data"] + ) + | None = None, +) -> None: + """ + Extract tar file at path to dest path. + If autodelete, the archive will be deleted after extraction. + If content_paths list is defined, + only listed items will be extracted, otherwise all. + """ + path = _get_path(path) + dest = _get_path(dest) + assert_file(path) + assert_not_file(dest) + make_dirs(dest) + with tarfile.TarFile(path, "r") as file: + if sys.version_info < (3, 12): + file.extractall(dest, members=content_paths) + else: + # https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter + file.extractall( + dest, + members=content_paths, + numeric_owner=False, + filter=(filter or "data"), + ) + if autodelete: + remove_file(path) + + +def extract_zip_file( + path: PathIn, + dest: PathIn, + *, + autodelete: bool = False, + content_paths: Iterable[str | zipfile.ZipInfo] | None = None, +) -> None: + """ + Extract zip file at path to dest path. + If autodelete, the archive will be deleted after extraction. + If content_paths list is defined, + only listed items will be extracted, otherwise all. + """ + path = _get_path(path) + dest = _get_path(dest) + assert_file(path) + assert_not_file(dest) + make_dirs(dest) + with zipfile.ZipFile(path, "r") as file: + file.extractall(dest, members=content_paths) + if autodelete: + remove_file(path) diff --git a/fsutil/args.py b/fsutil/args.py new file mode 100644 index 0000000..2e7e5fd --- /dev/null +++ b/fsutil/args.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import os + +from fsutil.types import PathIn + + +def get_path(path: PathIn) -> str: + if path is None: + return None + if isinstance(path, str): + return os.path.normpath(path) + return str(path) diff --git a/fsutil/checks.py b/fsutil/checks.py new file mode 100644 index 0000000..23e64fa --- /dev/null +++ b/fsutil/checks.py @@ -0,0 +1,114 @@ +from __future__ import annotations + +import os + +from fsutil.args import get_path as _get_path +from fsutil.types import PathIn + + +def assert_dir(path: PathIn) -> None: + """ + Raise an OSError if the given path doesn't exist or it is not a directory. + """ + path = _get_path(path) + if not is_dir(path): + raise OSError(f"Invalid directory path: {path}") + + +def assert_exists(path: PathIn) -> None: + """ + Raise an OSError if the given path doesn't exist. + """ + path = _get_path(path) + if not exists(path): + raise OSError(f"Invalid item path: {path}") + + +def assert_file(path: PathIn) -> None: + """ + Raise an OSError if the given path doesn't exist or it is not a file. + """ + path = _get_path(path) + if not is_file(path): + raise OSError(f"Invalid file path: {path}") + + +def assert_not_dir(path: PathIn) -> None: + """ + Raise an OSError if the given path is an existing directory. + """ + path = _get_path(path) + if is_dir(path): + raise OSError(f"Invalid path, directory already exists: {path}") + + +def assert_not_exists(path: PathIn) -> None: + """ + Raise an OSError if the given path already exists. + """ + path = _get_path(path) + if exists(path): + raise OSError(f"Invalid path, item already exists: {path}") + + +def assert_not_file(path: PathIn) -> None: + """ + Raise an OSError if the given path is an existing file. + """ + path = _get_path(path) + if is_file(path): + raise OSError(f"Invalid path, file already exists: {path}") + + +def exists(path: PathIn) -> bool: + """ + Check if a directory of a file exists at the given path. + """ + path = _get_path(path) + return os.path.exists(path) + + +def is_dir(path: PathIn) -> bool: + """ + Determine whether the specified path represents an existing directory. + """ + path = _get_path(path) + return os.path.isdir(path) + + +def is_empty(path: PathIn) -> bool: + """ + Determine whether the specified path represents an empty directory or an empty file. + """ + path = _get_path(path) + assert_exists(path) + if is_dir(path): + return is_empty_dir(path) + return is_empty_file(path) + + +def is_empty_dir(path: PathIn) -> bool: + """ + Determine whether the specified path represents an empty directory. + """ + path = _get_path(path) + assert_dir(path) + return len(os.listdir(path)) == 0 + + +def is_empty_file(path: PathIn) -> bool: + """ + Determine whether the specified path represents an empty file. + """ + from fsutil.info import get_file_size + + path = _get_path(path) + return get_file_size(path) == 0 + + +def is_file(path: PathIn) -> bool: + """ + Determine whether the specified path represents an existing file. + """ + path = _get_path(path) + return os.path.isfile(path) diff --git a/fsutil/converters.py b/fsutil/converters.py new file mode 100644 index 0000000..2399269 --- /dev/null +++ b/fsutil/converters.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +SIZE_UNITS = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + + +def convert_size_bytes_to_string(size: int) -> str: + """ + Convert the given size bytes to string using the right unit suffix. + """ + size_num = float(size) + units = SIZE_UNITS + factor = 0 + factor_limit = len(units) - 1 + while (size_num >= 1024) and (factor <= factor_limit): + size_num /= 1024 + factor += 1 + size_units = units[factor] + size_str = f"{size_num:.2f}" if (factor > 1) else f"{size_num:.0f}" + size_str = f"{size_str} {size_units}" + return size_str + + +def convert_size_string_to_bytes(size: str) -> float | int: + """ + Convert the given size string to bytes. + """ + units = [item.lower() for item in SIZE_UNITS] + parts = size.strip().replace(" ", " ").split(" ") + amount = float(parts[0]) + unit = parts[1] + factor = units.index(unit.lower()) + if not factor: + return amount + return int((1024**factor) * amount) diff --git a/fsutil/deps.py b/fsutil/deps.py new file mode 100644 index 0000000..4bd5d83 --- /dev/null +++ b/fsutil/deps.py @@ -0,0 +1,13 @@ +from types import ModuleType + + +def require_requests() -> ModuleType: + try: + import requests + + return requests + except ImportError as error: + raise ModuleNotFoundError( + "'requests' module is not installed, " + "it can be installed by running: 'pip install requests'" + ) from error diff --git a/fsutil/info.py b/fsutil/info.py new file mode 100644 index 0000000..3507c95 --- /dev/null +++ b/fsutil/info.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import hashlib +import os +from datetime import datetime + +from fsutil.args import get_path as _get_path +from fsutil.checks import assert_dir, assert_file +from fsutil.converters import convert_size_bytes_to_string +from fsutil.operations import search_files +from fsutil.types import PathIn + + +def get_dir_creation_date(path: PathIn) -> datetime: + """ + Get the directory creation date. + """ + path = _get_path(path) + assert_dir(path) + creation_timestamp = os.path.getctime(path) + creation_date = datetime.fromtimestamp(creation_timestamp) + return creation_date + + +def get_dir_creation_date_formatted( + path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" +) -> str: + """ + Get the directory creation date formatted using the given format. + """ + path = _get_path(path) + date = get_dir_creation_date(path) + return date.strftime(format) + + +def get_dir_hash(path: PathIn, *, func: str = "md5") -> str: + """ + Get the hash of the directory at the given path using + the specified algorithm function (md5 by default). + """ + path = _get_path(path) + assert_dir(path) + hash_ = hashlib.new(func) + files = search_files(path) + for file in sorted(files): + file_hash = get_file_hash(file, func=func) + file_hash_b = bytes(file_hash, "utf-8") + hash_.update(file_hash_b) + hash_hex = hash_.hexdigest() + return hash_hex + + +def get_dir_last_modified_date(path: PathIn) -> datetime: + """ + Get the directory last modification date. + """ + path = _get_path(path) + assert_dir(path) + last_modified_timestamp = os.path.getmtime(path) + for basepath, dirnames, filenames in os.walk(path): + for dirname in dirnames: + dirpath = os.path.join(basepath, dirname) + last_modified_timestamp = max( + last_modified_timestamp, os.path.getmtime(dirpath) + ) + for filename in filenames: + filepath = os.path.join(basepath, filename) + last_modified_timestamp = max( + last_modified_timestamp, os.path.getmtime(filepath) + ) + last_modified_date = datetime.fromtimestamp(last_modified_timestamp) + return last_modified_date + + +def get_dir_last_modified_date_formatted( + path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" +) -> str: + """ + Get the directory last modification date formatted using the given format. + """ + path = _get_path(path) + date = get_dir_last_modified_date(path) + return date.strftime(format) + + +def get_dir_size(path: PathIn) -> int: + """ + Get the directory size in bytes. + """ + path = _get_path(path) + assert_dir(path) + size = 0 + for basepath, _, filenames in os.walk(path): + for filename in filenames: + filepath = os.path.join(basepath, filename) + if not os.path.islink(filepath): + size += get_file_size(filepath) + return size + + +def get_dir_size_formatted(path: PathIn) -> str: + """ + Get the directory size formatted using the right unit suffix. + """ + size = get_dir_size(path) + size_formatted = convert_size_bytes_to_string(size) + return size_formatted + + +def get_file_creation_date(path: PathIn) -> datetime: + """ + Get the file creation date. + """ + path = _get_path(path) + assert_file(path) + creation_timestamp = os.path.getctime(path) + creation_date = datetime.fromtimestamp(creation_timestamp) + return creation_date + + +def get_file_creation_date_formatted( + path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" +) -> str: + """ + Get the file creation date formatted using the given format. + """ + path = _get_path(path) + date = get_file_creation_date(path) + return date.strftime(format) + + +def get_file_hash(path: PathIn, *, func: str = "md5") -> str: + """ + Get the hash of the file at the given path using + the specified algorithm function (md5 by default). + """ + path = _get_path(path) + assert_file(path) + hash = hashlib.new(func) + with open(path, "rb") as file: + for chunk in iter(lambda: file.read(4096), b""): + hash.update(chunk) + hash_hex = hash.hexdigest() + return hash_hex + + +def get_file_last_modified_date(path: PathIn) -> datetime: + """ + Get the file last modification date. + """ + path = _get_path(path) + assert_file(path) + last_modified_timestamp = os.path.getmtime(path) + last_modified_date = datetime.fromtimestamp(last_modified_timestamp) + return last_modified_date + + +def get_file_last_modified_date_formatted( + path: PathIn, *, format: str = "%Y-%m-%d %H:%M:%S" +) -> str: + """ + Get the file last modification date formatted using the given format. + """ + path = _get_path(path) + date = get_file_last_modified_date(path) + return date.strftime(format) + + +def get_file_size(path: PathIn) -> int: + """ + Get the directory size in bytes. + """ + path = _get_path(path) + assert_file(path) + # size = os.stat(path).st_size + size = os.path.getsize(path) + return size + + +def get_file_size_formatted(path: PathIn) -> str: + """ + Get the directory size formatted using the right unit suffix. + """ + path = _get_path(path) + size = get_file_size(path) + size_formatted = convert_size_bytes_to_string(size) + return size_formatted diff --git a/fsutil/io.py b/fsutil/io.py new file mode 100644 index 0000000..0ce66c7 --- /dev/null +++ b/fsutil/io.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import json +import os +import platform +import tempfile +from collections.abc import Generator +from datetime import datetime +from typing import Any + +from fsutil.args import get_path as _get_path +from fsutil.checks import assert_file, assert_not_dir, exists +from fsutil.deps import require_requests +from fsutil.operations import make_dirs_for_file, remove_file +from fsutil.paths import split_filepath +from fsutil.perms import get_permissions, set_permissions +from fsutil.types import PathIn + + +def read_file(path: PathIn, *, encoding: str = "utf-8") -> str: + """ + Read the content of the file at the given path using the specified encoding. + """ + path = _get_path(path) + assert_file(path) + content = "" + with open(path, encoding=encoding) as file: + content = file.read() + return content + + +def read_file_from_url(url: str, **kwargs: Any) -> str: + """ + Read the content of the file at the given url. + """ + requests = require_requests() + response = requests.get(url, **kwargs) + response.raise_for_status() + content = str(response.text) + return content + + +def read_file_json(path: PathIn, **kwargs: Any) -> Any: + """ + Read and decode a json encoded file at the given path. + """ + path = _get_path(path) + content = read_file(path) + data = json.loads(content, **kwargs) + return data + + +def _read_file_lines_in_range( + path: PathIn, + *, + line_start: int = 0, + line_end: int = -1, + encoding: str = "utf-8", +) -> Generator[str]: + path = _get_path(path) + line_start_negative = line_start < 0 + line_end_negative = line_end < 0 + if line_start_negative or line_end_negative: + # pre-calculate lines count only if using negative line indexes + lines_count = read_file_lines_count(path) + # normalize negative indexes + if line_start_negative: + line_start = max(0, line_start + lines_count) + if line_end_negative: + line_end = min(line_end + lines_count, lines_count - 1) + with open(path, "rb") as file: + file.seek(0) + line_index = 0 + for line in file: + if line_index >= line_start and line_index <= line_end: + yield line.decode(encoding) + line_index += 1 + + +def read_file_lines( + path: PathIn, + *, + line_start: int = 0, + line_end: int = -1, + strip_white: bool = True, + skip_empty: bool = True, + encoding: str = "utf-8", +) -> list[str]: + """ + Read file content lines. + It is possible to specify the line indexes (negative indexes too), + very useful especially when reading large files. + """ + path = _get_path(path) + assert_file(path) + if line_start == 0 and line_end == -1: + content = read_file(path, encoding=encoding) + lines = content.splitlines() + else: + lines = list( + _read_file_lines_in_range( + path, + line_start=line_start, + line_end=line_end, + encoding=encoding, + ) + ) + if strip_white: + lines = [line.strip() for line in lines] + if skip_empty: + lines = [line for line in lines if line] + return lines + + +def read_file_lines_count(path: PathIn) -> int: + """ + Read file lines count. + """ + path = _get_path(path) + assert_file(path) + lines_count = 0 + with open(path, "rb") as file: + file.seek(0) + lines_count = sum(1 for line in file) + return lines_count + + +def _write_file_atomic( + path: PathIn, + content: str, + *, + append: bool = False, + encoding: str = "utf-8", +) -> None: + path = _get_path(path) + mode = "a" if append else "w" + if append: + content = read_file(path, encoding=encoding) + content + dirpath, _ = split_filepath(path) + auto_delete_temp_file = False if platform.system() == "Windows" else True + try: + with tempfile.NamedTemporaryFile( + mode=mode, + dir=dirpath, + delete=auto_delete_temp_file, + # delete_on_close=False, # supported since Python >= 3.12 + encoding=encoding, + ) as file: + file.write(content) + file.flush() + os.fsync(file.fileno()) + temp_path = file.name + permissions = get_permissions(path) if exists(path) else None + os.replace(temp_path, path) + if permissions: + set_permissions(path, permissions) + except FileNotFoundError: + # success - the NamedTemporaryFile has not been able + # to remove the temp file on __exit__ because the temp file + # has replaced atomically the file at path. + pass + finally: + # attempt for fixing #121 (on Windows destroys created file on exit) + # manually delete the temporary file if still exists + if temp_path and exists(temp_path): + remove_file(temp_path) + + +def _write_file_non_atomic( + path: PathIn, + content: str, + *, + append: bool = False, + encoding: str = "utf-8", +) -> None: + mode = "a" if append else "w" + with open(path, mode, encoding=encoding) as file: + file.write(content) + + +def write_file( + path: PathIn, + content: str, + *, + append: bool = False, + encoding: str = "utf-8", + atomic: bool = False, +) -> None: + """ + Write file with the specified content at the given path. + """ + path = _get_path(path) + assert_not_dir(path) + make_dirs_for_file(path) + write_file_func = _write_file_atomic if atomic else _write_file_non_atomic + write_file_func( + path, + content, + append=append, + encoding=encoding, + ) + + +def write_file_json( + path: PathIn, + data: Any, + encoding: str = "utf-8", + atomic: bool = False, + **kwargs: Any, +) -> None: + """ + Write a json file at the given path with the specified data encoded in json format. + """ + path = _get_path(path) + + def default_encoder(obj: Any) -> Any: + if isinstance(obj, datetime): + return obj.isoformat() + elif isinstance(obj, set): + return list(obj) + return str(obj) + + kwargs.setdefault("default", default_encoder) + content = json.dumps(data, **kwargs) + write_file( + path, + content, + append=False, + encoding=encoding, + atomic=atomic, + ) diff --git a/fsutil/operations.py b/fsutil/operations.py new file mode 100644 index 0000000..71d21f9 --- /dev/null +++ b/fsutil/operations.py @@ -0,0 +1,536 @@ +from __future__ import annotations + +import glob +import os +import re +import shutil +import tempfile +import uuid +from collections.abc import Callable +from typing import Any + +from fsutil.args import get_path as _get_path +from fsutil.checks import ( + assert_dir, + assert_file, + assert_not_dir, + assert_not_exists, + assert_not_file, + exists, + is_dir, + is_empty_dir, + is_empty_file, + is_file, +) +from fsutil.deps import require_requests +from fsutil.paths import ( + get_file_basename, + get_file_extension, + get_filename, + get_unique_name, + join_filename, + join_filepath, + join_path, + split_filename, + split_filepath, +) +from fsutil.types import PathIn + + +def _clean_dir_empty_dirs(path: PathIn) -> None: + path = _get_path(path) + for basepath, dirnames, _ in os.walk(path, topdown=False): + for dirname in dirnames: + dirpath = os.path.join(basepath, dirname) + if is_empty_dir(dirpath): + remove_dir(dirpath) + + +def _clean_dir_empty_files(path: PathIn) -> None: + path = _get_path(path) + for basepath, _, filenames in os.walk(path, topdown=False): + for filename in filenames: + filepath = os.path.join(basepath, filename) + if is_empty_file(filepath): + remove_file(filepath) + + +def clean_dir(path: PathIn, *, dirs: bool = True, files: bool = True) -> None: + """ + Clean a directory by removing empty directories and/or empty files. + """ + path = _get_path(path) + assert_dir(path) + if files: + _clean_dir_empty_files(path) + if dirs: + _clean_dir_empty_dirs(path) + + +def copy_dir( + path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any +) -> None: + """ + Copy the directory at the given path and all its content to dest path. + If overwrite is not allowed and dest path exists, an OSError is raised. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + path = _get_path(path) + dest = _get_path(dest) + assert_dir(path) + dirname = os.path.basename(os.path.normpath(path)) + dest = os.path.join(dest, dirname) + assert_not_file(dest) + if not overwrite: + assert_not_exists(dest) + copy_dir_content(path, dest, **kwargs) + + +def copy_dir_content(path: PathIn, dest: PathIn, **kwargs: Any) -> None: + """ + Copy the content of the directory at the given path to dest path. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.copytree + """ + path = _get_path(path) + dest = _get_path(dest) + assert_dir(path) + assert_not_file(dest) + make_dirs(dest) + kwargs.setdefault("dirs_exist_ok", True) + shutil.copytree(path, dest, **kwargs) + + +def copy_file( + path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any +) -> None: + """ + Copy the file at the given path and its metadata to dest path. + If overwrite is not allowed and dest path exists, an OSError is raised. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.copy2 + """ + path = _get_path(path) + dest = _get_path(dest) + assert_file(path) + assert_not_dir(dest) + if not overwrite: + assert_not_exists(dest) + make_dirs_for_file(dest) + shutil.copy2(path, dest, **kwargs) + + +def create_dir(path: PathIn, *, overwrite: bool = False) -> None: + """ + Create directory at the given path. + If overwrite is not allowed and path exists, an OSError is raised. + """ + path = _get_path(path) + assert_not_file(path) + if not overwrite: + assert_not_exists(path) + make_dirs(path) + + +def create_file(path: PathIn, content: str = "", *, overwrite: bool = False) -> None: + """ + Create file with the specified content at the given path. + If overwrite is not allowed and path exists, an OSError is raised. + """ + from fsutil.io import write_file + + path = _get_path(path) + assert_not_dir(path) + if not overwrite: + assert_not_exists(path) + write_file(path, content) + + +def delete_dir(path: PathIn) -> bool: + """ + Alias for remove_dir. + """ + removed = remove_dir(path) + return removed + + +def delete_dir_content(path: PathIn) -> None: + """ + Alias for remove_dir_content. + """ + remove_dir_content(path) + + +def delete_dirs(*paths: PathIn) -> None: + """ + Alias for remove_dirs. + """ + remove_dirs(*paths) + + +def delete_file(path: PathIn) -> bool: + """ + Alias for remove_file. + """ + removed = remove_file(path) + return removed + + +def delete_files(*paths: PathIn) -> None: + """ + Alias for remove_files. + """ + remove_files(*paths) + + +def download_file( + url: str, + *, + dirpath: PathIn | None = None, + filename: str | None = None, + chunk_size: int = 8192, + **kwargs: Any, +) -> str: + """ + Download a file from url to dirpath. + If dirpath is not provided, the file will be downloaded to a temp directory. + If filename is provided, the file will be named using filename. + It is possible to pass extra request options + (eg. for authentication) using **kwargs. + """ + requests = require_requests() + # https://stackoverflow.com/a/16696317/2096218 + + kwargs["stream"] = True + with requests.get(url, **kwargs) as response: + response.raise_for_status() + + # build filename + if not filename: + # detect filename from headers + content_disposition = response.headers.get("content-disposition", "") or "" + filename_pattern = r'filename="(.*)"' + filename_match = re.search(filename_pattern, content_disposition) + if filename_match: + filename = filename_match.group(1) + # or detect filename from url + if not filename: + filename = get_filename(url) + # or fallback to a unique name + if not filename: + filename_uuid = str(uuid.uuid4()) + filename = f"download-{filename_uuid}" + + # build filepath + dirpath = dirpath or tempfile.gettempdir() + dirpath = _get_path(dirpath) + filepath = join_path(dirpath, filename) + make_dirs_for_file(filepath) + + # write file to disk + with open(filepath, "wb") as file: + for chunk in response.iter_content(chunk_size=chunk_size): + if chunk: + file.write(chunk) + return filepath + + +def _filter_paths( + basepath: str, + relpaths: list[str], + *, + predicate: Callable[[str], bool] | None = None, +) -> list[str]: + """ + Filter paths relative to basepath according to the optional predicate function. + If predicate is defined, paths are filtered using it, + otherwise all paths will be listed. + """ + paths = [] + for relpath in relpaths: + abspath = os.path.join(basepath, relpath) + if predicate is None or predicate(abspath): + paths.append(abspath) + paths.sort() + return paths + + +def list_dirs(path: PathIn) -> list[str]: + """ + List all directories contained at the given directory path. + """ + path = _get_path(path) + return _filter_paths(path, os.listdir(path), predicate=is_dir) + + +def list_files(path: PathIn) -> list[str]: + """ + List all files contained at the given directory path. + """ + path = _get_path(path) + return _filter_paths(path, os.listdir(path), predicate=is_file) + + +def make_dirs(path: PathIn) -> None: + """ + Create the directories needed to ensure that the given path exists. + If a file already exists at the given path an OSError is raised. + """ + path = _get_path(path) + if is_dir(path): + return + assert_not_file(path) + os.makedirs(path, exist_ok=True) + + +def make_dirs_for_file(path: PathIn) -> None: + """ + Create the directories needed to ensure that the given path exists. + If a directory already exists at the given path an OSError is raised. + """ + path = _get_path(path) + if is_file(path): + return + assert_not_dir(path) + dirpath, _ = split_filepath(path) + if dirpath: + make_dirs(dirpath) + + +def move_dir( + path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any +) -> None: + """ + Move an existing dir from path to dest directory. + If overwrite is not allowed and dest path exists, an OSError is raised. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.move + """ + path = _get_path(path) + dest = _get_path(dest) + assert_dir(path) + assert_not_file(dest) + if not overwrite: + assert_not_exists(dest) + make_dirs(dest) + shutil.move(path, dest, **kwargs) + + +def move_file( + path: PathIn, dest: PathIn, *, overwrite: bool = False, **kwargs: Any +) -> None: + """ + Move an existing file from path to dest directory. + If overwrite is not allowed and dest path exists, an OSError is raised. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.move + """ + path = _get_path(path) + dest = _get_path(dest) + assert_file(path) + assert_not_file(dest) + dest = os.path.join(dest, get_filename(path)) + assert_not_dir(dest) + if not overwrite: + assert_not_exists(dest) + make_dirs_for_file(dest) + shutil.move(path, dest, **kwargs) + + +def remove_dir(path: PathIn, **kwargs: Any) -> bool: + """ + Remove a directory at the given path and all its content. + If the directory is removed with success returns True, otherwise False. + More informations about kwargs supported options here: + https://docs.python.org/3/library/shutil.html#shutil.rmtree + """ + path = _get_path(path) + if not exists(path): + return False + assert_dir(path) + shutil.rmtree(path, **kwargs) + return not exists(path) + + +def remove_dir_content(path: PathIn) -> None: + """ + Removes all directory content (both sub-directories and files). + """ + path = _get_path(path) + assert_dir(path) + remove_dirs(*list_dirs(path)) + remove_files(*list_files(path)) + + +def remove_dirs(*paths: PathIn) -> None: + """ + Remove multiple directories at the given paths and all their content. + """ + for path in paths: + remove_dir(path) + + +def remove_file(path: PathIn) -> bool: + """ + Remove a file at the given path. + If the file is removed with success returns True, otherwise False. + """ + path = _get_path(path) + if not exists(path): + return False + assert_file(path) + os.remove(path) + return not exists(path) + + +def remove_files(*paths: PathIn) -> None: + """ + Remove multiple files at the given paths. + """ + for path in paths: + remove_file(path) + + +def rename_dir(path: PathIn, name: str) -> None: + """ + Rename a directory with the given name. + If a directory or a file with the given name already exists, an OSError is raised. + """ + path = _get_path(path) + assert_dir(path) + comps = list(os.path.split(path)) + comps[-1] = name + dest = os.path.join(*comps) + assert_not_exists(dest) + os.rename(path, dest) + + +def rename_file(path: PathIn, name: str) -> None: + """ + Rename a file with the given name. + If a directory or a file with the given name already exists, an OSError is raised. + """ + path = _get_path(path) + assert_file(path) + dirpath, _ = split_filepath(path) + dest = join_filepath(dirpath, name) + assert_not_exists(dest) + os.rename(path, dest) + + +def rename_file_basename(path: PathIn, basename: str) -> None: + """ + Rename a file basename with the given basename. + """ + path = _get_path(path) + extension = get_file_extension(path) + filename = join_filename(basename, extension) + rename_file(path, filename) + + +def rename_file_extension(path: PathIn, extension: str) -> None: + """ + Rename a file extension with the given extension. + """ + path = _get_path(path) + basename = get_file_basename(path) + filename = join_filename(basename, extension) + rename_file(path, filename) + + +def replace_dir(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None: + """ + Replace directory at the specified path with the directory located at src. + If autodelete, the src directory will be removed at the end of the operation. + Optimized for large files. + """ + path = _get_path(path) + src = _get_path(src) + assert_not_file(path) + assert_dir(src) + + if path == src: + return + + make_dirs(path) + + dirpath, dirname = split_filepath(path) + # safe temporary name to avoid clashes with existing files/directories + temp_dirname = get_unique_name(dirpath) + temp_dest = join_path(dirpath, temp_dirname) + copy_dir_content(src, temp_dest) + + if exists(path): + temp_dirname = get_unique_name(dirpath) + temp_path = join_path(dirpath, temp_dirname) + rename_dir(path=path, name=temp_dirname) + rename_dir(path=temp_dest, name=dirname) + remove_dir(path=temp_path) + else: + rename_dir(path=temp_dest, name=dirname) + + if autodelete: + remove_dir(path=src) + + +def replace_file(path: PathIn, src: PathIn, *, autodelete: bool = False) -> None: + """ + Replace file at the specified path with the file located at src. + If autodelete, the src file will be removed at the end of the operation. + Optimized for large files. + """ + path = _get_path(path) + src = _get_path(src) + assert_not_dir(path) + assert_file(src) + if path == src: + return + + make_dirs_for_file(path) + + dirpath, filename = split_filepath(path) + _, extension = split_filename(filename) + # safe temporary name to avoid clashes with existing files/directories + temp_filename = get_unique_name(dirpath, extension=extension) + temp_dest = join_path(dirpath, temp_filename) + copy_file(path=src, dest=temp_dest, overwrite=False) + + if exists(path): + temp_filename = get_unique_name(dirpath, extension=extension) + temp_path = join_path(dirpath, temp_filename) + rename_file(path=path, name=temp_filename) + rename_file(path=temp_dest, name=filename) + remove_file(path=temp_path) + else: + rename_file(path=temp_dest, name=filename) + + if autodelete: + remove_file(path=src) + + +def _search_paths(path: PathIn, pattern: str) -> list[str]: + """ + Search all paths relative to path matching the given pattern. + """ + path = _get_path(path) + assert_dir(path) + pathname = os.path.join(path, pattern) + paths = glob.glob(pathname, recursive=True) + return paths + + +def search_dirs(path: PathIn, pattern: str = "**/*") -> list[str]: + """ + Search for directories at path matching the given pattern. + """ + path = _get_path(path) + return _filter_paths(path, _search_paths(path, pattern), predicate=is_dir) + + +def search_files(path: PathIn, pattern: str = "**/*.*") -> list[str]: + """ + Search for files at path matching the given pattern. + """ + path = _get_path(path) + return _filter_paths(path, _search_paths(path, pattern), predicate=is_file) diff --git a/fsutil/paths.py b/fsutil/paths.py new file mode 100644 index 0000000..e009edd --- /dev/null +++ b/fsutil/paths.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import os +import uuid +from collections.abc import Callable +from urllib.parse import urlsplit + +from fsutil.args import get_path as _get_path +from fsutil.checks import assert_dir, exists +from fsutil.types import PathIn + + +def get_filename(path: PathIn) -> str: + """ + Get the filename from the given path/url. + """ + path = _get_path(path) + filepath = urlsplit(path).path + filename = os.path.basename(filepath) + return filename + + +def get_file_basename(path: PathIn) -> str: + """ + Get the file basename from the given path/url. + """ + path = _get_path(path) + basename, _ = split_filename(path) + return basename + + +def get_file_extension(path: PathIn) -> str: + """ + Get the file extension from the given path/url. + """ + path = _get_path(path) + _, extension = split_filename(path) + return extension + + +def get_parent_dir(path: PathIn, *, levels: int = 1) -> str: + """ + Get the parent directory for the given path going up N levels. + """ + path = _get_path(path) + return join_path(path, *([os.pardir] * max(1, levels))) + + +def get_unique_name( + path: PathIn, + *, + prefix: str = "", + suffix: str = "", + extension: str = "", + separator: str = "-", +) -> str: + """ + Get a unique name for a directory/file at the given directory path. + """ + path = _get_path(path) + assert_dir(path) + name = "" + while True: + if prefix: + name += f"{prefix}{separator}" + uid = uuid.uuid4() + name += f"{uid}" + if suffix: + name += f"{separator}{suffix}" + if extension: + extension = extension.lstrip(".").lower() + name += f".{extension}" + if exists(join_path(path, name)): + continue + break + return name + + +def join_filename(basename: str, extension: str) -> str: + """ + Create a filename joining the file basename and the extension. + """ + basename = basename.rstrip(".").strip() + extension = extension.replace(".", "").strip() + if basename and extension: + filename = f"{basename}.{extension}" + return filename + return basename or extension + + +def join_filepath(dirpath: PathIn, filename: str) -> str: + """ + Create a filepath joining the directory path and the filename. + """ + dirpath = _get_path(dirpath) + return join_path(dirpath, filename) + + +def join_path(path: PathIn, *paths: PathIn) -> str: + """ + Create a path joining path and paths. + If path is __file__ (or a .py file), the resulting path will be relative + to the directory path of the module in which it's used. + """ + path = _get_path(path) + basepath = path + if get_file_extension(path) in ["py", "pyc", "pyo"]: + basepath = os.path.dirname(os.path.realpath(path)) + paths_str = [_get_path(path).lstrip("/\\") for path in paths] + return os.path.normpath(os.path.join(basepath, *paths_str)) + + +def split_filename(path: PathIn) -> tuple[str, str]: + """ + Split a filename and returns its basename and extension. + """ + path = _get_path(path) + filename = get_filename(path) + basename, extension = os.path.splitext(filename) + extension = extension.replace(".", "").strip() + return (basename, extension) + + +def split_filepath(path: PathIn) -> tuple[str, str]: + """ + Split a filepath and returns its directory-path and filename. + """ + path = _get_path(path) + dirpath = os.path.dirname(path) + filename = get_filename(path) + return (dirpath, filename) + + +def split_path(path: PathIn) -> list[str]: + """ + Split a path and returns its path-names. + """ + path = _get_path(path) + head, tail = os.path.split(path) + names = head.split(os.sep) + [tail] + names = list(filter(None, names)) + return names + + +def transform_filepath( + path: PathIn, + *, + dirpath: str | Callable[[str], str] | None = None, + basename: str | Callable[[str], str] | None = None, + extension: str | Callable[[str], str] | None = None, +) -> str: + """ + Trasform a filepath by applying the provided optional changes. + + :param path: The path. + :type path: PathIn + :param dirpath: The new dirpath or a callable. + :type dirpath: str | Callable[[str], str] | None + :param basename: The new basename or a callable. + :type basename: str | Callable[[str], str] | None + :param extension: The new extension or a callable. + :type extension: str | Callable[[str], str] | None + + :returns: The filepath with the applied changes. + :rtype: str + """ + + def _get_value( + new_value: str | Callable[[str], str] | None, + old_value: str, + ) -> str: + value = old_value + if new_value is not None: + if callable(new_value): + value = new_value(old_value) + elif isinstance(new_value, str): + value = new_value + else: + value = old_value + return value + + if all([dirpath is None, basename is None, extension is None]): + raise ValueError( + "Invalid arguments: at least one of " + "'dirpath', 'basename' or 'extension' is required." + ) + old_dirpath, old_filename = split_filepath(path) + old_basename, old_extension = split_filename(old_filename) + new_dirpath = _get_value(dirpath, old_dirpath) + new_basename = _get_value(basename, old_basename) + new_extension = _get_value(extension, old_extension) + if not any([new_dirpath, new_basename, new_extension]): + raise ValueError( + "Invalid arguments: at least one of " + "'dirpath', 'basename' or 'extension' is required." + ) + new_filename = join_filename(new_basename, new_extension) + new_filepath = join_filepath(new_dirpath, new_filename) + return new_filepath diff --git a/fsutil/perms.py b/fsutil/perms.py new file mode 100644 index 0000000..b433aa2 --- /dev/null +++ b/fsutil/perms.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import os + +from fsutil.args import get_path as _get_path +from fsutil.checks import assert_exists +from fsutil.types import PathIn + + +def get_permissions(path: PathIn) -> int: + """ + Get the file/directory permissions. + """ + path = _get_path(path) + assert_exists(path) + st_mode = os.stat(path).st_mode + permissions = int(str(oct(st_mode & 0o777))[2:]) + return permissions + + +def set_permissions(path: PathIn, value: int) -> None: + """ + Set the file/directory permissions. + """ + path = _get_path(path) + assert_exists(path) + permissions = int(str(value), 8) & 0o777 + os.chmod(path, permissions) diff --git a/fsutil/types.py b/fsutil/types.py new file mode 100644 index 0000000..5522a88 --- /dev/null +++ b/fsutil/types.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +import pathlib +from typing import Union + +PathIn = Union[str, pathlib.Path] diff --git a/pyproject.toml b/pyproject.toml index d5c99bc..e6747e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ line-length = 88 [tool.ruff.lint] ignore = [] -select = ["B", "B9", "C", "E", "F", "W"] +select = ["B", "B9", "C", "E", "F", "I", "W"] [tool.ruff.lint.mccabe] max-complexity = 10 diff --git a/requirements-test.txt b/requirements-test.txt index 48641d3..6797bd0 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,5 +1,7 @@ coverage == 7.6.* -mypy == 1.11.* -pre-commit == 4.0.* +mypy == 1.15.* +pre-commit == 4.1.* +pytest == 8.3.* +pytest-cov == 6.0.* requests == 2.32.* -tox == 4.22.* +tox == 4.24.* diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d0405dd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import os +import tempfile + +import pytest + + +@pytest.fixture +def temp_path(): + with tempfile.TemporaryDirectory() as temp_dir: + + def _temp_path(filepath=""): + return os.path.join(temp_dir, filepath) + + yield _temp_path diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index 66598ef..0000000 --- a/tests/test.py +++ /dev/null @@ -1,1449 +0,0 @@ -import os -import re -import sys -import threading -import time -import unittest -from datetime import datetime, timedelta -from decimal import Decimal -from unittest.mock import patch - -import fsutil - - -class fsutil_test_case(unittest.TestCase): - def setUp(self): - fsutil.remove_dir(self.temp_path()) - - def tearDown(self): - fsutil.remove_dir(self.temp_path()) - - @staticmethod - def norm_path(filepath): - return os.path.normpath(filepath) - - @staticmethod - def temp_path(filepath=""): - return fsutil.join_path(__file__, f"temp/{filepath}") - - @staticmethod - def temp_file_of_size(path, size): - fsutil.create_file(path) - size_bytes = fsutil.convert_size_string_to_bytes(size) - with open(path, "wb") as file: - file.seek(size_bytes - 1) - file.write(b"\0") - - def test_assert_dir(self): - path = self.temp_path("a/b/") - with self.assertRaises(OSError): - fsutil.assert_dir(path) - fsutil.create_dir(path) - fsutil.assert_dir(path) - - def test_assert_dir_with_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - with self.assertRaises(OSError): - fsutil.assert_dir(path) - - def test_assert_exists_with_directory(self): - path = self.temp_path("a/b/") - with self.assertRaises(OSError): - fsutil.assert_exists(path) - fsutil.create_dir(path) - fsutil.assert_exists(path) - - def test_assert_exists_with_file(self): - path = self.temp_path("a/b/c.txt") - with self.assertRaises(OSError): - fsutil.assert_exists(path) - fsutil.create_file(path) - fsutil.assert_exists(path) - - def test_assert_file(self): - path = self.temp_path("a/b/c.txt") - with self.assertRaises(OSError): - fsutil.assert_file(path) - fsutil.create_file(path) - fsutil.assert_file(path) - - def test_assert_file_with_directory(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_dir(path) - with self.assertRaises(OSError): - fsutil.assert_file(path) - - def test_clean_dir_only_dirs(self): - fsutil.create_dir(self.temp_path("x/y/z/a")) - fsutil.create_dir(self.temp_path("x/y/z/b")) - fsutil.create_dir(self.temp_path("x/y/z/c")) - fsutil.create_dir(self.temp_path("x/y/z/d")) - fsutil.create_dir(self.temp_path("x/y/z/e")) - fsutil.create_file(self.temp_path("x/y/z/b/f.txt"), content="hello world") - fsutil.create_file(self.temp_path("x/y/z/d/f.txt"), content="hello world") - fsutil.clean_dir(self.temp_path("x/y"), dirs=False, files=True) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/a"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/b"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/c"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/d"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/e"))) - fsutil.clean_dir(self.temp_path("x/y"), dirs=True, files=True) - self.assertFalse(fsutil.exists(self.temp_path("x/y/z/a"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/b"))) - self.assertFalse(fsutil.exists(self.temp_path("x/y/z/c"))) - self.assertTrue(fsutil.exists(self.temp_path("x/y/z/d"))) - self.assertFalse(fsutil.exists(self.temp_path("x/y/z/e"))) - - def test_clean_dir_only_files(self): - fsutil.create_file(self.temp_path("a/b/c/f1.txt"), content="hello world") - fsutil.create_file(self.temp_path("a/b/c/f2.txt")) - fsutil.create_file(self.temp_path("a/b/c/f3.txt"), content="hello world") - fsutil.create_file(self.temp_path("a/b/c/f4.txt")) - fsutil.create_file(self.temp_path("a/b/c/f5.txt"), content="hello world") - fsutil.clean_dir(self.temp_path("a"), dirs=False, files=False) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f1.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f2.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f3.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f4.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f5.txt"))) - fsutil.clean_dir(self.temp_path("a"), dirs=False, files=True) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f1.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/f2.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f3.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/f4.txt"))) - self.assertTrue(fsutil.exists(self.temp_path("a/b/c/f5.txt"))) - - def test_clean_dir_dirs_and_files(self): - fsutil.create_file(self.temp_path("a/b/c/f1.txt")) - fsutil.create_file(self.temp_path("a/b/c/f2.txt")) - fsutil.create_file(self.temp_path("a/b/c/f3.txt")) - fsutil.create_file(self.temp_path("a/b/c/d/f4.txt")) - fsutil.create_file(self.temp_path("a/b/c/d/f5.txt")) - fsutil.clean_dir(self.temp_path("a"), dirs=True, files=True) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/d/f5.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/d/f4.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/f3.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/f2.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c/f1.txt"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b/c"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b"))) - self.assertTrue(fsutil.exists(self.temp_path("a"))) - - def test_convert_size_bytes_to_string(self): - self.assertEqual(fsutil.convert_size_bytes_to_string(1023), "1023 bytes") - self.assertEqual(fsutil.convert_size_bytes_to_string(1024), "1 KB") - self.assertEqual(fsutil.convert_size_bytes_to_string(1048576), "1.00 MB") - self.assertEqual(fsutil.convert_size_bytes_to_string(1572864), "1.50 MB") - self.assertEqual(fsutil.convert_size_bytes_to_string(1073741824), "1.00 GB") - self.assertEqual(fsutil.convert_size_bytes_to_string(1879048192), "1.75 GB") - self.assertEqual(fsutil.convert_size_bytes_to_string(1099511627776), "1.00 TB") - - def test_convert_size_bytes_to_string_and_convert_size_string_to_bytes(self): - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1023 bytes") - ), - "1023 bytes", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1 KB") - ), - "1 KB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.00 MB") - ), - "1.00 MB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.25 MB") - ), - "1.25 MB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("2.50 MB") - ), - "2.50 MB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.00 GB") - ), - "1.00 GB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.09 GB") - ), - "1.09 GB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.99 GB") - ), - "1.99 GB", - ) - self.assertEqual( - fsutil.convert_size_bytes_to_string( - fsutil.convert_size_string_to_bytes("1.00 TB") - ), - "1.00 TB", - ) - - def test_convert_size_string_to_bytes(self): - self.assertEqual(fsutil.convert_size_string_to_bytes("1 KB"), 1024) - self.assertEqual(fsutil.convert_size_string_to_bytes("1.00 MB"), 1048576) - self.assertEqual(fsutil.convert_size_string_to_bytes("1.00 GB"), 1073741824) - self.assertEqual(fsutil.convert_size_string_to_bytes("1.00 TB"), 1099511627776) - - def test_convert_size_string_to_bytes_and_convert_size_bytes_to_string(self): - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1023) - ), - 1023, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1024) - ), - 1024, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1048576) - ), - 1048576, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1310720) - ), - 1310720, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(2621440) - ), - 2621440, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1073741824) - ), - 1073741824, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1170378588) - ), - 1170378588, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(2136746229) - ), - 2136746229, - ) - self.assertEqual( - fsutil.convert_size_string_to_bytes( - fsutil.convert_size_bytes_to_string(1099511627776) - ), - 1099511627776, - ) - - def test_copy_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="hello world") - dest = self.temp_path("x/y/z.txt") - fsutil.copy_file(path, dest) - self.assertTrue(fsutil.is_file(path)) - self.assertTrue(fsutil.is_file(dest)) - self.assertEqual(fsutil.get_file_hash(path), fsutil.get_file_hash(dest)) - - def test_copy_dir(self): - fsutil.create_file(self.temp_path("a/b/f-1.txt")) - fsutil.create_file(self.temp_path("a/b/f-2.txt")) - fsutil.create_file(self.temp_path("a/b/f-3.txt")) - fsutil.copy_dir(self.temp_path("a/b"), self.temp_path("x/y/z")) - filepaths = fsutil.list_files(self.temp_path("a/b")) - filenames = [fsutil.get_filename(filepath) for filepath in filepaths] - self.assertEqual(len(filepaths), 3) - self.assertEqual(filenames, ["f-1.txt", "f-2.txt", "f-3.txt"]) - filepaths = fsutil.list_files(self.temp_path("x/y/z/b/")) - filenames = [fsutil.get_filename(filepath) for filepath in filepaths] - self.assertEqual(len(filepaths), 3) - self.assertEqual(filenames, ["f-1.txt", "f-2.txt", "f-3.txt"]) - - def test_copy_dir_with_overwrite(self): - fsutil.create_file(self.temp_path("a/b/f-1.txt")) - fsutil.create_file(self.temp_path("a/b/f-2.txt")) - fsutil.create_file(self.temp_path("a/b/f-3.txt")) - fsutil.create_file(self.temp_path("x/y/z/f-0.txt")) - fsutil.copy_dir(self.temp_path("a/b"), self.temp_path("x/y/z"), overwrite=False) - with self.assertRaises(OSError): - fsutil.copy_dir( - self.temp_path("a/b"), self.temp_path("x/y/z"), overwrite=False - ) - fsutil.copy_dir(self.temp_path("a/b"), self.temp_path("x/y/z"), overwrite=True) - - def test_copy_dir_content(self): - fsutil.create_file(self.temp_path("a/b/f-1.txt")) - fsutil.create_file(self.temp_path("a/b/f-2.txt")) - fsutil.create_file(self.temp_path("a/b/f-3.txt")) - fsutil.copy_dir_content(self.temp_path("a/b"), self.temp_path("z")) - filepaths = fsutil.list_files(self.temp_path("z")) - filenames = [fsutil.get_filename(filepath) for filepath in filepaths] - self.assertEqual(len(filepaths), 3) - self.assertEqual(filenames, ["f-1.txt", "f-2.txt", "f-3.txt"]) - - def test_create_file(self): - path = self.temp_path("a/b/c.txt") - self.assertFalse(fsutil.exists(path)) - fsutil.create_file(path, content="hello world") - self.assertTrue(fsutil.exists(path)) - self.assertTrue(fsutil.is_file(path)) - self.assertEqual(fsutil.read_file(path), "hello world") - - def test_create_file_with_overwrite(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="hello world") - with self.assertRaises(OSError): - fsutil.create_file(path, content="hello world") - fsutil.create_file(path, content="hello moon", overwrite=True) - self.assertEqual(fsutil.read_file(path), "hello moon") - - def test_create_zip_file(self): - zip_path = self.temp_path("archive.zip") - f1_path = self.temp_path("a/b/f1.txt") - f2_path = self.temp_path("a/b/f2.txt") - f3_path = self.temp_path("x/y/f3.txt") - f4_path = self.temp_path("x/y/f4.txt") - fsutil.create_file(f1_path, content="hello world 1") - fsutil.create_file(f2_path, content="hello world 2") - fsutil.create_file(f3_path, content="hello world 3") - fsutil.create_file(f4_path, content="hello world 4") - fsutil.create_zip_file(zip_path, [f1_path, f2_path, f3_path, f4_path]) - with self.assertRaises(OSError): - fsutil.create_zip_file( - zip_path, [f1_path, f2_path, f3_path, f4_path], overwrite=False - ) - self.assertTrue(fsutil.is_file(f1_path)) - self.assertTrue(fsutil.is_file(f2_path)) - self.assertTrue(fsutil.is_file(f3_path)) - self.assertTrue(fsutil.is_file(f4_path)) - self.assertTrue(fsutil.is_file(zip_path)) - self.assertTrue(fsutil.get_file_size(zip_path) > 0) - - def test_create_tar_file(self): - tar_path = self.temp_path("archive.zip") - f1_path = self.temp_path("a/b/f1.txt") - f2_path = self.temp_path("a/b/f2.txt") - f3_path = self.temp_path("x/y/f3.txt") - f4_path = self.temp_path("x/y/f4.txt") - fsutil.create_file(f1_path, content="hello world 1") - fsutil.create_file(f2_path, content="hello world 2") - fsutil.create_file(f3_path, content="hello world 3") - fsutil.create_file(f4_path, content="hello world 4") - fsutil.create_tar_file(tar_path, [f1_path, f2_path, f3_path, f4_path]) - with self.assertRaises(OSError): - fsutil.create_tar_file( - tar_path, [f1_path, f2_path, f3_path, f4_path], overwrite=False - ) - self.assertTrue(fsutil.is_file(f1_path)) - self.assertTrue(fsutil.is_file(f2_path)) - self.assertTrue(fsutil.is_file(f3_path)) - self.assertTrue(fsutil.is_file(f4_path)) - self.assertTrue(fsutil.is_file(tar_path)) - self.assertTrue(fsutil.get_file_size(tar_path) > 0) - - def test_delete_dir(self): - fsutil.create_file(self.temp_path("a/b/c/d.txt")) - fsutil.create_file(self.temp_path("a/b/c/e.txt")) - fsutil.create_file(self.temp_path("a/b/c/f.txt")) - deleted = fsutil.delete_dir(self.temp_path("a/c/")) - self.assertFalse(deleted) - deleted = fsutil.delete_dir(self.temp_path("a/b/")) - self.assertTrue(deleted) - self.assertTrue(fsutil.exists(self.temp_path("a"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b"))) - - def test_delete_dir_content(self): - fsutil.create_file(self.temp_path("a/b/c/d.txt")) - fsutil.create_file(self.temp_path("a/b/e.txt")) - fsutil.create_file(self.temp_path("a/b/f.txt")) - path = self.temp_path("a/b/") - fsutil.delete_dir_content(path) - self.assertTrue(fsutil.is_empty_dir(path)) - - def test_delete_dirs(self): - fsutil.create_file(self.temp_path("a/b/c/document.txt")) - fsutil.create_file(self.temp_path("a/b/d/document.txt")) - fsutil.create_file(self.temp_path("a/b/e/document.txt")) - fsutil.create_file(self.temp_path("a/b/f/document.txt")) - path1 = self.temp_path("a/b/c/") - path2 = self.temp_path("a/b/d/") - path3 = self.temp_path("a/b/e/") - path4 = self.temp_path("a/b/f/") - self.assertTrue(fsutil.exists(path1)) - self.assertTrue(fsutil.exists(path2)) - self.assertTrue(fsutil.exists(path3)) - self.assertTrue(fsutil.exists(path4)) - fsutil.delete_dirs(path1, path2, path3, path4) - self.assertFalse(fsutil.exists(path1)) - self.assertFalse(fsutil.exists(path2)) - self.assertFalse(fsutil.exists(path3)) - self.assertFalse(fsutil.exists(path4)) - - def test_delete_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(self.temp_path("a/b/c.txt")) - self.assertTrue(fsutil.exists(path)) - deleted = fsutil.delete_file(self.temp_path("a/b/d.txt")) - self.assertFalse(deleted) - deleted = fsutil.delete_file(path) - self.assertTrue(deleted) - self.assertFalse(fsutil.exists(path)) - - def test_delete_files(self): - path1 = self.temp_path("a/b/c/document.txt") - path2 = self.temp_path("a/b/d/document.txt") - path3 = self.temp_path("a/b/e/document.txt") - path4 = self.temp_path("a/b/f/document.txt") - fsutil.create_file(path1) - fsutil.create_file(path2) - fsutil.create_file(path3) - fsutil.create_file(path4) - self.assertTrue(fsutil.exists(path1)) - self.assertTrue(fsutil.exists(path2)) - self.assertTrue(fsutil.exists(path3)) - self.assertTrue(fsutil.exists(path4)) - fsutil.delete_files(path1, path2, path3, path4) - self.assertFalse(fsutil.exists(path1)) - self.assertFalse(fsutil.exists(path2)) - self.assertFalse(fsutil.exists(path3)) - self.assertFalse(fsutil.exists(path4)) - - def test_download_file(self): - url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" - path = fsutil.download_file(url, dirpath=__file__) - self.assertTrue(fsutil.exists(path)) - lines = fsutil.read_file_lines(path, skip_empty=False) - lines_count = len(lines) - self.assertTrue(lines_count > 500 and lines_count < 1000) - fsutil.remove_file(path) - self.assertFalse(fsutil.exists(path)) - - def test_download_file_multiple_to_temp_dir(self): - for _ in range(3): - url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" - path = fsutil.download_file(url) - self.assertTrue(fsutil.exists(path)) - lines = fsutil.read_file_lines(path, skip_empty=False) - lines_count = len(lines) - self.assertTrue(lines_count > 500 and lines_count < 1000) - fsutil.remove_file(path) - self.assertFalse(fsutil.exists(path)) - - def test_download_file_without_requests_installed(self): - requests_installed = fsutil.requests_installed - fsutil.requests_installed = False - url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" - with self.assertRaises(ModuleNotFoundError): - fsutil.download_file(url, dirpath=__file__) - fsutil.requests_installed = requests_installed - - def test_exists(self): - path = self.temp_path("a/b/") - self.assertFalse(fsutil.exists(path)) - fsutil.create_dir(path) - self.assertTrue(fsutil.exists(path)) - path = self.temp_path("a/b/c.txt") - self.assertFalse(fsutil.exists(path)) - fsutil.create_file(path) - self.assertTrue(fsutil.exists(path)) - - def test_extract_zip_file(self): - zip_path = self.temp_path("archive.zip") - unzip_path = self.temp_path("unarchive/") - f1_path = self.temp_path("a/b/f1.txt") - f2_path = self.temp_path("a/b/f2.txt") - f3_path = self.temp_path("j/k/f3.txt") - f4_path = self.temp_path("j/k/f4.txt") - f5_path = self.temp_path("x/y/z/f5.txt") - f6_path = self.temp_path("x/y/z/f6.txt") - f5_f6_dir = self.temp_path("x") - fsutil.create_file(f1_path, content="hello world 1") - fsutil.create_file(f2_path, content="hello world 2") - fsutil.create_file(f3_path, content="hello world 3") - fsutil.create_file(f4_path, content="hello world 4") - fsutil.create_file(f5_path, content="hello world 5") - fsutil.create_file(f6_path, content="hello world 6") - fsutil.create_zip_file( - zip_path, [f1_path, f2_path, f3_path, f4_path, f5_f6_dir] - ) - fsutil.extract_zip_file(zip_path, unzip_path) - self.assertTrue(fsutil.is_dir(unzip_path)) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f1.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f2.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f3.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f4.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/y/z/f5.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/y/z/f6.txt"))) - self.assertTrue(fsutil.is_file(zip_path)) - - def test_extract_zip_file_with_autodelete(self): - zip_path = self.temp_path("archive.zip") - unzip_path = self.temp_path("unarchive/") - path = self.temp_path("f1.txt") - fsutil.create_file(path, content="hello world 1") - fsutil.create_zip_file(zip_path, [path]) - fsutil.extract_zip_file(zip_path, unzip_path, autodelete=True) - self.assertTrue(fsutil.is_dir(unzip_path)) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f1.txt"))) - self.assertFalse(fsutil.is_file(zip_path)) - - def test_extract_tar_file(self): - tar_path = self.temp_path("archive.tar") - untar_path = self.temp_path("unarchive/") - f1_path = self.temp_path("a/b/f1.txt") - f2_path = self.temp_path("a/b/f2.txt") - f3_path = self.temp_path("j/k/f3.txt") - f4_path = self.temp_path("j/k/f4.txt") - f5_path = self.temp_path("x/y/z/f5.txt") - f6_path = self.temp_path("x/y/z/f6.txt") - f5_f6_dir = self.temp_path("x") - fsutil.create_file(f1_path, content="hello world 1") - fsutil.create_file(f2_path, content="hello world 2") - fsutil.create_file(f3_path, content="hello world 3") - fsutil.create_file(f4_path, content="hello world 4") - fsutil.create_file(f5_path, content="hello world 5") - fsutil.create_file(f6_path, content="hello world 6") - fsutil.create_tar_file( - tar_path, [f1_path, f2_path, f3_path, f4_path, f5_f6_dir] - ) - fsutil.extract_tar_file(tar_path, untar_path) - self.assertTrue(fsutil.is_dir(untar_path)) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f1.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f2.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f3.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f4.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/y/z/f5.txt"))) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/y/z/f6.txt"))) - self.assertTrue(fsutil.is_file(tar_path)) - - def test_extract_tar_file_with_autodelete(self): - tar_path = self.temp_path("archive.tar") - untar_path = self.temp_path("unarchive/") - path = self.temp_path("f1.txt") - fsutil.create_file(path, content="hello world 1") - fsutil.create_tar_file(tar_path, [path]) - fsutil.extract_tar_file(tar_path, untar_path, autodelete=True) - self.assertTrue(fsutil.is_dir(untar_path)) - self.assertTrue(fsutil.is_file(self.temp_path("unarchive/f1.txt"))) - self.assertFalse(fsutil.is_file(tar_path)) - - def test_get_dir_creation_date(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - creation_date = fsutil.get_dir_creation_date(self.temp_path("a/b")) - now = datetime.now() - self.assertTrue((now - creation_date) < timedelta(seconds=0.1)) - time.sleep(0.2) - creation_date = fsutil.get_dir_creation_date(self.temp_path("a/b")) - now = datetime.now() - self.assertFalse((now - creation_date) < timedelta(seconds=0.1)) - - def test_get_dir_creation_date_formatted(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - creation_date_str = fsutil.get_dir_creation_date_formatted( - self.temp_path("a/b"), format="%Y/%m/%d" - ) - creation_date_re = re.compile(r"^[\d]{4}\/[\d]{2}\/[\d]{2}$") - self.assertTrue(creation_date_re.match(creation_date_str)) - - def test_get_dir_hash(self): - f1_path = self.temp_path("x/a/b/f1.txt") - f2_path = self.temp_path("x/a/b/f2.txt") - f3_path = self.temp_path("x/j/k/f3.txt") - f4_path = self.temp_path("x/j/k/f4.txt") - f5_path = self.temp_path("x/y/z/f5.txt") - f6_path = self.temp_path("x/y/z/f6.txt") - fsutil.create_file(f1_path, content="hello world 1") - fsutil.create_file(f2_path, content="hello world 2") - fsutil.create_file(f3_path, content="hello world 3") - fsutil.create_file(f4_path, content="hello world 4") - fsutil.create_file(f5_path, content="hello world 5") - fsutil.create_file(f6_path, content="hello world 6") - hash = fsutil.get_dir_hash(self.temp_path("x/")) - self.assertEqual(hash, "eabe619c41f0c4611b7b9746bededfcb") - - def test_get_dir_last_modified_date(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello") - creation_date = fsutil.get_dir_creation_date(self.temp_path("a")) - time.sleep(0.2) - fsutil.write_file(path, content="Goodbye", append=True) - now = datetime.now() - lastmod_date = fsutil.get_dir_last_modified_date(self.temp_path("a")) - self.assertTrue((now - lastmod_date) < timedelta(seconds=0.1)) - self.assertTrue((lastmod_date - creation_date) > timedelta(seconds=0.15)) - - def test_get_dir_last_modified_date_formatted(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - lastmod_date_str = fsutil.get_dir_last_modified_date_formatted( - self.temp_path("a") - ) - lastmod_date_re = re.compile( - r"^[\d]{4}\-[\d]{2}\-[\d]{2}[\s]{1}[\d]{2}\:[\d]{2}\:[\d]{2}$" - ) - self.assertTrue(lastmod_date_re.match(lastmod_date_str)) - - def test_get_dir_size(self): - self.temp_file_of_size(self.temp_path("a/a-1.txt"), "1.05 MB") # 1101004 - self.temp_file_of_size(self.temp_path("a/b/b-1.txt"), "2 MB") # 2097152 - self.temp_file_of_size(self.temp_path("a/b/b-2.txt"), "2.25 MB") # 2359296 - self.temp_file_of_size(self.temp_path("a/b/c/c-1.txt"), "3.75 MB") # 3932160 - self.temp_file_of_size(self.temp_path("a/b/c/c-2.txt"), "500 KB") # 512000 - self.temp_file_of_size(self.temp_path("a/b/c/c-3.txt"), "200 KB") # 204800 - self.assertEqual(fsutil.get_dir_size(self.temp_path("a")), 10206412) - self.assertEqual(fsutil.get_dir_size(self.temp_path("a/b")), 9105408) - self.assertEqual(fsutil.get_dir_size(self.temp_path("a/b/c")), 4648960) - - def test_get_dir_size_formatted(self): - self.temp_file_of_size(self.temp_path("a/a-1.txt"), "1.05 MB") # 1101004 - self.temp_file_of_size(self.temp_path("a/b/b-1.txt"), "2 MB") # 2097152 - self.temp_file_of_size(self.temp_path("a/b/b-2.txt"), "2.25 MB") # 2359296 - self.temp_file_of_size(self.temp_path("a/b/c/c-1.txt"), "3.75 MB") # 3932160 - self.temp_file_of_size(self.temp_path("a/b/c/c-2.txt"), "500 KB") # 512000 - self.temp_file_of_size(self.temp_path("a/b/c/c-3.txt"), "200 KB") # 204800 - self.assertEqual(fsutil.get_dir_size_formatted(self.temp_path("a")), "9.73 MB") - self.assertEqual( - fsutil.get_dir_size_formatted(self.temp_path("a/b")), "8.68 MB" - ) - self.assertEqual( - fsutil.get_dir_size_formatted(self.temp_path("a/b/c")), "4.43 MB" - ) - - def test_get_file_basename(self): - s = "Document" - self.assertEqual(fsutil.get_file_basename(s), "Document") - s = "Document.txt" - self.assertEqual(fsutil.get_file_basename(s), "Document") - s = ".Document.txt" - self.assertEqual(fsutil.get_file_basename(s), ".Document") - s = "/root/a/b/c/Document.txt" - self.assertEqual(fsutil.get_file_basename(s), "Document") - s = "https://domain-name.com/Document.txt?p=1" - self.assertEqual(fsutil.get_file_basename(s), "Document") - - def test_get_file_creation_date(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - creation_date = fsutil.get_file_creation_date(path) - now = datetime.now() - self.assertTrue((now - creation_date) < timedelta(seconds=0.1)) - time.sleep(0.2) - creation_date = fsutil.get_file_creation_date(path) - now = datetime.now() - self.assertFalse((now - creation_date) < timedelta(seconds=0.1)) - - def test_get_file_creation_date_formatted(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - creation_date_str = fsutil.get_file_creation_date_formatted( - path, format="%Y/%m/%d" - ) - creation_date_re = re.compile(r"^[\d]{4}\/[\d]{2}\/[\d]{2}$") - self.assertTrue(creation_date_re.match(creation_date_str)) - - def test_get_file_extension(self): - s = "Document" - self.assertEqual(fsutil.get_file_extension(s), "") - s = "Document.txt" - self.assertEqual(fsutil.get_file_extension(s), "txt") - s = ".Document.txt" - self.assertEqual(fsutil.get_file_extension(s), "txt") - s = "/root/a/b/c/Document.txt" - self.assertEqual(fsutil.get_file_extension(s), "txt") - s = "https://domain-name.com/Document.txt?p=1" - self.assertEqual(fsutil.get_file_extension(s), "txt") - - def test_get_file_hash(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - hash = fsutil.get_file_hash(path) - self.assertEqual(hash, "b10a8db164e0754105b7a99be72e3fe5") - - def test_get_file_last_modified_date(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello") - creation_date = fsutil.get_file_creation_date(path) - time.sleep(0.2) - fsutil.write_file(path, content="Goodbye", append=True) - now = datetime.now() - lastmod_date = fsutil.get_file_last_modified_date(path) - self.assertTrue((now - lastmod_date) < timedelta(seconds=0.1)) - self.assertTrue((lastmod_date - creation_date) > timedelta(seconds=0.15)) - - def test_get_file_last_modified_date_formatted(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - lastmod_date_str = fsutil.get_file_last_modified_date_formatted(path) - lastmod_date_re = re.compile( - r"^[\d]{4}\-[\d]{2}\-[\d]{2}[\s]{1}[\d]{2}\:[\d]{2}\:[\d]{2}$" - ) - self.assertTrue(lastmod_date_re.match(lastmod_date_str)) - - def test_get_file_size(self): - path = self.temp_path("a/b/c.txt") - self.temp_file_of_size(path, "1.75 MB") - size = fsutil.get_file_size(path) - self.assertEqual(size, fsutil.convert_size_string_to_bytes("1.75 MB")) - - def test_get_file_size_formatted(self): - path = self.temp_path("a/b/c.txt") - self.temp_file_of_size(path, "1.75 MB") - size = fsutil.get_file_size_formatted(path) - self.assertEqual(size, "1.75 MB") - - def test_get_filename(self): - s = "Document" - self.assertEqual(fsutil.get_filename(s), "Document") - s = "Document.txt" - self.assertEqual(fsutil.get_filename(s), "Document.txt") - s = ".Document.txt" - self.assertEqual(fsutil.get_filename(s), ".Document.txt") - s = "/root/a/b/c/Document.txt" - self.assertEqual(fsutil.get_filename(s), "Document.txt") - s = "https://domain-name.com/Document.txt?p=1" - self.assertEqual(fsutil.get_filename(s), "Document.txt") - - def test_get_parent_dir(self): - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s), - self.norm_path("/root/a/b/c"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=0), - self.norm_path("/root/a/b/c"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=1), - self.norm_path("/root/a/b/c"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=2), - self.norm_path("/root/a/b"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=3), - self.norm_path("/root/a"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=4), - self.norm_path("/root"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=5), - self.norm_path("/"), - ) - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.get_parent_dir(s, levels=6), - self.norm_path("/"), - ) - - @unittest.skipIf(sys.platform.startswith("win"), "Test skipped on Windows") - def test_get_permissions(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World") - permissions = fsutil.get_permissions(path) - self.assertEqual(permissions, 644) - - def test_get_unique_name(self): - path = self.temp_path("a/b/c") - fsutil.create_dir(path) - name = fsutil.get_unique_name( - path, - prefix="custom-prefix", - suffix="custom-suffix", - extension="txt", - separator="_", - ) - basename, extension = fsutil.split_filename(name) - self.assertTrue(basename.startswith("custom-prefix_")) - self.assertTrue(basename.endswith("_custom-suffix")) - self.assertEqual(extension, "txt") - - def test_is_dir(self): - path = self.temp_path("a/b/") - self.assertFalse(fsutil.is_dir(path)) - fsutil.create_dir(path) - self.assertTrue(fsutil.is_dir(path)) - path = self.temp_path("a/b/c.txt") - self.assertFalse(fsutil.is_dir(path)) - fsutil.create_file(path) - self.assertFalse(fsutil.is_dir(path)) - - def test_is_empty(self): - fsutil.create_file(self.temp_path("a/b/c.txt")) - fsutil.create_file(self.temp_path("a/b/d.txt"), content="1") - fsutil.create_dir(self.temp_path("a/b/e")) - self.assertTrue(fsutil.is_empty(self.temp_path("a/b/c.txt"))) - self.assertFalse(fsutil.is_empty(self.temp_path("a/b/d.txt"))) - self.assertTrue(fsutil.is_empty(self.temp_path("a/b/e"))) - self.assertFalse(fsutil.is_empty(self.temp_path("a/b"))) - - def test_is_empty_dir(self): - path = self.temp_path("a/b/") - fsutil.create_dir(path) - self.assertTrue(fsutil.is_empty_dir(path)) - filepath = self.temp_path("a/b/c.txt") - fsutil.create_file(filepath) - self.assertTrue(fsutil.is_file(filepath)) - self.assertFalse(fsutil.is_empty_dir(path)) - - def test_is_empty_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - self.assertTrue(fsutil.is_empty_file(path)) - path = self.temp_path("a/b/d.txt") - fsutil.create_file(path, content="hello world") - self.assertFalse(fsutil.is_empty_file(path)) - - def test_is_file(self): - path = self.temp_path("a/b/c.txt") - self.assertFalse(fsutil.is_file(path)) - fsutil.create_file(path) - self.assertTrue(fsutil.is_file(path)) - - def test_join_filename(self): - self.assertEqual(fsutil.join_filename("Document", "txt"), "Document.txt") - self.assertEqual(fsutil.join_filename("Document", ".txt"), "Document.txt") - self.assertEqual(fsutil.join_filename(" Document ", " txt "), "Document.txt") - self.assertEqual(fsutil.join_filename("Document", " .txt "), "Document.txt") - self.assertEqual(fsutil.join_filename("Document", ""), "Document") - self.assertEqual(fsutil.join_filename("", "txt"), "txt") - - def test_join_filepath(self): - self.assertEqual( - fsutil.join_filepath("a/b/c", "Document.txt"), - self.norm_path("a/b/c/Document.txt"), - ) - - def test_join_path_with_absolute_path(self): - self.assertEqual( - fsutil.join_path("/a/b/c/", "/document.txt"), - self.norm_path("/a/b/c/document.txt"), - ) - - @patch("os.sep", "\\") - def test_join_path_with_absolute_path_on_windows(self): - self.assertEqual( - fsutil.join_path("/a/b/c/", "/document.txt"), - self.norm_path("/a/b/c/document.txt"), - ) - - def test_join_path_with_parent_dirs(self): - self.assertEqual( - fsutil.join_path("/a/b/c/", "../../document.txt"), - self.norm_path("/a/document.txt"), - ) - - def test_list_dirs(self): - for i in range(0, 5): - fsutil.create_dir(self.temp_path(f"a/b/c/d-{i}")) - fsutil.create_file(self.temp_path(f"a/b/c/f-{i}"), content=f"{i}") - dirpaths = fsutil.list_dirs(self.temp_path("a/b/c")) - dirnames = [fsutil.split_path(dirpath)[-1] for dirpath in dirpaths] - self.assertEqual(len(dirpaths), 5) - self.assertEqual(dirnames, ["d-0", "d-1", "d-2", "d-3", "d-4"]) - - def test_list_files(self): - for i in range(0, 5): - fsutil.create_dir(self.temp_path(f"a/b/c/d-{i}")) - fsutil.create_file(self.temp_path(f"a/b/c/f-{i}.txt"), content=f"{i}") - filepaths = fsutil.list_files(self.temp_path("a/b/c")) - filenames = [fsutil.get_filename(filepath) for filepath in filepaths] - self.assertEqual(len(filepaths), 5) - self.assertEqual( - filenames, ["f-0.txt", "f-1.txt", "f-2.txt", "f-3.txt", "f-4.txt"] - ) - - def test_make_dirs(self): - path = self.temp_path("a/b/c/") - fsutil.make_dirs(path) - self.assertTrue(fsutil.is_dir(path)) - - def test_make_dirs_race_condition(self): - path = self.temp_path("a/b/c/") - for _ in range(0, 20): - t = threading.Thread(target=fsutil.make_dirs, args=[path], kwargs={}) - t.start() - t.join() - self.assertTrue(fsutil.is_dir(path)) - - def test_make_dirs_with_existing_dir(self): - path = self.temp_path("a/b/c/") - fsutil.create_dir(path) - fsutil.make_dirs(path) - self.assertTrue(fsutil.is_dir(path)) - - def test_make_dirs_with_existing_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - with self.assertRaises(OSError): - fsutil.make_dirs(path) - - def test_make_dirs_for_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.make_dirs_for_file(path) - self.assertTrue(fsutil.is_dir(self.temp_path("a/b/"))) - self.assertFalse(fsutil.is_dir(path)) - self.assertFalse(fsutil.is_file(path)) - - def test_make_dirs_for_file_with_existing_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - fsutil.make_dirs_for_file(path) - self.assertTrue(fsutil.is_dir(self.temp_path("a/b/"))) - self.assertFalse(fsutil.is_dir(path)) - self.assertTrue(fsutil.is_file(path)) - - def test_make_dirs_for_file_with_existing_dir(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_dir(path) - with self.assertRaises(OSError): - fsutil.make_dirs_for_file(path) - - def test_make_dirs_for_file_with_filename_only(self): - path = "document.txt" - fsutil.make_dirs_for_file(path) - self.assertFalse(fsutil.is_file(path)) - - def test_move_dir(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - fsutil.move_dir(self.temp_path("a/b"), self.temp_path("x/y")) - self.assertFalse(fsutil.exists(path)) - self.assertTrue(fsutil.is_file(self.temp_path("x/y/b/c.txt"))) - - def test_move_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path, content="Hello World") - dest = self.temp_path("a") - fsutil.move_file(path, dest) - self.assertFalse(fsutil.exists(path)) - self.assertTrue(fsutil.is_file(self.temp_path("a/c.txt"))) - - def test_read_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World") - self.assertEqual(fsutil.read_file(path), "Hello World") - - def test_read_file_from_url(self): - url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" - content = fsutil.read_file_from_url(url) - self.assertTrue("python-fsutil" in content) - - def test_read_file_json(self): - path = self.temp_path("a/b/c.json") - now = datetime.now() - data = { - "test": "Hello World", - "test_datetime": now, - "test_set": {1, 2, 3}, - } - fsutil.write_file_json(self.temp_path("a/b/c.json"), data=data) - expected_data = data.copy() - expected_data["test_datetime"] = now.isoformat() - expected_data["test_set"] = list(expected_data["test_set"]) - self.assertEqual(fsutil.read_file_json(path), expected_data) - - def test_read_file_lines(self): - path = self.temp_path("a/b/c.txt") - lines = ["", "1 ", " 2", "", "", " 3 ", " 4 ", "", "", "5"] - fsutil.write_file(path, content="\n".join(lines)) - - expected_lines = list(lines) - lines = fsutil.read_file_lines(path, strip_white=False, skip_empty=False) - self.assertEqual(lines, expected_lines) - - expected_lines = ["", "1", "2", "", "", "3", "4", "", "", "5"] - lines = fsutil.read_file_lines(path, strip_white=True, skip_empty=False) - self.assertEqual(lines, expected_lines) - - expected_lines = ["1 ", " 2", " 3 ", " 4 ", "5"] - lines = fsutil.read_file_lines(path, strip_white=False, skip_empty=True) - self.assertEqual(lines, expected_lines) - - expected_lines = ["1", "2", "3", "4", "5"] - lines = fsutil.read_file_lines(path, strip_white=True, skip_empty=True) - self.assertEqual(lines, expected_lines) - - def test_read_file_lines_with_lines_range(self): - path = self.temp_path("a/b/c.txt") - lines = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] - fsutil.write_file(path, content="\n".join(lines)) - - # single line - expected_lines = ["1"] - lines = fsutil.read_file_lines(path, line_start=1, line_end=1) - self.assertEqual(lines, expected_lines) - - # multiple lines - expected_lines = ["1", "2", "3"] - lines = fsutil.read_file_lines(path, line_start=1, line_end=3) - self.assertEqual(lines, expected_lines) - - # multiple lines not stripped - newline = "\r\n" if sys.platform == "win32" else "\n" - expected_lines = [f"1{newline}", f"2{newline}", f"3{newline}"] - lines = fsutil.read_file_lines( - path, line_start=1, line_end=3, strip_white=False, skip_empty=False - ) - self.assertEqual(lines, expected_lines) - - # last line - expected_lines = ["9"] - lines = fsutil.read_file_lines(path, line_start=-1) - self.assertEqual(lines, expected_lines) - - # last 3 lines - expected_lines = ["7", "8", "9"] - lines = fsutil.read_file_lines(path, line_start=-3) - self.assertEqual(lines, expected_lines) - - # empty file - fsutil.write_file(path, content="") - expected_lines = [] - lines = fsutil.read_file_lines(path, line_start=-2) - self.assertEqual(lines, expected_lines) - - def test_read_file_lines_count(self): - path = self.temp_path("a/b/c.txt") - lines = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] - fsutil.write_file(path, content="\n".join(lines)) - - lines_count = fsutil.read_file_lines_count(path) - self.assertEqual(lines_count, 10) - - def test_rename_dir(self): - path = self.temp_path("a/b/c") - fsutil.make_dirs(path) - fsutil.rename_dir(path, "d") - self.assertFalse(fsutil.exists(path)) - path = self.temp_path("a/b/d") - self.assertTrue(fsutil.exists(path)) - - def test_rename_dir_with_existing_name(self): - path = self.temp_path("a/b/c") - fsutil.make_dirs(path) - fsutil.make_dirs(self.temp_path("a/b/d")) - with self.assertRaises(OSError): - fsutil.rename_dir(path, "d") - - def test_rename_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - fsutil.rename_file(path, "d.txt.backup") - self.assertFalse(fsutil.exists(path)) - path = self.temp_path("a/b/d.txt.backup") - self.assertTrue(fsutil.exists(path)) - - def test_rename_file_with_existing_name(self): - path = self.temp_path("a/b/c") - fsutil.create_file(path) - path = self.temp_path("a/b/d") - fsutil.create_file(path) - with self.assertRaises(OSError): - fsutil.rename_file(path, "c") - - def test_rename_file_basename(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - fsutil.rename_file_basename(path, "d") - self.assertFalse(fsutil.exists(path)) - path = self.temp_path("a/b/d.txt") - self.assertTrue(fsutil.exists(path)) - - def test_rename_file_extension(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(path) - fsutil.rename_file_extension(path, "json") - self.assertFalse(fsutil.exists(path)) - path = self.temp_path("a/b/c.json") - self.assertTrue(fsutil.exists(path)) - - def test_remove_dir(self): - fsutil.create_file(self.temp_path("a/b/c/d.txt")) - fsutil.create_file(self.temp_path("a/b/c/e.txt")) - fsutil.create_file(self.temp_path("a/b/c/f.txt")) - removed = fsutil.remove_dir(self.temp_path("a/c/")) - self.assertFalse(removed) - removed = fsutil.remove_dir(self.temp_path("a/b/")) - self.assertTrue(removed) - self.assertTrue(fsutil.exists(self.temp_path("a"))) - self.assertFalse(fsutil.exists(self.temp_path("a/b"))) - - def test_remove_dir_content(self): - fsutil.create_file(self.temp_path("a/b/c/d.txt")) - fsutil.create_file(self.temp_path("a/b/e.txt")) - fsutil.create_file(self.temp_path("a/b/f.txt")) - path = self.temp_path("a/b/") - fsutil.remove_dir_content(path) - self.assertTrue(fsutil.is_empty_dir(path)) - - def test_remove_dirs(self): - fsutil.create_file(self.temp_path("a/b/c/document.txt")) - fsutil.create_file(self.temp_path("a/b/d/document.txt")) - fsutil.create_file(self.temp_path("a/b/e/document.txt")) - fsutil.create_file(self.temp_path("a/b/f/document.txt")) - path1 = self.temp_path("a/b/c/") - path2 = self.temp_path("a/b/d/") - path3 = self.temp_path("a/b/e/") - path4 = self.temp_path("a/b/f/") - self.assertTrue(fsutil.exists(path1)) - self.assertTrue(fsutil.exists(path2)) - self.assertTrue(fsutil.exists(path3)) - self.assertTrue(fsutil.exists(path4)) - fsutil.remove_dirs(path1, path2, path3, path4) - self.assertFalse(fsutil.exists(path1)) - self.assertFalse(fsutil.exists(path2)) - self.assertFalse(fsutil.exists(path3)) - self.assertFalse(fsutil.exists(path4)) - - def test_remove_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.create_file(self.temp_path("a/b/c.txt")) - self.assertTrue(fsutil.exists(path)) - removed = fsutil.remove_file(self.temp_path("a/b/d.txt")) - self.assertFalse(removed) - removed = fsutil.remove_file(path) - self.assertTrue(removed) - self.assertFalse(fsutil.exists(path)) - - def test_remove_files(self): - path1 = self.temp_path("a/b/c/document.txt") - path2 = self.temp_path("a/b/d/document.txt") - path3 = self.temp_path("a/b/e/document.txt") - path4 = self.temp_path("a/b/f/document.txt") - fsutil.create_file(path1) - fsutil.create_file(path2) - fsutil.create_file(path3) - fsutil.create_file(path4) - self.assertTrue(fsutil.exists(path1)) - self.assertTrue(fsutil.exists(path2)) - self.assertTrue(fsutil.exists(path3)) - self.assertTrue(fsutil.exists(path4)) - fsutil.remove_files(path1, path2, path3, path4) - self.assertFalse(fsutil.exists(path1)) - self.assertFalse(fsutil.exists(path2)) - self.assertFalse(fsutil.exists(path3)) - self.assertFalse(fsutil.exists(path4)) - - def test_replace_file(self): - dest = self.temp_path("a/b/c.txt") - src = self.temp_path("d/e/f.txt") - fsutil.create_file(dest, "old") - fsutil.create_file(src, "new") - fsutil.replace_file(dest, src) - content = fsutil.read_file(dest) - self.assertEqual(content, "new") - self.assertTrue(fsutil.exists(src)) - - def test_replace_file_with_autodelete(self): - dest_file = self.temp_path("a/b/c.txt") - src_file = self.temp_path("d/e/f.txt") - fsutil.create_file(dest_file, "old") - fsutil.create_file(src_file, "new") - fsutil.replace_file(dest_file, src_file, autodelete=True) - content = fsutil.read_file(dest_file) - self.assertEqual(content, "new") - self.assertFalse(fsutil.exists(src_file)) - - def test_replace_dir(self): - dest_dir = self.temp_path("a/b/") - dest_file = self.temp_path("a/b/c.txt") - src_dir = self.temp_path("d/e/") - src_file = self.temp_path("d/e/f.txt") - fsutil.create_file(dest_file, "old") - fsutil.create_file(src_file, "new") - fsutil.replace_dir(dest_dir, src_dir) - content = fsutil.read_file(self.temp_path("a/b/f.txt")) - self.assertEqual(content, "new") - self.assertTrue(fsutil.exists(src_dir)) - - def test_replace_dir_with_autodelete(self): - dest_dir = self.temp_path("a/b/") - dest_file = self.temp_path("a/b/c.txt") - src_dir = self.temp_path("d/e/") - src_file = self.temp_path("d/e/f.txt") - fsutil.create_file(dest_file, "old") - fsutil.create_file(src_file, "new") - fsutil.replace_dir(dest_dir, src_dir, autodelete=True) - content = fsutil.read_file(self.temp_path("a/b/f.txt")) - self.assertEqual(content, "new") - self.assertFalse(fsutil.exists(src_dir)) - - def test_search_files(self): - fsutil.create_file(self.temp_path("a/b/c/IMG_1000.jpg")) - fsutil.create_file(self.temp_path("a/b/c/IMG_1001.jpg")) - fsutil.create_file(self.temp_path("a/b/c/IMG_1002.png")) - fsutil.create_file(self.temp_path("a/b/c/IMG_1003.jpg")) - fsutil.create_file(self.temp_path("a/b/c/IMG_1004.jpg")) - fsutil.create_file(self.temp_path("a/x/c/IMG_1005.png")) - fsutil.create_file(self.temp_path("x/b/c/IMG_1006.png")) - fsutil.create_file(self.temp_path("a/b/c/DOC_1007.png")) - results = fsutil.search_files(self.temp_path("a/"), "**/c/IMG_*.png") - expected_results = [ - self.temp_path("a/b/c/IMG_1002.png"), - self.temp_path("a/x/c/IMG_1005.png"), - ] - self.assertEqual(results, expected_results) - - def test_search_dirs(self): - fsutil.create_file(self.temp_path("a/b/c/IMG_1000.jpg")) - fsutil.create_file(self.temp_path("x/y/z/c/IMG_1001.jpg")) - fsutil.create_file(self.temp_path("a/c/IMG_1002.png")) - fsutil.create_file(self.temp_path("c/b/c/IMG_1003.jpg")) - results = fsutil.search_dirs(self.temp_path(""), "**/c") - expected_results = [ - self.temp_path("a/b/c"), - self.temp_path("a/c"), - self.temp_path("c"), - self.temp_path("c/b/c"), - self.temp_path("x/y/z/c"), - ] - self.assertEqual(results, expected_results) - - @unittest.skipIf(sys.platform.startswith("win"), "Test skipped on Windows") - def test_set_permissions(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World") - fsutil.set_permissions(path, 777) - permissions = fsutil.get_permissions(path) - self.assertEqual(permissions, 777) - - def test_split_filename(self): - s = "Document" - self.assertEqual(fsutil.split_filename(s), ("Document", "")) - s = ".Document" - self.assertEqual(fsutil.split_filename(s), (".Document", "")) - s = "Document.txt" - self.assertEqual(fsutil.split_filename(s), ("Document", "txt")) - s = ".Document.txt" - self.assertEqual(fsutil.split_filename(s), (".Document", "txt")) - s = "/root/a/b/c/Document.txt" - self.assertEqual(fsutil.split_filename(s), ("Document", "txt")) - s = "https://domain-name.com/Document.txt?p=1" - self.assertEqual(fsutil.split_filename(s), ("Document", "txt")) - - def test_split_filepath(self): - s = self.norm_path("/root/a/b/c/Document.txt") - self.assertEqual( - fsutil.split_filepath(s), - (self.norm_path("/root/a/b/c"), "Document.txt"), - ) - - def test_split_filepath_with_filename_only(self): - s = self.norm_path("Document.txt") - self.assertEqual( - fsutil.split_filepath(s), - ("", "Document.txt"), - ) - - def test_split_path(self): - s = self.norm_path("/root/a/b/c/Document.txt") - self.assertEqual( - fsutil.split_path(s), - ["root", "a", "b", "c", "Document.txt"], - ) - - def test_transform_filepath_without_args(self): - s = "/root/a/b/c/Document.txt" - with self.assertRaises(ValueError): - (fsutil.transform_filepath(s),) - - def test_transform_filepath_with_empty_str_args(self): - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.transform_filepath(s, dirpath=""), - self.norm_path("Document.txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, basename=""), - self.norm_path("/root/a/b/c/txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, extension=""), - self.norm_path("/root/a/b/c/Document"), - ) - self.assertEqual( - fsutil.transform_filepath( - s, dirpath="/root/x/y/z/", basename="NewDocument", extension="xls" - ), - self.norm_path("/root/x/y/z/NewDocument.xls"), - ) - with self.assertRaises(ValueError): - (fsutil.transform_filepath(s, dirpath="", basename="", extension=""),) - - def test_transform_filepath_with_str_args(self): - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.transform_filepath(s, dirpath="/root/x/y/z/"), - self.norm_path("/root/x/y/z/Document.txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, basename="NewDocument"), - self.norm_path("/root/a/b/c/NewDocument.txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, extension="xls"), - self.norm_path("/root/a/b/c/Document.xls"), - ) - self.assertEqual( - fsutil.transform_filepath(s, extension=".xls"), - self.norm_path("/root/a/b/c/Document.xls"), - ) - self.assertEqual( - fsutil.transform_filepath( - s, dirpath="/root/x/y/z/", basename="NewDocument", extension="xls" - ), - self.norm_path("/root/x/y/z/NewDocument.xls"), - ) - - def test_transform_filepath_with_callable_args(self): - s = "/root/a/b/c/Document.txt" - self.assertEqual( - fsutil.transform_filepath(s, dirpath=lambda d: f"{d}/x/y/z/"), - self.norm_path("/root/a/b/c/x/y/z/Document.txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, basename=lambda b: b.lower()), - self.norm_path("/root/a/b/c/document.txt"), - ) - self.assertEqual( - fsutil.transform_filepath(s, extension=lambda e: "xls"), - self.norm_path("/root/a/b/c/Document.xls"), - ) - self.assertEqual( - fsutil.transform_filepath( - s, - dirpath=lambda d: f"{d}/x/y/z/", - basename=lambda b: b.lower(), - extension=lambda e: "xls", - ), - self.norm_path("/root/a/b/c/x/y/z/document.xls"), - ) - - def test_write_file(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World") - self.assertEqual(fsutil.read_file(path), "Hello World") - fsutil.write_file(path, content="Hello Jupiter") - self.assertEqual(fsutil.read_file(path), "Hello Jupiter") - - def test_write_file_atomic(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World", atomic=True) - self.assertEqual(fsutil.read_file(path), "Hello World") - fsutil.write_file(path, content="Hello Jupiter", atomic=True) - self.assertEqual(fsutil.read_file(path), "Hello Jupiter") - - def test_write_file_atomic_no_temp_files_left(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World", atomic=True) - fsutil.write_file(path, content="Hello Jupiter", atomic=True) - self.assertEqual(fsutil.list_files(self.temp_path("a/b/")), [path]) - - @unittest.skipIf(sys.platform.startswith("win"), "Test skipped on Windows") - def test_write_file_atomic_permissions_inheritance(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World", atomic=False) - self.assertEqual(fsutil.get_permissions(path), 644) - fsutil.set_permissions(path, 777) - fsutil.write_file(path, content="Hello Jupiter", atomic=True) - self.assertEqual(fsutil.get_permissions(path), 777) - - def test_write_file_with_filename_only(self): - path = "document.txt" - fsutil.write_file(path, content="Hello World") - self.assertTrue(fsutil.is_file(path)) - # cleanup - fsutil.remove_file(path) - - def test_write_file_json(self): - path = self.temp_path("a/b/c.json") - now = datetime.now() - dec = Decimal("3.33") - data = { - "test": "Hello World", - "test_datetime": now, - "test_decimal": dec, - } - fsutil.write_file_json(path, data=data) - self.assertEqual( - fsutil.read_file(path), - ( - "{" - f'"test": "Hello World", ' - f'"test_datetime": "{now.isoformat()}", ' - f'"test_decimal": "{dec}"' - "}" - ), - ) - - def test_write_file_json_atomic(self): - path = self.temp_path("a/b/c.json") - now = datetime.now() - dec = Decimal("3.33") - data = { - "test": "Hello World", - "test_datetime": now, - "test_decimal": dec, - } - fsutil.write_file_json(path, data=data, atomic=True) - self.assertEqual( - fsutil.read_file(path), - ( - "{" - f'"test": "Hello World", ' - f'"test_datetime": "{now.isoformat()}", ' - f'"test_decimal": "{dec}"' - "}" - ), - ) - - def test_write_file_with_append(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World") - self.assertEqual(fsutil.read_file(path), "Hello World") - fsutil.write_file(path, content=" - Hello Sun", append=True) - self.assertEqual(fsutil.read_file(path), "Hello World - Hello Sun") - - def test_write_file_with_append_atomic(self): - path = self.temp_path("a/b/c.txt") - fsutil.write_file(path, content="Hello World", atomic=True) - self.assertEqual(fsutil.read_file(path), "Hello World") - fsutil.write_file(path, content=" - Hello Sun", append=True, atomic=True) - self.assertEqual(fsutil.read_file(path), "Hello World - Hello Sun") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_archives.py b/tests/test_archives.py new file mode 100644 index 0000000..acd1abc --- /dev/null +++ b/tests/test_archives.py @@ -0,0 +1,133 @@ +import pytest + +import fsutil + + +def test_create_zip_file(temp_path): + zip_path = temp_path("archive.zip") + f1_path = temp_path("a/b/f1.txt") + f2_path = temp_path("a/b/f2.txt") + f3_path = temp_path("x/y/f3.txt") + f4_path = temp_path("x/y/f4.txt") + fsutil.create_file(f1_path, content="hello world 1") + fsutil.create_file(f2_path, content="hello world 2") + fsutil.create_file(f3_path, content="hello world 3") + fsutil.create_file(f4_path, content="hello world 4") + fsutil.create_zip_file(zip_path, [f1_path, f2_path, f3_path, f4_path]) + with pytest.raises(OSError): + fsutil.create_zip_file( + zip_path, [f1_path, f2_path, f3_path, f4_path], overwrite=False + ) + assert fsutil.is_file(f1_path) + assert fsutil.is_file(f2_path) + assert fsutil.is_file(f3_path) + assert fsutil.is_file(f4_path) + assert fsutil.is_file(zip_path) + assert fsutil.get_file_size(zip_path) > 0 + + +def test_create_tar_file(temp_path): + tar_path = temp_path("archive.tar") + f1_path = temp_path("a/b/f1.txt") + f2_path = temp_path("a/b/f2.txt") + f3_path = temp_path("x/y/f3.txt") + f4_path = temp_path("x/y/f4.txt") + fsutil.create_file(f1_path, content="hello world 1") + fsutil.create_file(f2_path, content="hello world 2") + fsutil.create_file(f3_path, content="hello world 3") + fsutil.create_file(f4_path, content="hello world 4") + fsutil.create_tar_file(tar_path, [f1_path, f2_path, f3_path, f4_path]) + with pytest.raises(OSError): + fsutil.create_tar_file( + tar_path, [f1_path, f2_path, f3_path, f4_path], overwrite=False + ) + assert fsutil.is_file(f1_path) + assert fsutil.is_file(f2_path) + assert fsutil.is_file(f3_path) + assert fsutil.is_file(f4_path) + assert fsutil.is_file(tar_path) + assert fsutil.get_file_size(tar_path) > 0 + + +def test_extract_zip_file(temp_path): + zip_path = temp_path("archive.zip") + unzip_path = temp_path("unarchive/") + f1_path = temp_path("a/b/f1.txt") + f2_path = temp_path("a/b/f2.txt") + f3_path = temp_path("j/k/f3.txt") + f4_path = temp_path("j/k/f4.txt") + f5_path = temp_path("x/y/z/f5.txt") + f6_path = temp_path("x/y/z/f6.txt") + f5_f6_dir = temp_path("x") + fsutil.create_file(f1_path, content="hello world 1") + fsutil.create_file(f2_path, content="hello world 2") + fsutil.create_file(f3_path, content="hello world 3") + fsutil.create_file(f4_path, content="hello world 4") + fsutil.create_file(f5_path, content="hello world 5") + fsutil.create_file(f6_path, content="hello world 6") + fsutil.create_zip_file(zip_path, [f1_path, f2_path, f3_path, f4_path, f5_f6_dir]) + fsutil.extract_zip_file(zip_path, unzip_path) + assert fsutil.is_dir(unzip_path) + assert fsutil.is_file(temp_path("unarchive/f1.txt")) + assert fsutil.is_file(temp_path("unarchive/f2.txt")) + assert fsutil.is_file(temp_path("unarchive/f3.txt")) + assert fsutil.is_file(temp_path("unarchive/f4.txt")) + assert fsutil.is_file(temp_path("unarchive/y/z/f5.txt")) + assert fsutil.is_file(temp_path("unarchive/y/z/f6.txt")) + assert fsutil.is_file(zip_path) + + +def test_extract_zip_file_with_autodelete(temp_path): + zip_path = temp_path("archive.zip") + unzip_path = temp_path("unarchive/") + path = temp_path("f1.txt") + fsutil.create_file(path, content="hello world 1") + fsutil.create_zip_file(zip_path, [path]) + fsutil.extract_zip_file(zip_path, unzip_path, autodelete=True) + assert fsutil.is_dir(unzip_path) + assert fsutil.is_file(temp_path("unarchive/f1.txt")) + assert not fsutil.is_file(zip_path) + + +def test_extract_tar_file(temp_path): + tar_path = temp_path("archive.tar") + untar_path = temp_path("unarchive/") + f1_path = temp_path("a/b/f1.txt") + f2_path = temp_path("a/b/f2.txt") + f3_path = temp_path("j/k/f3.txt") + f4_path = temp_path("j/k/f4.txt") + f5_path = temp_path("x/y/z/f5.txt") + f6_path = temp_path("x/y/z/f6.txt") + f5_f6_dir = temp_path("x") + fsutil.create_file(f1_path, content="hello world 1") + fsutil.create_file(f2_path, content="hello world 2") + fsutil.create_file(f3_path, content="hello world 3") + fsutil.create_file(f4_path, content="hello world 4") + fsutil.create_file(f5_path, content="hello world 5") + fsutil.create_file(f6_path, content="hello world 6") + fsutil.create_tar_file(tar_path, [f1_path, f2_path, f3_path, f4_path, f5_f6_dir]) + fsutil.extract_tar_file(tar_path, untar_path) + assert fsutil.is_dir(untar_path) + assert fsutil.is_file(temp_path("unarchive/f1.txt")) + assert fsutil.is_file(temp_path("unarchive/f2.txt")) + assert fsutil.is_file(temp_path("unarchive/f3.txt")) + assert fsutil.is_file(temp_path("unarchive/f4.txt")) + assert fsutil.is_file(temp_path("unarchive/y/z/f5.txt")) + assert fsutil.is_file(temp_path("unarchive/y/z/f6.txt")) + assert fsutil.is_file(tar_path) + + +def test_extract_tar_file_with_autodelete(temp_path): + tar_path = temp_path("archive.tar") + untar_path = temp_path("unarchive/") + path = temp_path("f1.txt") + fsutil.create_file(path, content="hello world 1") + fsutil.create_tar_file(tar_path, [path]) + fsutil.extract_tar_file(tar_path, untar_path, autodelete=True) + assert fsutil.is_dir(untar_path) + assert fsutil.is_file(temp_path("unarchive/f1.txt")) + assert not fsutil.is_file(tar_path) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_args.py b/tests/test_args.py new file mode 100644 index 0000000..b708c76 --- /dev/null +++ b/tests/test_args.py @@ -0,0 +1,29 @@ +import os +from pathlib import Path + +import pytest + +from fsutil.args import get_path + + +@pytest.mark.parametrize( + "input_path, expected", + [ + (None, None), + ("", "."), + ("/home/user/docs", os.path.normpath("/home/user/docs")), + ("C:\\Users\\test", os.path.normpath("C:\\Users\\test")), + ("./relative/path", os.path.normpath("./relative/path")), + ("..", os.path.normpath("..")), + (Path("/home/user/docs"), os.path.normpath("/home/user/docs")), + (Path("C:\\Users\\test"), os.path.normpath("C:\\Users\\test")), + (Path("./relative/path"), os.path.normpath("./relative/path")), + (Path(".."), os.path.normpath("..")), + ], +) +def test_get_path(input_path, expected): + assert get_path(input_path) == expected + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_checks.py b/tests/test_checks.py new file mode 100644 index 0000000..2998d36 --- /dev/null +++ b/tests/test_checks.py @@ -0,0 +1,111 @@ +import pytest + +import fsutil + + +def test_assert_dir(temp_path): + path = temp_path("a/b/") + with pytest.raises(OSError): + fsutil.assert_dir(path) + fsutil.create_dir(path) + fsutil.assert_dir(path) + + +def test_assert_dir_with_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + with pytest.raises(OSError): + fsutil.assert_dir(path) + + +def test_assert_exists_with_directory(temp_path): + path = temp_path("a/b/") + with pytest.raises(OSError): + fsutil.assert_exists(path) + fsutil.create_dir(path) + fsutil.assert_exists(path) + + +def test_assert_exists_with_file(temp_path): + path = temp_path("a/b/c.txt") + with pytest.raises(OSError): + fsutil.assert_exists(path) + fsutil.create_file(path) + fsutil.assert_exists(path) + + +def test_assert_file(temp_path): + path = temp_path("a/b/c.txt") + with pytest.raises(OSError): + fsutil.assert_file(path) + fsutil.create_file(path) + fsutil.assert_file(path) + + +def test_assert_file_with_directory(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_dir(path) + with pytest.raises(OSError): + fsutil.assert_file(path) + + +def test_exists(temp_path): + path = temp_path("a/b/") + assert not fsutil.exists(path) + fsutil.create_dir(path) + assert fsutil.exists(path) + path = temp_path("a/b/c.txt") + assert not fsutil.exists(path) + fsutil.create_file(path) + assert fsutil.exists(path) + + +def test_is_dir(temp_path): + path = temp_path("a/b/") + assert not fsutil.is_dir(path) + fsutil.create_dir(path) + assert fsutil.is_dir(path) + path = temp_path("a/b/c.txt") + assert not fsutil.is_dir(path) + fsutil.create_file(path) + assert not fsutil.is_dir(path) + + +def test_is_empty(temp_path): + fsutil.create_file(temp_path("a/b/c.txt")) + fsutil.create_file(temp_path("a/b/d.txt"), content="1") + fsutil.create_dir(temp_path("a/b/e")) + assert fsutil.is_empty(temp_path("a/b/c.txt")) + assert not fsutil.is_empty(temp_path("a/b/d.txt")) + assert fsutil.is_empty(temp_path("a/b/e")) + assert not fsutil.is_empty(temp_path("a/b")) + + +def test_is_empty_dir(temp_path): + path = temp_path("a/b/") + fsutil.create_dir(path) + assert fsutil.is_empty_dir(path) + filepath = temp_path("a/b/c.txt") + fsutil.create_file(filepath) + assert fsutil.is_file(filepath) + assert not fsutil.is_empty_dir(path) + + +def test_is_empty_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + assert fsutil.is_empty_file(path) + path = temp_path("a/b/d.txt") + fsutil.create_file(path, content="hello world") + assert not fsutil.is_empty_file(path) + + +def test_is_file(temp_path): + path = temp_path("a/b/c.txt") + assert not fsutil.is_file(path) + fsutil.create_file(path) + assert fsutil.is_file(path) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_converters.py b/tests/test_converters.py new file mode 100644 index 0000000..f7e95f1 --- /dev/null +++ b/tests/test_converters.py @@ -0,0 +1,86 @@ +import pytest + +import fsutil + + +@pytest.mark.parametrize( + "size_bytes, expected_output", + [ + (1023, "1023 bytes"), + (1024, "1 KB"), + (1048576, "1.00 MB"), + (1572864, "1.50 MB"), + (1073741824, "1.00 GB"), + (1879048192, "1.75 GB"), + (1099511627776, "1.00 TB"), + ], +) +def test_convert_size_bytes_to_string(size_bytes, expected_output): + assert fsutil.convert_size_bytes_to_string(size_bytes) == expected_output + + +@pytest.mark.parametrize( + "size_string, expected_output", + [ + ("1023 bytes", "1023 bytes"), + ("1 KB", "1 KB"), + ("1.00 MB", "1.00 MB"), + ("1.25 MB", "1.25 MB"), + ("2.50 MB", "2.50 MB"), + ("1.00 GB", "1.00 GB"), + ("1.09 GB", "1.09 GB"), + ("1.99 GB", "1.99 GB"), + ("1.00 TB", "1.00 TB"), + ], +) +def test_convert_size_bytes_to_string_and_convert_size_string_to_bytes( + size_string, expected_output +): + assert ( + fsutil.convert_size_bytes_to_string( + fsutil.convert_size_string_to_bytes(size_string) + ) + == expected_output + ) + + +@pytest.mark.parametrize( + "size_string, expected_output", + [ + ("1 KB", 1024), + ("1.00 MB", 1048576), + ("1.00 GB", 1073741824), + ("1.00 TB", 1099511627776), + ], +) +def test_convert_size_string_to_bytes(size_string, expected_output): + assert fsutil.convert_size_string_to_bytes(size_string) == expected_output + + +@pytest.mark.parametrize( + "size_bytes, expected_output", + [ + (1023, 1023), + (1024, 1024), + (1048576, 1048576), + (1310720, 1310720), + (2621440, 2621440), + (1073741824, 1073741824), + (1170378588, 1170378588), + (2136746229, 2136746229), + (1099511627776, 1099511627776), + ], +) +def test_convert_size_string_to_bytes_and_convert_size_bytes_to_string( + size_bytes, expected_output +): + assert ( + fsutil.convert_size_string_to_bytes( + fsutil.convert_size_bytes_to_string(size_bytes) + ) + == expected_output + ) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_deps.py b/tests/test_deps.py new file mode 100644 index 0000000..bb0a3e8 --- /dev/null +++ b/tests/test_deps.py @@ -0,0 +1,25 @@ +import sys +from types import ModuleType +from unittest import mock + +import pytest + +from fsutil.deps import require_requests + + +def test_require_requests_installed(): + with mock.patch.dict(sys.modules, {"requests": mock.Mock(spec=ModuleType)}): + requests_module = require_requests() + assert isinstance(requests_module, ModuleType) + + +def test_require_requests_not_installed(): + with mock.patch.dict(sys.modules, {"requests": None}): + with pytest.raises( + ModuleNotFoundError, match="'requests' module is not installed" + ): + require_requests() + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_info.py b/tests/test_info.py new file mode 100644 index 0000000..6ae0464 --- /dev/null +++ b/tests/test_info.py @@ -0,0 +1,167 @@ +import re +import time +from datetime import datetime, timedelta + +import pytest + +import fsutil + + +def create_file_of_size(path, size): + fsutil.create_file(path) + size_bytes = fsutil.convert_size_string_to_bytes(size) + with open(path, "wb") as file: + file.seek(size_bytes - 1) + file.write(b"\0") + + +def test_get_dir_creation_date(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + creation_date = fsutil.get_dir_creation_date(temp_path("a/b")) + now = datetime.now() + assert (now - creation_date) < timedelta(seconds=0.1) + time.sleep(0.2) + creation_date = fsutil.get_dir_creation_date(temp_path("a/b")) + now = datetime.now() + assert not (now - creation_date) < timedelta(seconds=0.1) + + +def test_get_dir_creation_date_formatted(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + creation_date_str = fsutil.get_dir_creation_date_formatted( + temp_path("a/b"), format="%Y/%m/%d" + ) + creation_date_re = re.compile(r"^[\d]{4}\/[\d]{2}\/[\d]{2}$") + assert creation_date_re.match(creation_date_str) is not None + + +def test_get_dir_hash(temp_path): + f1_path = temp_path("x/a/b/f1.txt") + f2_path = temp_path("x/a/b/f2.txt") + f3_path = temp_path("x/j/k/f3.txt") + f4_path = temp_path("x/j/k/f4.txt") + f5_path = temp_path("x/y/z/f5.txt") + f6_path = temp_path("x/y/z/f6.txt") + fsutil.create_file(f1_path, content="hello world 1") + fsutil.create_file(f2_path, content="hello world 2") + fsutil.create_file(f3_path, content="hello world 3") + fsutil.create_file(f4_path, content="hello world 4") + fsutil.create_file(f5_path, content="hello world 5") + fsutil.create_file(f6_path, content="hello world 6") + dir_hash = fsutil.get_dir_hash(temp_path("x/")) + assert dir_hash == "eabe619c41f0c4611b7b9746bededfcb" + + +def test_get_dir_last_modified_date(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello") + creation_date = fsutil.get_dir_creation_date(temp_path("a")) + time.sleep(0.2) + fsutil.write_file(path, content="Goodbye", append=True) + now = datetime.now() + lastmod_date = fsutil.get_dir_last_modified_date(temp_path("a")) + assert (now - lastmod_date) < timedelta(seconds=0.1) + assert (lastmod_date - creation_date) > timedelta(seconds=0.15) + + +def test_get_dir_last_modified_date_formatted(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + lastmod_date_str = fsutil.get_dir_last_modified_date_formatted(temp_path("a")) + lastmod_date_re = re.compile( + r"^[\d]{4}\-[\d]{2}\-[\d]{2}[\s]{1}[\d]{2}\:[\d]{2}\:[\d]{2}$" + ) + assert lastmod_date_re.match(lastmod_date_str) is not None + + +def test_get_dir_size(temp_path): + create_file_of_size(temp_path("a/a-1.txt"), "1.05 MB") # 1101004 + create_file_of_size(temp_path("a/b/b-1.txt"), "2 MB") # 2097152 + create_file_of_size(temp_path("a/b/b-2.txt"), "2.25 MB") # 2359296 + create_file_of_size(temp_path("a/b/c/c-1.txt"), "3.75 MB") # 3932160 + create_file_of_size(temp_path("a/b/c/c-2.txt"), "500 KB") # 512000 + create_file_of_size(temp_path("a/b/c/c-3.txt"), "200 KB") # 204800 + assert fsutil.get_dir_size(temp_path("a")) == 10206412 + assert fsutil.get_dir_size(temp_path("a/b")) == 9105408 + assert fsutil.get_dir_size(temp_path("a/b/c")) == 4648960 + + +def test_get_dir_size_formatted(temp_path): + create_file_of_size(temp_path("a/a-1.txt"), "1.05 MB") # 1101004 + create_file_of_size(temp_path("a/b/b-1.txt"), "2 MB") # 2097152 + create_file_of_size(temp_path("a/b/b-2.txt"), "2.25 MB") # 2359296 + create_file_of_size(temp_path("a/b/c/c-1.txt"), "3.75 MB") # 3932160 + create_file_of_size(temp_path("a/b/c/c-2.txt"), "500 KB") # 512000 + create_file_of_size(temp_path("a/b/c/c-3.txt"), "200 KB") # 204800 + assert fsutil.get_dir_size_formatted(temp_path("a")) == "9.73 MB" + assert fsutil.get_dir_size_formatted(temp_path("a/b")) == "8.68 MB" + assert fsutil.get_dir_size_formatted(temp_path("a/b/c")) == "4.43 MB" + + +def test_get_file_creation_date(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + creation_date = fsutil.get_file_creation_date(path) + now = datetime.now() + assert (now - creation_date) < timedelta(seconds=0.1) + time.sleep(0.2) + creation_date = fsutil.get_file_creation_date(path) + now = datetime.now() + assert not (now - creation_date) < timedelta(seconds=0.1) + + +def test_get_file_creation_date_formatted(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + creation_date_str = fsutil.get_file_creation_date_formatted(path, format="%Y/%m/%d") + creation_date_re = re.compile(r"^[\d]{4}\/[\d]{2}\/[\d]{2}$") + assert creation_date_re.match(creation_date_str) is not None + + +def test_get_file_hash(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + file_hash = fsutil.get_file_hash(path) + assert file_hash == "b10a8db164e0754105b7a99be72e3fe5" + + +def test_get_file_last_modified_date(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello") + creation_date = fsutil.get_file_creation_date(path) + time.sleep(0.2) + fsutil.write_file(path, content="Goodbye", append=True) + now = datetime.now() + lastmod_date = fsutil.get_file_last_modified_date(path) + assert (now - lastmod_date) < timedelta(seconds=0.1) + assert (lastmod_date - creation_date) > timedelta(seconds=0.15) + + +def test_get_file_last_modified_date_formatted(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + lastmod_date_str = fsutil.get_file_last_modified_date_formatted(path) + lastmod_date_re = re.compile( + r"^[\d]{4}\-[\d]{2}\-[\d]{2}[\s]{1}[\d]{2}\:[\d]{2}\:[\d]{2}$" + ) + assert lastmod_date_re.match(lastmod_date_str) is not None + + +def test_get_file_size(temp_path): + path = temp_path("a/b/c.txt") + create_file_of_size(path, "1.75 MB") + size = fsutil.get_file_size(path) + assert size == fsutil.convert_size_string_to_bytes("1.75 MB") + + +def test_get_file_size_formatted(temp_path): + path = temp_path("a/b/c.txt") + create_file_of_size(path, "1.75 MB") + size = fsutil.get_file_size_formatted(path) + assert size == "1.75 MB" + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_io.py b/tests/test_io.py new file mode 100644 index 0000000..8542a32 --- /dev/null +++ b/tests/test_io.py @@ -0,0 +1,204 @@ +import sys +from datetime import datetime +from decimal import Decimal + +import pytest + +import fsutil + + +def test_read_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World") + assert fsutil.read_file(path) == "Hello World" + + +def test_read_file_from_url(): + url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" + content = fsutil.read_file_from_url(url) + assert "python-fsutil" in content + + +def test_read_file_json(temp_path): + path = temp_path("a/b/c.json") + now = datetime.now() + data = { + "test": "Hello World", + "test_datetime": now, + "test_set": {1, 2, 3}, + } + fsutil.write_file_json(path, data=data) + expected_data = data.copy() + expected_data["test_datetime"] = now.isoformat() + expected_data["test_set"] = list(expected_data["test_set"]) + assert fsutil.read_file_json(path) == expected_data + + +def test_read_file_lines(temp_path): + path = temp_path("a/b/c.txt") + lines = ["", "1 ", " 2", "", "", " 3 ", " 4 ", "", "", "5"] + fsutil.write_file(path, content="\n".join(lines)) + + expected_lines = list(lines) + lines = fsutil.read_file_lines(path, strip_white=False, skip_empty=False) + assert lines == expected_lines + + expected_lines = ["", "1", "2", "", "", "3", "4", "", "", "5"] + lines = fsutil.read_file_lines(path, strip_white=True, skip_empty=False) + assert lines == expected_lines + + expected_lines = ["1 ", " 2", " 3 ", " 4 ", "5"] + lines = fsutil.read_file_lines(path, strip_white=False, skip_empty=True) + assert lines == expected_lines + + expected_lines = ["1", "2", "3", "4", "5"] + lines = fsutil.read_file_lines(path, strip_white=True, skip_empty=True) + assert lines == expected_lines + + +def test_read_file_lines_with_lines_range(temp_path): + path = temp_path("a/b/c.txt") + lines = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + fsutil.write_file(path, content="\n".join(lines)) + + # single line + expected_lines = ["1"] + lines = fsutil.read_file_lines(path, line_start=1, line_end=1) + assert lines == expected_lines + + # multiple lines + expected_lines = ["1", "2", "3"] + lines = fsutil.read_file_lines(path, line_start=1, line_end=3) + assert lines == expected_lines + + # multiple lines not stripped + newline = "\r\n" if sys.platform == "win32" else "\n" + expected_lines = [f"1{newline}", f"2{newline}", f"3{newline}"] + lines = fsutil.read_file_lines( + path, line_start=1, line_end=3, strip_white=False, skip_empty=False + ) + assert lines == expected_lines + + # last line + expected_lines = ["9"] + lines = fsutil.read_file_lines(path, line_start=-1) + assert lines == expected_lines + + # last 3 lines + expected_lines = ["7", "8", "9"] + lines = fsutil.read_file_lines(path, line_start=-3) + assert lines == expected_lines + + # empty file + fsutil.write_file(path, content="") + expected_lines = [] + lines = fsutil.read_file_lines(path, line_start=-2) + assert lines == expected_lines + + +def test_read_file_lines_count(temp_path): + path = temp_path("a/b/c.txt") + lines = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + fsutil.write_file(path, content="\n".join(lines)) + + lines_count = fsutil.read_file_lines_count(path) + assert lines_count == 10 + + +def test_write_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World") + assert fsutil.read_file(path) == "Hello World" + fsutil.write_file(path, content="Hello Jupiter") + assert fsutil.read_file(path) == "Hello Jupiter" + + +def test_write_file_atomic(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World", atomic=True) + assert fsutil.read_file(path) == "Hello World" + fsutil.write_file(path, content="Hello Jupiter", atomic=True) + assert fsutil.read_file(path) == "Hello Jupiter" + + +def test_write_file_atomic_no_temp_files_left(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World", atomic=True) + fsutil.write_file(path, content="Hello Jupiter", atomic=True) + assert fsutil.list_files(temp_path("a/b/")) == [path] + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Test skipped on Windows") +def test_write_file_atomic_permissions_inheritance(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World", atomic=False) + assert fsutil.get_permissions(path) == 644 + fsutil.set_permissions(path, 777) + fsutil.write_file(path, content="Hello Jupiter", atomic=True) + assert fsutil.get_permissions(path) == 777 + + +def test_write_file_with_filename_only(): + path = "document.txt" + fsutil.write_file(path, content="Hello World") + assert fsutil.is_file(path) + # cleanup + fsutil.remove_file(path) + + +def test_write_file_json(temp_path): + path = temp_path("a/b/c.json") + now = datetime.now() + dec = Decimal("3.33") + data = { + "test": "Hello World", + "test_datetime": now, + "test_decimal": dec, + } + fsutil.write_file_json(path, data=data) + assert fsutil.read_file(path) == ( + "{" + f'"test": "Hello World", ' + f'"test_datetime": "{now.isoformat()}", ' + f'"test_decimal": "{dec}"' + "}" + ) + + +def test_write_file_json_atomic(temp_path): + path = temp_path("a/b/c.json") + now = datetime.now() + dec = Decimal("3.33") + data = { + "test": "Hello World", + "test_datetime": now, + "test_decimal": dec, + } + fsutil.write_file_json(path, data=data, atomic=True) + assert fsutil.read_file(path) == ( + "{" + f'"test": "Hello World", ' + f'"test_datetime": "{now.isoformat()}", ' + f'"test_decimal": "{dec}"' + "}" + ) + + +def test_write_file_with_append(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World") + assert fsutil.read_file(path) == "Hello World" + fsutil.write_file(path, content=" - Hello Sun", append=True) + assert fsutil.read_file(path) == "Hello World - Hello Sun" + + +def test_write_file_with_append_atomic(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World", atomic=True) + assert fsutil.read_file(path) == "Hello World" + fsutil.write_file(path, content=" - Hello Sun", append=True, atomic=True) + assert fsutil.read_file(path) == "Hello World - Hello Sun" + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..0c31f99 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,25 @@ +import pytest + +from fsutil.metadata import ( + __author__, + __copyright__, + __description__, + __email__, + __license__, + __title__, + __version__, +) + + +def test_metadata_variables(): + assert bool(__author__) and isinstance(__author__, str) + assert bool(__copyright__) and isinstance(__copyright__, str) + assert bool(__description__) and isinstance(__description__, str) + assert bool(__email__) and isinstance(__email__, str) + assert bool(__license__) and isinstance(__license__, str) + assert bool(__title__) and isinstance(__title__, str) + assert bool(__version__) and isinstance(__version__, str) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_operations.py b/tests/test_operations.py new file mode 100644 index 0000000..e643443 --- /dev/null +++ b/tests/test_operations.py @@ -0,0 +1,538 @@ +import threading +from unittest.mock import patch + +import pytest + +import fsutil + + +def test_clean_dir_only_dirs(temp_path): + fsutil.create_dir(temp_path("x/y/z/a")) + fsutil.create_dir(temp_path("x/y/z/b")) + fsutil.create_dir(temp_path("x/y/z/c")) + fsutil.create_dir(temp_path("x/y/z/d")) + fsutil.create_dir(temp_path("x/y/z/e")) + fsutil.create_file(temp_path("x/y/z/b/f.txt"), content="hello world") + fsutil.create_file(temp_path("x/y/z/d/f.txt"), content="hello world") + fsutil.clean_dir(temp_path("x/y"), dirs=False, files=True) + assert fsutil.exists(temp_path("x/y/z/a")) + assert fsutil.exists(temp_path("x/y/z/b")) + assert fsutil.exists(temp_path("x/y/z/c")) + assert fsutil.exists(temp_path("x/y/z/d")) + assert fsutil.exists(temp_path("x/y/z/e")) + fsutil.clean_dir(temp_path("x/y"), dirs=True, files=True) + assert not fsutil.exists(temp_path("x/y/z/a")) + assert fsutil.exists(temp_path("x/y/z/b")) + assert not fsutil.exists(temp_path("x/y/z/c")) + assert fsutil.exists(temp_path("x/y/z/d")) + assert not fsutil.exists(temp_path("x/y/z/e")) + + +def test_clean_dir_only_files(temp_path): + fsutil.create_file(temp_path("a/b/c/f1.txt"), content="hello world") + fsutil.create_file(temp_path("a/b/c/f2.txt")) + fsutil.create_file(temp_path("a/b/c/f3.txt"), content="hello world") + fsutil.create_file(temp_path("a/b/c/f4.txt")) + fsutil.create_file(temp_path("a/b/c/f5.txt"), content="hello world") + fsutil.clean_dir(temp_path("a"), dirs=False, files=False) + assert fsutil.exists(temp_path("a/b/c/f1.txt")) + assert fsutil.exists(temp_path("a/b/c/f2.txt")) + assert fsutil.exists(temp_path("a/b/c/f3.txt")) + assert fsutil.exists(temp_path("a/b/c/f4.txt")) + assert fsutil.exists(temp_path("a/b/c/f5.txt")) + fsutil.clean_dir(temp_path("a"), dirs=False, files=True) + assert fsutil.exists(temp_path("a/b/c/f1.txt")) + assert not fsutil.exists(temp_path("a/b/c/f2.txt")) + assert fsutil.exists(temp_path("a/b/c/f3.txt")) + assert not fsutil.exists(temp_path("a/b/c/f4.txt")) + assert fsutil.exists(temp_path("a/b/c/f5.txt")) + + +def test_clean_dir_dirs_and_files(temp_path): + fsutil.create_file(temp_path("a/b/c/f1.txt")) + fsutil.create_file(temp_path("a/b/c/f2.txt")) + fsutil.create_file(temp_path("a/b/c/f3.txt")) + fsutil.create_file(temp_path("a/b/c/d/f4.txt")) + fsutil.create_file(temp_path("a/b/c/d/f5.txt")) + fsutil.clean_dir(temp_path("a"), dirs=True, files=True) + assert not fsutil.exists(temp_path("a/b/c/d/f5.txt")) + assert not fsutil.exists(temp_path("a/b/c/d/f4.txt")) + assert not fsutil.exists(temp_path("a/b/c/f3.txt")) + assert not fsutil.exists(temp_path("a/b/c/f2.txt")) + assert not fsutil.exists(temp_path("a/b/c/f1.txt")) + assert not fsutil.exists(temp_path("a/b/c")) + assert not fsutil.exists(temp_path("a/b")) + assert fsutil.exists(temp_path("a")) + + +def test_copy_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="hello world") + dest = temp_path("x/y/z.txt") + fsutil.copy_file(path, dest) + assert fsutil.is_file(path) + assert fsutil.is_file(dest) + assert fsutil.get_file_hash(path) == fsutil.get_file_hash(dest) + + +def test_copy_dir(temp_path): + fsutil.create_file(temp_path("a/b/f-1.txt")) + fsutil.create_file(temp_path("a/b/f-2.txt")) + fsutil.create_file(temp_path("a/b/f-3.txt")) + fsutil.copy_dir(temp_path("a/b"), temp_path("x/y/z")) + filepaths = fsutil.list_files(temp_path("a/b")) + filenames = [fsutil.get_filename(filepath) for filepath in filepaths] + assert len(filepaths) == 3 + assert filenames == ["f-1.txt", "f-2.txt", "f-3.txt"] + filepaths = fsutil.list_files(temp_path("x/y/z/b/")) + filenames = [fsutil.get_filename(filepath) for filepath in filepaths] + assert len(filepaths) == 3 + assert filenames == ["f-1.txt", "f-2.txt", "f-3.txt"] + + +def test_copy_dir_with_overwrite(temp_path): + fsutil.create_file(temp_path("a/b/f-1.txt")) + fsutil.create_file(temp_path("a/b/f-2.txt")) + fsutil.create_file(temp_path("a/b/f-3.txt")) + fsutil.create_file(temp_path("x/y/z/f-0.txt")) + fsutil.copy_dir(temp_path("a/b"), temp_path("x/y/z"), overwrite=False) + with pytest.raises(OSError): + fsutil.copy_dir(temp_path("a/b"), temp_path("x/y/z"), overwrite=False) + fsutil.copy_dir(temp_path("a/b"), temp_path("x/y/z"), overwrite=True) + + +def test_copy_dir_content(temp_path): + fsutil.create_file(temp_path("a/b/f-1.txt")) + fsutil.create_file(temp_path("a/b/f-2.txt")) + fsutil.create_file(temp_path("a/b/f-3.txt")) + fsutil.copy_dir_content(temp_path("a/b"), temp_path("z")) + filepaths = fsutil.list_files(temp_path("z")) + filenames = [fsutil.get_filename(filepath) for filepath in filepaths] + assert len(filepaths) == 3 + assert filenames == ["f-1.txt", "f-2.txt", "f-3.txt"] + + +def test_create_file(temp_path): + path = temp_path("a/b/c.txt") + assert not fsutil.exists(path) + fsutil.create_file(path, content="hello world") + assert fsutil.exists(path) + assert fsutil.is_file(path) + assert fsutil.read_file(path) == "hello world" + + +def test_create_file_with_overwrite(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="hello world") + with pytest.raises(OSError): + fsutil.create_file(path, content="hello world") + fsutil.create_file(path, content="hello moon", overwrite=True) + assert fsutil.read_file(path) == "hello moon" + + +def test_delete_dir(temp_path): + fsutil.create_file(temp_path("a/b/c/d.txt")) + fsutil.create_file(temp_path("a/b/c/e.txt")) + fsutil.create_file(temp_path("a/b/c/f.txt")) + deleted = fsutil.delete_dir(temp_path("a/c/")) + assert not deleted + deleted = fsutil.delete_dir(temp_path("a/b/")) + assert deleted + assert fsutil.exists(temp_path("a")) + assert not fsutil.exists(temp_path("a/b")) + + +def test_delete_dir_content(temp_path): + fsutil.create_file(temp_path("a/b/c/d.txt")) + fsutil.create_file(temp_path("a/b/e.txt")) + fsutil.create_file(temp_path("a/b/f.txt")) + path = temp_path("a/b/") + fsutil.delete_dir_content(path) + assert fsutil.is_empty_dir(path) + + +def test_delete_dirs(temp_path): + fsutil.create_file(temp_path("a/b/c/document.txt")) + fsutil.create_file(temp_path("a/b/d/document.txt")) + fsutil.create_file(temp_path("a/b/e/document.txt")) + fsutil.create_file(temp_path("a/b/f/document.txt")) + path1 = temp_path("a/b/c/") + path2 = temp_path("a/b/d/") + path3 = temp_path("a/b/e/") + path4 = temp_path("a/b/f/") + assert fsutil.exists(path1) + assert fsutil.exists(path2) + assert fsutil.exists(path3) + assert fsutil.exists(path4) + fsutil.delete_dirs(path1, path2, path3, path4) + assert not fsutil.exists(path1) + assert not fsutil.exists(path2) + assert not fsutil.exists(path3) + assert not fsutil.exists(path4) + + +def test_delete_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + assert fsutil.exists(path) + deleted = fsutil.delete_file(temp_path("a/b/d.txt")) + assert not deleted + deleted = fsutil.delete_file(path) + assert deleted + assert not fsutil.exists(path) + + +def test_delete_files(temp_path): + path1 = temp_path("a/b/c/document.txt") + path2 = temp_path("a/b/d/document.txt") + path3 = temp_path("a/b/e/document.txt") + path4 = temp_path("a/b/f/document.txt") + fsutil.create_file(path1) + fsutil.create_file(path2) + fsutil.create_file(path3) + fsutil.create_file(path4) + assert fsutil.exists(path1) + assert fsutil.exists(path2) + assert fsutil.exists(path3) + assert fsutil.exists(path4) + fsutil.delete_files(path1, path2, path3, path4) + assert not fsutil.exists(path1) + assert not fsutil.exists(path2) + assert not fsutil.exists(path3) + assert not fsutil.exists(path4) + + +def test_download_file(temp_path): + url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" + path = fsutil.download_file(url, dirpath=temp_path()) + assert fsutil.exists(path) + lines = fsutil.read_file_lines(path, skip_empty=False) + lines_count = len(lines) + assert 500 < lines_count < 1000 + fsutil.remove_file(path) + assert not fsutil.exists(path) + + +def test_download_file_multiple_to_temp_dir(temp_path): + for _ in range(3): + url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" + path = fsutil.download_file(url) + assert fsutil.exists(path) + lines = fsutil.read_file_lines(path, skip_empty=False) + lines_count = len(lines) + assert 500 < lines_count < 1000 + fsutil.remove_file(path) + assert not fsutil.exists(path) + + +def test_download_file_without_requests_installed(temp_path): + url = "https://raw.githubusercontent.com/fabiocaccamo/python-fsutil/main/README.md" + with patch("fsutil.operations.require_requests", side_effect=ModuleNotFoundError()): + with pytest.raises(ModuleNotFoundError): + fsutil.download_file(url, dirpath=temp_path()) + + +def test_list_dirs(temp_path): + for i in range(0, 5): + fsutil.create_dir(temp_path(f"a/b/c/d-{i}")) + fsutil.create_file(temp_path(f"a/b/c/f-{i}"), content=f"{i}") + dirpaths = fsutil.list_dirs(temp_path("a/b/c")) + dirnames = [fsutil.split_path(dirpath)[-1] for dirpath in dirpaths] + assert len(dirpaths) == 5 + assert dirnames == ["d-0", "d-1", "d-2", "d-3", "d-4"] + + +def test_list_files(temp_path): + for i in range(0, 5): + fsutil.create_dir(temp_path(f"a/b/c/d-{i}")) + fsutil.create_file(temp_path(f"a/b/c/f-{i}.txt"), content=f"{i}") + filepaths = fsutil.list_files(temp_path("a/b/c")) + filenames = [fsutil.get_filename(filepath) for filepath in filepaths] + assert len(filepaths) == 5 + assert filenames == ["f-0.txt", "f-1.txt", "f-2.txt", "f-3.txt", "f-4.txt"] + + +def test_make_dirs(temp_path): + path = temp_path("a/b/c/") + fsutil.make_dirs(path) + assert fsutil.is_dir(path) + + +def test_make_dirs_race_condition(temp_path): + path = temp_path("a/b/c/") + for _ in range(0, 20): + t = threading.Thread(target=fsutil.make_dirs, args=[path], kwargs={}) + t.start() + t.join() + assert fsutil.is_dir(path) + + +def test_make_dirs_with_existing_dir(temp_path): + path = temp_path("a/b/c/") + fsutil.create_dir(path) + fsutil.make_dirs(path) + assert fsutil.is_dir(path) + + +def test_make_dirs_with_existing_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + with pytest.raises(OSError): + fsutil.make_dirs(path) + + +def test_make_dirs_for_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.make_dirs_for_file(path) + assert fsutil.is_dir(temp_path("a/b/")) + assert not fsutil.is_dir(path) + assert not fsutil.is_file(path) + + +def test_make_dirs_for_file_with_existing_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + fsutil.make_dirs_for_file(path) + assert fsutil.is_dir(temp_path("a/b/")) + assert not fsutil.is_dir(path) + assert fsutil.is_file(path) + + +def test_make_dirs_for_file_with_existing_dir(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_dir(path) + with pytest.raises(OSError): + fsutil.make_dirs_for_file(path) + + +def test_make_dirs_for_file_with_filename_only(temp_path): + path = "document.txt" + fsutil.make_dirs_for_file(path) + assert not fsutil.is_file(path) + + +def test_move_dir(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + fsutil.move_dir(temp_path("a/b"), temp_path("x/y")) + assert not fsutil.exists(path) + assert fsutil.is_file(temp_path("x/y/b/c.txt")) + + +def test_move_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path, content="Hello World") + dest = temp_path("a") + fsutil.move_file(path, dest) + assert not fsutil.exists(path) + assert fsutil.is_file(temp_path("a/c.txt")) + + +def test_rename_dir(temp_path): + path = temp_path("a/b/c") + fsutil.make_dirs(path) + fsutil.rename_dir(path, "d") + assert not fsutil.exists(path) + path = temp_path("a/b/d") + assert fsutil.exists(path) + + +def test_rename_dir_with_existing_name(temp_path): + path = temp_path("a/b/c") + fsutil.make_dirs(path) + fsutil.make_dirs(temp_path("a/b/d")) + with pytest.raises(OSError): + fsutil.rename_dir(path, "d") + + +def test_rename_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + fsutil.rename_file(path, "d.txt.backup") + assert not fsutil.exists(path) + path = temp_path("a/b/d.txt.backup") + assert fsutil.exists(path) + + +def test_rename_file_with_existing_name(temp_path): + path = temp_path("a/b/c") + fsutil.create_file(path) + path = temp_path("a/b/d") + fsutil.create_file(path) + with pytest.raises(OSError): + fsutil.rename_file(path, "c") + + +def test_rename_file_basename(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + fsutil.rename_file_basename(path, "d") + assert not fsutil.exists(path) + path = temp_path("a/b/d.txt") + assert fsutil.exists(path) + + +def test_rename_file_extension(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + fsutil.rename_file_extension(path, "json") + assert not fsutil.exists(path) + path = temp_path("a/b/c.json") + assert fsutil.exists(path) + + +def test_remove_dir(temp_path): + fsutil.create_file(temp_path("a/b/c/d.txt")) + fsutil.create_file(temp_path("a/b/c/e.txt")) + fsutil.create_file(temp_path("a/b/c/f.txt")) + removed = fsutil.remove_dir(temp_path("a/c/")) + assert not removed + removed = fsutil.remove_dir(temp_path("a/b/")) + assert removed + assert fsutil.exists(temp_path("a")) + assert not fsutil.exists(temp_path("a/b")) + + +def test_remove_dir_content(temp_path): + fsutil.create_file(temp_path("a/b/c/d.txt")) + fsutil.create_file(temp_path("a/b/e.txt")) + fsutil.create_file(temp_path("a/b/f.txt")) + path = temp_path("a/b/") + fsutil.remove_dir_content(path) + assert fsutil.is_empty_dir(path) + + +def test_remove_dirs(temp_path): + fsutil.create_file(temp_path("a/b/c/document.txt")) + fsutil.create_file(temp_path("a/b/d/document.txt")) + fsutil.create_file(temp_path("a/b/e/document.txt")) + fsutil.create_file(temp_path("a/b/f/document.txt")) + path1 = temp_path("a/b/c/") + path2 = temp_path("a/b/d/") + path3 = temp_path("a/b/e/") + path4 = temp_path("a/b/f/") + assert fsutil.exists(path1) + assert fsutil.exists(path2) + assert fsutil.exists(path3) + assert fsutil.exists(path4) + fsutil.remove_dirs(path1, path2, path3, path4) + assert not fsutil.exists(path1) + assert not fsutil.exists(path2) + assert not fsutil.exists(path3) + assert not fsutil.exists(path4) + + +def test_remove_file(temp_path): + path = temp_path("a/b/c.txt") + fsutil.create_file(path) + assert fsutil.exists(path) + removed = fsutil.remove_file(temp_path("a/b/d.txt")) + assert not removed + removed = fsutil.remove_file(path) + assert removed + assert not fsutil.exists(path) + + +def test_remove_files(temp_path): + path1 = temp_path("a/b/c/document.txt") + path2 = temp_path("a/b/d/document.txt") + path3 = temp_path("a/b/e/document.txt") + path4 = temp_path("a/b/f/document.txt") + fsutil.create_file(path1) + fsutil.create_file(path2) + fsutil.create_file(path3) + fsutil.create_file(path4) + assert fsutil.exists(path1) + assert fsutil.exists(path2) + assert fsutil.exists(path3) + assert fsutil.exists(path4) + fsutil.remove_files(path1, path2, path3, path4) + assert not fsutil.exists(path1) + assert not fsutil.exists(path2) + assert not fsutil.exists(path3) + assert not fsutil.exists(path4) + + +def test_replace_file(temp_path): + dest = temp_path("a/b/c.txt") + src = temp_path("d/e/f.txt") + fsutil.create_file(dest, "old") + fsutil.create_file(src, "new") + fsutil.replace_file(dest, src) + content = fsutil.read_file(dest) + assert content == "new" + assert fsutil.exists(src) + + +def test_replace_file_with_autodelete(temp_path): + dest_file = temp_path("a/b/c.txt") + src_file = temp_path("d/e/f.txt") + fsutil.create_file(dest_file, "old") + fsutil.create_file(src_file, "new") + fsutil.replace_file(dest_file, src_file, autodelete=True) + content = fsutil.read_file(dest_file) + assert content == "new" + assert not fsutil.exists(src_file) + + +def test_replace_dir(temp_path): + dest_dir = temp_path("a/b/") + dest_file = temp_path("a/b/c.txt") + src_dir = temp_path("d/e/") + src_file = temp_path("d/e/f.txt") + fsutil.create_file(dest_file, "old") + fsutil.create_file(src_file, "new") + fsutil.replace_dir(dest_dir, src_dir) + content = fsutil.read_file(temp_path("a/b/f.txt")) + assert content == "new" + assert fsutil.exists(src_dir) + + +def test_replace_dir_with_autodelete(temp_path): + dest_dir = temp_path("a/b/") + dest_file = temp_path("a/b/c.txt") + src_dir = temp_path("d/e/") + src_file = temp_path("d/e/f.txt") + fsutil.create_file(dest_file, "old") + fsutil.create_file(src_file, "new") + fsutil.replace_dir(dest_dir, src_dir, autodelete=True) + content = fsutil.read_file(temp_path("a/b/f.txt")) + assert content == "new" + assert not fsutil.exists(src_dir) + + +def test_search_files(temp_path): + fsutil.create_file(temp_path("a/b/c/IMG_1000.jpg")) + fsutil.create_file(temp_path("a/b/c/IMG_1001.jpg")) + fsutil.create_file(temp_path("a/b/c/IMG_1002.png")) + fsutil.create_file(temp_path("a/b/c/IMG_1003.jpg")) + fsutil.create_file(temp_path("a/b/c/IMG_1004.jpg")) + fsutil.create_file(temp_path("a/x/c/IMG_1005.png")) + fsutil.create_file(temp_path("x/b/c/IMG_1006.png")) + fsutil.create_file(temp_path("a/b/c/DOC_1007.png")) + results = fsutil.search_files(temp_path("a/"), "**/c/IMG_*.png") + expected_results = [ + temp_path("a/b/c/IMG_1002.png"), + temp_path("a/x/c/IMG_1005.png"), + ] + assert results == expected_results + + +def test_search_dirs(temp_path): + fsutil.create_file(temp_path("a/b/c/IMG_1000.jpg")) + fsutil.create_file(temp_path("x/y/z/c/IMG_1001.jpg")) + fsutil.create_file(temp_path("a/c/IMG_1002.png")) + fsutil.create_file(temp_path("c/b/c/IMG_1003.jpg")) + results = fsutil.search_dirs(temp_path(""), "**/c") + expected_results = [ + temp_path("a/b/c"), + temp_path("a/c"), + temp_path("c"), + temp_path("c/b/c"), + temp_path("x/y/z/c"), + ] + assert results == expected_results + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_paths.py b/tests/test_paths.py new file mode 100644 index 0000000..a8bf57d --- /dev/null +++ b/tests/test_paths.py @@ -0,0 +1,191 @@ +import os +from unittest.mock import patch + +import pytest + +import fsutil + + +def test_get_file_basename(): + assert fsutil.get_file_basename("Document") == "Document" + assert fsutil.get_file_basename("Document.txt") == "Document" + assert fsutil.get_file_basename(".Document.txt") == ".Document" + assert fsutil.get_file_basename("/root/a/b/c/Document.txt") == "Document" + assert ( + fsutil.get_file_basename("https://domain-name.com/Document.txt?p=1") + == "Document" + ) + + +def test_get_file_extension(): + assert fsutil.get_file_extension("Document") == "" + assert fsutil.get_file_extension("Document.txt") == "txt" + assert fsutil.get_file_extension(".Document.txt") == "txt" + assert fsutil.get_file_extension("/root/a/b/c/Document.txt") == "txt" + assert ( + fsutil.get_file_extension("https://domain-name.com/Document.txt?p=1") == "txt" + ) + + +def test_get_filename(): + assert fsutil.get_filename("Document") == "Document" + assert fsutil.get_filename("Document.txt") == "Document.txt" + assert fsutil.get_filename(".Document.txt") == ".Document.txt" + assert fsutil.get_filename("/root/a/b/c/Document.txt") == "Document.txt" + assert ( + fsutil.get_filename("https://domain-name.com/Document.txt?p=1") + == "Document.txt" + ) + + +def test_get_parent_dir(): + s = "/root/a/b/c/Document.txt" + assert fsutil.get_parent_dir(s) == os.path.normpath("/root/a/b/c") + assert fsutil.get_parent_dir(s, levels=0) == os.path.normpath("/root/a/b/c") + assert fsutil.get_parent_dir(s, levels=1) == os.path.normpath("/root/a/b/c") + assert fsutil.get_parent_dir(s, levels=2) == os.path.normpath("/root/a/b") + assert fsutil.get_parent_dir(s, levels=3) == os.path.normpath("/root/a") + assert fsutil.get_parent_dir(s, levels=4) == os.path.normpath("/root") + assert fsutil.get_parent_dir(s, levels=5) == os.path.normpath("/") + assert fsutil.get_parent_dir(s, levels=6) == os.path.normpath("/") + + +def test_get_unique_name(temp_path): + path = temp_path("a/b/c") + fsutil.create_dir(path) + name = fsutil.get_unique_name( + path, + prefix="custom-prefix", + suffix="custom-suffix", + extension="txt", + separator="_", + ) + basename, extension = fsutil.split_filename(name) + assert basename.startswith("custom-prefix_") + assert basename.endswith("_custom-suffix") + assert extension == "txt" + + +def test_join_filename(): + assert fsutil.join_filename("Document", "txt") == "Document.txt" + assert fsutil.join_filename("Document", ".txt") == "Document.txt" + assert fsutil.join_filename(" Document ", " txt ") == "Document.txt" + assert fsutil.join_filename("Document", " .txt ") == "Document.txt" + assert fsutil.join_filename("Document", "") == "Document" + assert fsutil.join_filename("", "txt") == "txt" + + +def test_join_filepath(): + assert fsutil.join_filepath("a/b/c", "Document.txt") == os.path.normpath( + "a/b/c/Document.txt" + ) + + +def test_join_path_with_absolute_path(): + assert fsutil.join_path("/a/b/c/", "/document.txt") == os.path.normpath( + "/a/b/c/document.txt" + ) + + +@patch("os.sep", "\\") +def test_join_path_with_absolute_path_on_windows(): + assert fsutil.join_path("/a/b/c/", "/document.txt") == os.path.normpath( + "/a/b/c/document.txt" + ) + + +def test_join_path_with_parent_dirs(): + assert fsutil.join_path("/a/b/c/", "../../document.txt") == os.path.normpath( + "/a/document.txt" + ) + + +def test_split_filename(): + assert fsutil.split_filename("Document") == ("Document", "") + assert fsutil.split_filename(".Document") == (".Document", "") + assert fsutil.split_filename("Document.txt") == ("Document", "txt") + assert fsutil.split_filename(".Document.txt") == (".Document", "txt") + assert fsutil.split_filename("/root/a/b/c/Document.txt") == ("Document", "txt") + assert fsutil.split_filename("https://domain-name.com/Document.txt?p=1") == ( + "Document", + "txt", + ) + + +def test_split_filepath(): + s = os.path.normpath("/root/a/b/c/Document.txt") + assert fsutil.split_filepath(s) == (os.path.normpath("/root/a/b/c"), "Document.txt") + + +def test_split_filepath_with_filename_only(): + s = os.path.normpath("Document.txt") + assert fsutil.split_filepath(s) == ("", "Document.txt") + + +def test_split_path(): + s = os.path.normpath("/root/a/b/c/Document.txt") + assert fsutil.split_path(s) == ["root", "a", "b", "c", "Document.txt"] + + +def test_transform_filepath_without_args(): + s = "/root/a/b/c/Document.txt" + with pytest.raises(ValueError): + fsutil.transform_filepath(s) + + +def test_transform_filepath_with_empty_str_args(): + s = "/root/a/b/c/Document.txt" + assert fsutil.transform_filepath(s, dirpath="") == os.path.normpath("Document.txt") + assert fsutil.transform_filepath(s, basename="") == os.path.normpath( + "/root/a/b/c/txt" + ) + assert fsutil.transform_filepath(s, extension="") == os.path.normpath( + "/root/a/b/c/Document" + ) + assert fsutil.transform_filepath( + s, dirpath="/root/x/y/z/", basename="NewDocument", extension="xls" + ) == os.path.normpath("/root/x/y/z/NewDocument.xls") + with pytest.raises(ValueError): + fsutil.transform_filepath(s, dirpath="", basename="", extension="") + + +def test_transform_filepath_with_str_args(): + s = "/root/a/b/c/Document.txt" + assert fsutil.transform_filepath(s, dirpath="/root/x/y/z/") == os.path.normpath( + "/root/x/y/z/Document.txt" + ) + assert fsutil.transform_filepath(s, basename="NewDocument") == os.path.normpath( + "/root/a/b/c/NewDocument.txt" + ) + assert fsutil.transform_filepath(s, extension="xls") == os.path.normpath( + "/root/a/b/c/Document.xls" + ) + assert fsutil.transform_filepath(s, extension=".xls") == os.path.normpath( + "/root/a/b/c/Document.xls" + ) + assert fsutil.transform_filepath( + s, dirpath="/root/x/y/z/", basename="NewDocument", extension="xls" + ) == os.path.normpath("/root/x/y/z/NewDocument.xls") + + +def test_transform_filepath_with_callable_args(): + s = "/root/a/b/c/Document.txt" + assert fsutil.transform_filepath( + s, dirpath=lambda d: f"{d}/x/y/z/" + ) == os.path.normpath("/root/a/b/c/x/y/z/Document.txt") + assert fsutil.transform_filepath( + s, basename=lambda b: b.lower() + ) == os.path.normpath("/root/a/b/c/document.txt") + assert fsutil.transform_filepath(s, extension=lambda e: "xls") == os.path.normpath( + "/root/a/b/c/Document.xls" + ) + assert fsutil.transform_filepath( + s, + dirpath=lambda d: f"{d}/x/y/z/", + basename=lambda b: b.lower(), + extension=lambda e: "xls", + ) == os.path.normpath("/root/a/b/c/x/y/z/document.xls") + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/test_perms.py b/tests/test_perms.py new file mode 100644 index 0000000..1f77d5a --- /dev/null +++ b/tests/test_perms.py @@ -0,0 +1,26 @@ +import sys + +import pytest + +import fsutil + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Test skipped on Windows") +def test_get_permissions(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World") + permissions = fsutil.get_permissions(path) + assert permissions == 644 + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Test skipped on Windows") +def test_set_permissions(temp_path): + path = temp_path("a/b/c.txt") + fsutil.write_file(path, content="Hello World") + fsutil.set_permissions(path, 777) + permissions = fsutil.get_permissions(path) + assert permissions == 777 + + +if __name__ == "__main__": + pytest.main() diff --git a/tox.ini b/tox.ini index b381536..fd16490 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,6 @@ deps = -r requirements-test.txt commands = - pre-commit run -a - mypy --install-types --non-interactive --strict - coverage run --append --source=fsutil -m unittest - coverage report --show-missing --ignore-errors + pre-commit run --all-files + mypy --install-types --non-interactive + pytest tests --cov=fsutil --cov-report=term-missing --cov-fail-under=90