## hatch_ci.fileos

src/hatch_ci/fileos.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    """various file/dir related functions"""

    from __future__ import annotations

    import os
    import types
    from pathlib import Path
    from typing import Any, overload

    class FileOSError(Exception):
        pass

    class FileOSModuleNotFoundError(FileOSError):
        pass

    class FileOSMInvalidModuleError(FileOSError):
        pass

    def rmtree(path: Path):
        """universal (win|*nix) rmtree"""

        from os import name
        from shutil import rmtree
        from stat import S_IWUSR

        if name == "nt":
            for p in path.rglob("*"):
                p.chmod(S_IWUSR)
        rmtree(path, ignore_errors=True)
        if path.exists():
            raise RuntimeError(f"cannot remove {path=}")

    def mkdir(path: Path) -> Path:
        """make a path directory and returns if it has been created"""
        path.mkdir(exist_ok=True, parents=True)
        return path

    def touch(path: Path) -> Path:
        """touch a new empty file"""
        mkdir(path.parent)
        path.write_text("")
        return path

    @overload
    def which(exe: Path | str, kind: type[list], abort: bool = True) -> list[Path]: ...

    @overload
    def which(exe: Path | str, kind: None, abort: bool = True) -> Path | None: ...

    def which(
        exe: Path | str, kind: type[list] | None = None, abort: bool = True
    ) -> list[Path] | Path | None:
        candidates: list[Path] = []
        for srcdir in os.environ.get("PATH", "").split(os.pathsep):
            for ext in os.environ.get("PATHEXT", "").split(os.pathsep):
                path = srcdir / Path(exe).with_suffix(ext)
                if not path.exists():
                    continue
                if kind is None:
                    return path
                candidates.append(path)
        return candidates

    def loadmod(path: Path | str, suffix: str | None = "") -> types.ModuleType:
        import inspect
        from importlib import machinery, util

        if isinstance(path, str):
            if not Path(path).is_absolute():
                path = Path(inspect.stack()[1].filename).parent / path
            path = Path(path)

        if suffix is not None:
            machinery.SOURCE_SUFFIXES.append(suffix)
        try:
            spec = util.spec_from_file_location(Path(path).name, Path(path))
            if not spec:
                raise FileOSModuleNotFoundError(f"cannot find module for {path=}")
            module = util.module_from_spec(spec)
            if not spec.loader:
                raise FileOSMInvalidModuleError(f"invalid module in {path=}")
            spec.loader.exec_module(module)
        finally:
            if suffix is not None:
                machinery.SOURCE_SUFFIXES.pop()
        return module

    ### FILE UTILITIES

    def zextract(path: Path | str, items: list[str] | None = None) -> dict[str, Any]:
        """extracts from path (a zipfile/tarball) all data in a dictionary"""
        from tarfile import TarFile, is_tarfile
        from zipfile import ZipFile, is_zipfile

        path = Path(path)
        result = {}
        if is_tarfile(path):
            with TarFile.open(path) as tfp:
                for member in tfp.getmembers():
                    fp = tfp.extractfile(member)
                    if not fp:
                        continue
                    result[member.name] = str(fp.read(), encoding="utf-8")
        elif is_zipfile(path):
            with ZipFile(path) as tfp:
                for zinfo in tfp.infolist():
                    if items and zinfo.filename not in items:
                        continue
                    with tfp.open(zinfo.filename) as fp:
                        result[zinfo.filename] = str(fp.read(), encoding="utf-8").replace(
                            "\r", ""
                        )

        return result

    def backup(path: Path, ext: str, overwrite: bool = False, abort: bool = True) -> Path:
        """creates a backup of path"""
        from shutil import copyfile, copymode

        path2 = path.parent / f"{path.name}{ext}"
        if path2.exists() and not overwrite:
            if abort:
                raise FileOSError(f"backup file present {path2}")
            return path2

        copyfile(path, path2)
        copymode(path, path2)
        return path2

    def unbackup(path: Path, ext: str, abort: bool = True) -> Path:
        """restores from a backup of path"""
        from shutil import move

        path2 = path.parent / f"{path.name}{ext}"
        if abort and not path2.exists():
            raise FileOSError(f"cannot find backup file {path2} for {path=}")
        if path2.exists():
            move(str(path2), str(path))
        return path2