122 lines
4.5 KiB
Python
122 lines
4.5 KiB
Python
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.')
|