2025-09-01 11:27:01 -04:00
|
|
|
# from jaraco.path 3.7.2
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
import functools
|
|
|
|
|
import pathlib
|
2025-09-01 11:27:01 -04:00
|
|
|
from collections.abc import Mapping
|
|
|
|
|
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from typing_extensions import Self
|
2023-04-20 22:12:48 -04:00
|
|
|
|
2023-12-21 15:04:05 -05:00
|
|
|
|
|
|
|
|
class Symlink(str):
|
|
|
|
|
"""
|
|
|
|
|
A string indicating the target of a symlink.
|
|
|
|
|
"""
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
|
2025-09-01 11:27:01 -04:00
|
|
|
FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']]
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@runtime_checkable
|
|
|
|
|
class TreeMaker(Protocol):
|
2025-09-01 11:27:01 -04:00
|
|
|
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: ...
|
2023-12-21 15:04:05 -05:00
|
|
|
|
2023-04-20 22:12:48 -04:00
|
|
|
|
2025-09-01 11:27:01 -04:00
|
|
|
def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
|
|
|
|
|
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def build(
|
|
|
|
|
spec: FilesSpec,
|
2025-09-01 11:27:01 -04:00
|
|
|
prefix: str | TreeMaker = pathlib.Path(),
|
2023-04-20 22:12:48 -04:00
|
|
|
):
|
|
|
|
|
"""
|
|
|
|
|
Build a set of files/directories, as described by the spec.
|
|
|
|
|
|
|
|
|
|
Each key represents a pathname, and the value represents
|
|
|
|
|
the content. Content may be a nested directory.
|
|
|
|
|
|
|
|
|
|
>>> spec = {
|
|
|
|
|
... 'README.txt': "A README file",
|
|
|
|
|
... "foo": {
|
|
|
|
|
... "__init__.py": "",
|
|
|
|
|
... "bar": {
|
|
|
|
|
... "__init__.py": "",
|
|
|
|
|
... },
|
|
|
|
|
... "baz.py": "# Some code",
|
2023-12-21 15:04:05 -05:00
|
|
|
... "bar.py": Symlink("baz.py"),
|
|
|
|
|
... },
|
|
|
|
|
... "bing": Symlink("foo"),
|
2023-04-20 22:12:48 -04:00
|
|
|
... }
|
|
|
|
|
>>> target = getfixture('tmp_path')
|
|
|
|
|
>>> build(spec, target)
|
|
|
|
|
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
|
|
|
|
|
'# Some code'
|
2023-12-21 15:04:05 -05:00
|
|
|
>>> target.joinpath('bing/bar.py').read_text(encoding='utf-8')
|
|
|
|
|
'# Some code'
|
2023-04-20 22:12:48 -04:00
|
|
|
"""
|
|
|
|
|
for name, contents in spec.items():
|
|
|
|
|
create(contents, _ensure_tree_maker(prefix) / name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@functools.singledispatch
|
2025-09-01 11:27:01 -04:00
|
|
|
def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None:
|
2023-04-20 22:12:48 -04:00
|
|
|
path.mkdir(exist_ok=True)
|
2025-09-01 11:27:01 -04:00
|
|
|
# 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
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@create.register
|
2025-09-01 11:27:01 -04:00
|
|
|
def _(content: bytes, path: TreeMaker) -> None:
|
2023-04-20 22:12:48 -04:00
|
|
|
path.write_bytes(content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@create.register
|
2025-09-01 11:27:01 -04:00
|
|
|
def _(content: str, path: TreeMaker) -> None:
|
2023-04-20 22:12:48 -04:00
|
|
|
path.write_text(content, encoding='utf-8')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@create.register
|
2025-09-01 11:27:01 -04:00
|
|
|
def _(content: Symlink, path: TreeMaker) -> None:
|
2023-12-21 15:04:05 -05:00
|
|
|
path.symlink_to(content)
|
2023-04-20 22:12:48 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Recording:
|
|
|
|
|
"""
|
|
|
|
|
A TreeMaker object that records everything that would be written.
|
|
|
|
|
|
|
|
|
|
>>> r = Recording()
|
|
|
|
|
>>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r)
|
|
|
|
|
>>> r.record
|
|
|
|
|
['foo/foo1.txt', 'bar.txt']
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, loc=pathlib.PurePosixPath(), record=None):
|
|
|
|
|
self.loc = loc
|
|
|
|
|
self.record = record if record is not None else []
|
|
|
|
|
|
|
|
|
|
def __truediv__(self, other):
|
|
|
|
|
return Recording(self.loc / other, self.record)
|
|
|
|
|
|
|
|
|
|
def write_text(self, content, **kwargs):
|
|
|
|
|
self.record.append(str(self.loc))
|
|
|
|
|
|
|
|
|
|
write_bytes = write_text
|
|
|
|
|
|
|
|
|
|
def mkdir(self, **kwargs):
|
|
|
|
|
return
|
2023-12-21 15:04:05 -05:00
|
|
|
|
|
|
|
|
def symlink_to(self, target):
|
|
|
|
|
pass
|