diff --git a/jaraco/path.py b/jaraco/path.py index c49a842..46538a5 100644 --- a/jaraco/path.py +++ b/jaraco/path.py @@ -2,6 +2,8 @@ Tools for working with files and file systems """ +from __future__ import annotations + import os import re import itertools @@ -16,9 +18,10 @@ import ctypes import importlib import pathlib -from typing import Dict, Protocol, Union -from typing import runtime_checkable +from typing import TYPE_CHECKING, Dict, Protocol, Union, runtime_checkable +if TYPE_CHECKING: + from typing_extensions import Self log = logging.getLogger(__name__) @@ -290,24 +293,20 @@ class Symlink(str): @runtime_checkable class TreeMaker(Protocol): - def __truediv__(self, *args, **kwargs): ... # pragma: no cover - - def mkdir(self, **kwargs): ... # pragma: no cover - - def write_text(self, content, **kwargs): ... # pragma: no cover - - def write_bytes(self, content): ... # pragma: no cover - - def symlink_to(self, target): ... # pragma: no cover + def __truediv__(self, other, /) -> Self: ... + def mkdir(self, *, exist_ok) -> object: ... + def write_text(self, content, /, *, encoding) -> object: ... + def write_bytes(self, content, /) -> object: ... + def symlink_to(self, target, /) -> object: ... -def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] +def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: str | TreeMaker = pathlib.Path(), ): """ Build a set of files/directories, as described by the spec. @@ -339,23 +338,24 @@ def build( @functools.singledispatch -def create(content: Union[str, bytes, FilesSpec], path): +def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None: path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore[arg-type] + # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union + build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 @create.register -def _(content: bytes, path): +def _(content: bytes, path: TreeMaker) -> None: path.write_bytes(content) @create.register -def _(content: str, path): +def _(content: str, path: TreeMaker) -> None: path.write_text(content, encoding='utf-8') @create.register -def _(content: Symlink, path): +def _(content: Symlink, path: TreeMaker) -> None: path.symlink_to(content) diff --git a/tests/test_path.py b/tests/test_path.py index c581e39..febf795 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,4 +1,5 @@ import os +import pathlib import platform import pytest @@ -43,3 +44,12 @@ def test_is_hidden_Darwin(): target = os.path.expanduser('~/Library') assert path.is_hidden(target) assert path.is_hidden_Darwin(target) + + +def test_TreeMaker_Protocol() -> None: + # Ensure the validity of the TreeMaker Protocol both statically and at runtime + tree_maker: path.TreeMaker + tree_maker = pathlib.Path() + assert isinstance(tree_maker, path.TreeMaker) + tree_maker = path.Recording() + assert isinstance(tree_maker, path.TreeMaker)