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
|
import pathlib
from typing import Any, Iterable, Sequence, Union
from attrs import field
from cyclopts.utils import frozen, to_tuple_converter
def ext_converter(value: Union[None, Any, Iterable[Any]]) -> tuple[str, ...]:
return tuple(e.lower().lstrip(".") for e in to_tuple_converter(value))
@frozen(kw_only=True)
class Path:
"""Assertions on properties of :class:`pathlib.Path`.
Example Usage:
.. code-block:: python
from cyclopts import App, Parameter, validators
from pathlib import Path
from typing import Annotated
app = App()
@app.default
def main(
# ``src`` must be a file that exists.
src: Annotated[Path, Parameter(validator=validators.Path(exists=True, dir_okay=False))],
# ``dst`` must be a path that does **not** exist.
dst: Annotated[Path, Parameter(validator=validators.Path(dir_okay=False, file_okay=False))],
):
"Copies src->dst."
dst.write_bytes(src.read_bytes())
app()
.. code-block:: console
$ my-script foo.bin bar.bin # if foo.bin does not exist
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "foo.bin" for "SRC". "foo.bin" does not exist. │
╰───────────────────────────────────────────────────────────────╯
$ my-script foo.bin bar.bin # if bar.bin exists
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value "bar.bin" for "DST". "bar.bin" already exists. │
╰───────────────────────────────────────────────────────────────╯
"""
exists: bool = False
"""If :obj:`True`, specified path **must** exist. Defaults to :obj:`False`."""
file_okay: bool = True
"""
If path exists, check it's type:
* If :obj:`True`, specified path may be an **existing** file.
* If :obj:`False`, then **existing** files are not allowed.
Defaults to :obj:`True`.
"""
dir_okay: bool = True
"""
If path exists, check it's type:
* If :obj:`True`, specified path may be an **existing** directory.
* If :obj:`False`, then **existing** directories are not allowed.
Defaults to :obj:`True`.
"""
# Can only ever really be a tuple[str, ...]
ext: Union[str, Sequence[str]] = field(default=None, converter=ext_converter)
"""
Supplied path must have this extension (case insensitive).
May or may not include the ".".
"""
def __attrs_post_init__(self):
if self.exists and not self.file_okay and not self.dir_okay:
raise ValueError("(exists=True, file_okay=False, dir_okay=False) is an invalid configuration.")
def __call__(self, type_: Any, path: Any):
if isinstance(path, Sequence):
if isinstance(path, str):
raise TypeError
for p in path:
self(type_, p)
else:
if not isinstance(path, pathlib.Path):
return
if self.ext and path.suffix.lower().lstrip(".") not in self.ext:
if len(self.ext) == 1:
raise ValueError(f'"{path}" must have extension "{self.ext[0]}".')
else:
pretty_ext = "{" + ", ".join(f'"{x}"' for x in self.ext) + "}"
raise ValueError(f'"{path}" does not match one of supported extensions {pretty_ext}.')
if path.exists():
if not self.file_okay and path.is_file():
if self.dir_okay:
raise ValueError(f'Only directory is allowed, but "{path}" is a file.')
else:
raise ValueError(f'"{path}" already exists.')
if not self.dir_okay and path.is_dir():
if self.file_okay:
raise ValueError(f'Only file is allowed, but "{path}" is a directory.')
else:
raise ValueError(f'"{path}" already exists.')
elif self.exists:
raise ValueError(f'"{path}" does not exist.')
|