fss-cli 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fss_cli/__init__.py +24 -0
- fss_cli/_accounts.py +29 -0
- fss_cli/_app.py +663 -0
- fss_cli/_cat.py +391 -0
- fss_cli/_command.py +368 -0
- fss_cli/_concurrent.py +181 -0
- fss_cli/_content.py +94 -0
- fss_cli/_copy_file.py +65 -0
- fss_cli/_cp.py +740 -0
- fss_cli/_diagnostics.py +12 -0
- fss_cli/_du.py +90 -0
- fss_cli/_find.py +109 -0
- fss_cli/_head_tail.py +128 -0
- fss_cli/_info.py +179 -0
- fss_cli/_listing.py +289 -0
- fss_cli/_logging.py +72 -0
- fss_cli/_ls.py +435 -0
- fss_cli/_manifest.py +264 -0
- fss_cli/_metadata.py +70 -0
- fss_cli/_mkdir.py +97 -0
- fss_cli/_mv.py +146 -0
- fss_cli/_path.py +62 -0
- fss_cli/_post_copy.py +191 -0
- fss_cli/_progress.py +132 -0
- fss_cli/_recursive_cp.py +908 -0
- fss_cli/_recursive_rm.py +335 -0
- fss_cli/_results.py +115 -0
- fss_cli/_rm.py +149 -0
- fss_cli/_rmdir.py +148 -0
- fss_cli/_size.py +95 -0
- fss_cli/_sources.py +178 -0
- fss_cli/_stat.py +196 -0
- fss_cli/_test.py +59 -0
- fss_cli/_tree.py +208 -0
- fss_cli/_unlink.py +90 -0
- fss_cli/_walk.py +141 -0
- fss_cli-0.11.0.dist-info/METADATA +144 -0
- fss_cli-0.11.0.dist-info/RECORD +40 -0
- fss_cli-0.11.0.dist-info/WHEEL +4 -0
- fss_cli-0.11.0.dist-info/licenses/LICENSE +661 -0
fss_cli/_diagnostics.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shared rendering for stable command diagnostics."""
|
|
2
|
+
|
|
3
|
+
_ESCAPES = {code: f"\\x{code:02x}" for code in (*range(0x20), 0x7F)}
|
|
4
|
+
_ESCAPES[ord("\\")] = "\\\\"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _render_diagnostic_value(value: str) -> str:
|
|
8
|
+
return value.translate(_ESCAPES)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _render_diagnostic_prefix(command: str) -> str:
|
|
12
|
+
return f"{_render_diagnostic_value(command)}:"
|
fss_cli/_du.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Disk-usage execution for the central ``du`` callback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from ._command import (
|
|
9
|
+
_collate,
|
|
10
|
+
_Failure,
|
|
11
|
+
_MappedOperand,
|
|
12
|
+
_run_single_operand_text,
|
|
13
|
+
_valid_size,
|
|
14
|
+
)
|
|
15
|
+
from ._listing import format_size
|
|
16
|
+
from ._metadata import valid_display_text
|
|
17
|
+
from ._walk import _IncompatibleListingError, _ListedRow, _walk
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
|
|
22
|
+
from fsspec.asyn import AsyncFileSystem
|
|
23
|
+
|
|
24
|
+
from ._app import AsyncFilesystemSource
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class _DuRequest:
|
|
29
|
+
summarize: bool
|
|
30
|
+
human_readable: bool
|
|
31
|
+
operand: _MappedOperand
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _file_sizes(rows: list[_ListedRow]) -> dict[str, int] | None:
|
|
35
|
+
"""Collect each listed file's size by reported name, or ``None`` if invalid.
|
|
36
|
+
|
|
37
|
+
Like fsspec's ``_du``, every non-directory entry counts and a name listed
|
|
38
|
+
twice counts once.
|
|
39
|
+
"""
|
|
40
|
+
sizes: dict[str, int] = {}
|
|
41
|
+
try:
|
|
42
|
+
for row in rows:
|
|
43
|
+
for info in row.files.values():
|
|
44
|
+
path = info.get("name")
|
|
45
|
+
size = info.get("size")
|
|
46
|
+
if type(path) is not str or not valid_display_text(path) or not _valid_size(size):
|
|
47
|
+
return None
|
|
48
|
+
sizes[path] = size
|
|
49
|
+
except Exception: # noqa: BLE001 - fail closed on hostile mapping behavior.
|
|
50
|
+
return None
|
|
51
|
+
return sizes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _render_sizes(request: _DuRequest, sizes: Mapping[str, int]) -> str:
|
|
55
|
+
if request.summarize:
|
|
56
|
+
size = format_size(sum(sizes.values()), human_readable=request.human_readable)
|
|
57
|
+
return f"{size}\t{request.operand.path}\n"
|
|
58
|
+
return "".join(
|
|
59
|
+
f"{format_size(size, human_readable=request.human_readable)}\t{path}\n"
|
|
60
|
+
for path, size in sorted(sizes.items(), key=lambda entry: _collate(entry[0]))
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def _measure(
|
|
65
|
+
request: _DuRequest,
|
|
66
|
+
filesystem: AsyncFileSystem,
|
|
67
|
+
) -> str | _Failure:
|
|
68
|
+
try:
|
|
69
|
+
rows = await _walk(filesystem, request.operand.path)
|
|
70
|
+
except _IncompatibleListingError:
|
|
71
|
+
return _Failure(request.operand)
|
|
72
|
+
except Exception as error: # noqa: BLE001 - classify awaited backend failure.
|
|
73
|
+
return _Failure(request.operand, backend_error=error)
|
|
74
|
+
sizes = _file_sizes(rows)
|
|
75
|
+
if sizes is None:
|
|
76
|
+
return _Failure(request.operand)
|
|
77
|
+
return _render_sizes(request, sizes)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _run_du(
|
|
81
|
+
command: str,
|
|
82
|
+
request: _DuRequest,
|
|
83
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
84
|
+
) -> None:
|
|
85
|
+
await _run_single_operand_text(
|
|
86
|
+
command,
|
|
87
|
+
request.operand,
|
|
88
|
+
sources,
|
|
89
|
+
lambda filesystem: _measure(request, filesystem),
|
|
90
|
+
)
|
fss_cli/_find.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Recursive-find execution for the central ``find`` callback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, TypeGuard, cast
|
|
8
|
+
|
|
9
|
+
from ._command import (
|
|
10
|
+
_call,
|
|
11
|
+
_collate,
|
|
12
|
+
_Failure,
|
|
13
|
+
_MappedOperand,
|
|
14
|
+
_run_single_operand_text,
|
|
15
|
+
)
|
|
16
|
+
from ._metadata import valid_display_text
|
|
17
|
+
from ._walk import _IncompatibleListingError, _ListedRow, _walk
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from fsspec.asyn import AsyncFileSystem
|
|
21
|
+
|
|
22
|
+
from ._app import AsyncFilesystemSource
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class _FindRequest:
|
|
27
|
+
maxdepth: int | None
|
|
28
|
+
kind: Literal["f", "d"]
|
|
29
|
+
operand: _MappedOperand
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _render_paths(request: _FindRequest, paths: list[str]) -> str:
|
|
33
|
+
if request.maxdepth == 0:
|
|
34
|
+
root = request.operand.path.rstrip("/")
|
|
35
|
+
paths = [path for path in paths if path.rstrip("/") == root]
|
|
36
|
+
paths.sort(key=_collate)
|
|
37
|
+
return "".join(f"{path}\n" for path in dict.fromkeys(paths))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _selected_paths(
|
|
41
|
+
request: _FindRequest,
|
|
42
|
+
root_info: object,
|
|
43
|
+
rows: list[_ListedRow],
|
|
44
|
+
) -> list[str] | None:
|
|
45
|
+
"""Select displayable file or directory paths, or ``None`` if invalid.
|
|
46
|
+
|
|
47
|
+
File entries are every non-directory row entry, as fsspec's ``_find``
|
|
48
|
+
reports them; ``--type d`` reports the operand itself when it is a
|
|
49
|
+
directory plus every listed directory.
|
|
50
|
+
"""
|
|
51
|
+
infos: list[Mapping[object, object]] = []
|
|
52
|
+
if request.kind == "d":
|
|
53
|
+
if not isinstance(root_info, Mapping):
|
|
54
|
+
return None
|
|
55
|
+
infos.append(cast("Mapping[object, object]", root_info))
|
|
56
|
+
for row in rows:
|
|
57
|
+
if request.kind == "d":
|
|
58
|
+
infos.extend(row.directories.values())
|
|
59
|
+
infos.extend(row.files.values())
|
|
60
|
+
paths: list[str] = []
|
|
61
|
+
try:
|
|
62
|
+
for info in infos:
|
|
63
|
+
path = info.get("name")
|
|
64
|
+
kind = info.get("type")
|
|
65
|
+
if not _valid_path(path) or type(kind) is not str:
|
|
66
|
+
return None
|
|
67
|
+
if (kind == "directory") == (request.kind == "d"):
|
|
68
|
+
paths.append(path)
|
|
69
|
+
except Exception: # noqa: BLE001 - fail closed on hostile mapping consumption.
|
|
70
|
+
return None
|
|
71
|
+
return paths
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _valid_path(path: object) -> TypeGuard[str]:
|
|
75
|
+
return type(path) is str and valid_display_text(path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _search(
|
|
79
|
+
request: _FindRequest,
|
|
80
|
+
filesystem: AsyncFileSystem,
|
|
81
|
+
) -> str | _Failure:
|
|
82
|
+
try:
|
|
83
|
+
root_info = await _call(filesystem, "_info", request.operand.path) if request.kind == "d" else None
|
|
84
|
+
rows = await _walk(
|
|
85
|
+
filesystem,
|
|
86
|
+
request.operand.path,
|
|
87
|
+
maxdepth=1 if request.maxdepth == 0 else request.maxdepth,
|
|
88
|
+
)
|
|
89
|
+
except _IncompatibleListingError:
|
|
90
|
+
return _Failure(request.operand)
|
|
91
|
+
except Exception as error: # noqa: BLE001 - classify awaited backend failure.
|
|
92
|
+
return _Failure(request.operand, backend_error=error)
|
|
93
|
+
paths = _selected_paths(request, root_info, rows)
|
|
94
|
+
if paths is None:
|
|
95
|
+
return _Failure(request.operand)
|
|
96
|
+
return _render_paths(request, paths)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def _run_find(
|
|
100
|
+
command: str,
|
|
101
|
+
request: _FindRequest,
|
|
102
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
103
|
+
) -> None:
|
|
104
|
+
await _run_single_operand_text(
|
|
105
|
+
command,
|
|
106
|
+
request.operand,
|
|
107
|
+
sources,
|
|
108
|
+
lambda filesystem: _search(request, filesystem),
|
|
109
|
+
)
|
fss_cli/_head_tail.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Typed ``head`` and ``tail`` runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ._command import (
|
|
10
|
+
_binary_stdout,
|
|
11
|
+
_CommandFailureError,
|
|
12
|
+
_drain_current_operation,
|
|
13
|
+
_MappedOperand,
|
|
14
|
+
_run_mapped_command,
|
|
15
|
+
_valid_size,
|
|
16
|
+
_write_binary,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
from fsspec.asyn import AsyncFileSystem
|
|
23
|
+
|
|
24
|
+
from ._app import AsyncFilesystemSource
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class _ByteRangeRequest:
|
|
29
|
+
count: int
|
|
30
|
+
operand: _MappedOperand
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _read_head(
|
|
34
|
+
request: _ByteRangeRequest,
|
|
35
|
+
filesystem: AsyncFileSystem,
|
|
36
|
+
) -> bytes:
|
|
37
|
+
if request.count == 0:
|
|
38
|
+
try:
|
|
39
|
+
info = await _drain_current_operation(filesystem._info(request.operand.path))
|
|
40
|
+
except Exception as error:
|
|
41
|
+
raise _CommandFailureError(request.operand, error) from error
|
|
42
|
+
if _is_file(request.operand, info):
|
|
43
|
+
return b""
|
|
44
|
+
try:
|
|
45
|
+
result = await _drain_current_operation(filesystem._cat_file(request.operand.path, start=0, end=request.count))
|
|
46
|
+
except Exception as error:
|
|
47
|
+
raise _CommandFailureError(request.operand, error) from error
|
|
48
|
+
if type(result) is not bytes or len(result) > request.count:
|
|
49
|
+
raise _CommandFailureError(request.operand)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _info_field(operand: _MappedOperand, info: object, key: str) -> object:
|
|
54
|
+
if not isinstance(info, Mapping):
|
|
55
|
+
raise _CommandFailureError(operand)
|
|
56
|
+
try:
|
|
57
|
+
return info.get(key)
|
|
58
|
+
except Exception: # noqa: BLE001 - hostile metadata mapping boundary.
|
|
59
|
+
raise _CommandFailureError(operand) from None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _size_from_info(operand: _MappedOperand, info: object) -> int:
|
|
63
|
+
size = _info_field(operand, info, "size")
|
|
64
|
+
if not _valid_size(size):
|
|
65
|
+
raise _CommandFailureError(operand)
|
|
66
|
+
return size
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_file(operand: _MappedOperand, info: object) -> bool:
|
|
70
|
+
"""Return whether ``info`` proves a regular file, so a zero read needs no bytes.
|
|
71
|
+
|
|
72
|
+
Any other kind falls back to the bounded read, so the backend still owns
|
|
73
|
+
the diagnostic for directories and other non-file operands.
|
|
74
|
+
"""
|
|
75
|
+
kind = _info_field(operand, info, "type")
|
|
76
|
+
return type(kind) is str and kind == "file"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _read_tail(
|
|
80
|
+
request: _ByteRangeRequest,
|
|
81
|
+
filesystem: AsyncFileSystem,
|
|
82
|
+
) -> bytes:
|
|
83
|
+
try:
|
|
84
|
+
info = await _drain_current_operation(filesystem._info(request.operand.path))
|
|
85
|
+
except Exception as error:
|
|
86
|
+
raise _CommandFailureError(request.operand, error) from error
|
|
87
|
+
_size_from_info(request.operand, info)
|
|
88
|
+
if request.count == 0 and _is_file(request.operand, info):
|
|
89
|
+
return b""
|
|
90
|
+
try:
|
|
91
|
+
result = await _drain_current_operation(
|
|
92
|
+
filesystem._cat_file(request.operand.path, start=-request.count, end=None)
|
|
93
|
+
)
|
|
94
|
+
except Exception as error:
|
|
95
|
+
raise _CommandFailureError(request.operand, error) from error
|
|
96
|
+
if type(result) is not bytes or len(result) > request.count:
|
|
97
|
+
raise _CommandFailureError(request.operand)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _emit(payload: bytes) -> None:
|
|
102
|
+
if not payload:
|
|
103
|
+
return
|
|
104
|
+
stdout = _binary_stdout()
|
|
105
|
+
_write_binary(stdout, payload)
|
|
106
|
+
stdout.flush()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _run_byte_range(
|
|
110
|
+
command: str,
|
|
111
|
+
count: int,
|
|
112
|
+
operand: _MappedOperand,
|
|
113
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
114
|
+
operation: Callable[
|
|
115
|
+
[_ByteRangeRequest, AsyncFileSystem],
|
|
116
|
+
Awaitable[bytes],
|
|
117
|
+
],
|
|
118
|
+
) -> None:
|
|
119
|
+
request = _ByteRangeRequest(count, operand)
|
|
120
|
+
|
|
121
|
+
async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
|
|
122
|
+
payload = await operation(request, filesystems[operand.name])
|
|
123
|
+
try:
|
|
124
|
+
_emit(payload)
|
|
125
|
+
except Exception as error:
|
|
126
|
+
raise _CommandFailureError(error=error) from error
|
|
127
|
+
|
|
128
|
+
await _run_mapped_command(command, (operand,), sources, execute)
|
fss_cli/_info.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Normalized metadata execution for typed ``info``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pprint
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, cast
|
|
9
|
+
|
|
10
|
+
from ._command import (
|
|
11
|
+
_binary_stdout,
|
|
12
|
+
_CommandFailureError,
|
|
13
|
+
_drain_current_operation,
|
|
14
|
+
_MappedOperand,
|
|
15
|
+
_run_mapped_command,
|
|
16
|
+
_write_binary,
|
|
17
|
+
)
|
|
18
|
+
from ._listing import ListingRow, to_listing
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from fsspec.asyn import AsyncFileSystem
|
|
22
|
+
|
|
23
|
+
from ._app import AsyncFilesystemSource
|
|
24
|
+
|
|
25
|
+
_PRETTY_WIDTH = 80
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, order=True)
|
|
29
|
+
class _StablePresentation:
|
|
30
|
+
sort_type: str
|
|
31
|
+
text: str
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return self.text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _canonical_value(value: object, active: set[int]) -> object:
|
|
38
|
+
if not isinstance(value, (Mapping, list, tuple, set, frozenset)):
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
identity = id(value)
|
|
42
|
+
if identity in active:
|
|
43
|
+
message = "recursive metadata containers are not supported"
|
|
44
|
+
raise ValueError(message)
|
|
45
|
+
active.add(identity)
|
|
46
|
+
try:
|
|
47
|
+
if isinstance(value, Mapping):
|
|
48
|
+
canonical: object = _canonical_mapping(cast("Mapping[object, object]", value), active)
|
|
49
|
+
elif isinstance(value, list):
|
|
50
|
+
canonical = [_canonical_value(item, active) for item in value]
|
|
51
|
+
elif isinstance(value, tuple):
|
|
52
|
+
canonical = tuple(_canonical_value(item, active) for item in value)
|
|
53
|
+
elif isinstance(value, set):
|
|
54
|
+
canonical = _canonical_set(cast("set[object]", value), active, frozen=False)
|
|
55
|
+
else:
|
|
56
|
+
canonical = _canonical_set(value, active, frozen=True)
|
|
57
|
+
return canonical
|
|
58
|
+
finally:
|
|
59
|
+
active.remove(identity)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _canonical_mapping(
|
|
63
|
+
value: Mapping[object, object],
|
|
64
|
+
active: set[int],
|
|
65
|
+
) -> dict[object, object]:
|
|
66
|
+
source_length = len(value)
|
|
67
|
+
entries: list[tuple[object, object]] = []
|
|
68
|
+
spellings: set[str] = set()
|
|
69
|
+
for key in value:
|
|
70
|
+
item = value[key]
|
|
71
|
+
canonical_key = _canonical_value(key, active)
|
|
72
|
+
hash(canonical_key)
|
|
73
|
+
spelling = _pretty(canonical_key)
|
|
74
|
+
if spelling in spellings:
|
|
75
|
+
message = "distinct mapping keys have the same presentation"
|
|
76
|
+
raise ValueError(message)
|
|
77
|
+
spellings.add(spelling)
|
|
78
|
+
entries.append(
|
|
79
|
+
(
|
|
80
|
+
_StablePresentation(
|
|
81
|
+
f"{type(key).__module__}.{type(key).__qualname__}",
|
|
82
|
+
spelling,
|
|
83
|
+
),
|
|
84
|
+
_canonical_value(item, active),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if len(entries) != source_length:
|
|
89
|
+
message = "mapping iteration does not match its reported length"
|
|
90
|
+
raise ValueError(message)
|
|
91
|
+
canonical = dict(entries)
|
|
92
|
+
if len(canonical) != source_length:
|
|
93
|
+
message = "canonical mapping keys are not distinct"
|
|
94
|
+
raise ValueError(message)
|
|
95
|
+
return canonical
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _canonical_set(
|
|
99
|
+
value: set[object] | frozenset[object],
|
|
100
|
+
active: set[int],
|
|
101
|
+
*,
|
|
102
|
+
frozen: bool,
|
|
103
|
+
) -> _StablePresentation:
|
|
104
|
+
members = sorted(_pretty(_canonical_value(item, active)) for item in value)
|
|
105
|
+
if frozen:
|
|
106
|
+
spelling = "frozenset()" if not members else f"frozenset({{{', '.join(members)}}})"
|
|
107
|
+
else:
|
|
108
|
+
spelling = "set()" if not members else f"{{{', '.join(members)}}}"
|
|
109
|
+
return _StablePresentation(f"{type(value).__module__}.{type(value).__qualname__}", spelling)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pretty(value: object) -> str:
|
|
113
|
+
return pprint.pformat(value, width=_PRETTY_WIDTH, sort_dicts=True)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _render_info(row: ListingRow) -> bytes:
|
|
117
|
+
values = {
|
|
118
|
+
"name": row.name,
|
|
119
|
+
"kind": row.kind,
|
|
120
|
+
"size": row.size,
|
|
121
|
+
"mtime": row.mtime,
|
|
122
|
+
"mode": row.mode,
|
|
123
|
+
"nlink": row.nlink,
|
|
124
|
+
"owner": row.owner,
|
|
125
|
+
"group": row.group,
|
|
126
|
+
"link_target": row.link_target,
|
|
127
|
+
"extra": _canonical_value(row.extra, set()),
|
|
128
|
+
}
|
|
129
|
+
rendered = _pretty(values)
|
|
130
|
+
return f"{rendered}\n".encode()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _normalize_info(result: object) -> bytes | None:
|
|
134
|
+
if not isinstance(result, Mapping):
|
|
135
|
+
return None
|
|
136
|
+
try:
|
|
137
|
+
if any(type(key) is not str for key in result):
|
|
138
|
+
return None
|
|
139
|
+
row = to_listing(cast("Mapping[str, object]", result))
|
|
140
|
+
return _render_info(row)
|
|
141
|
+
except Exception: # noqa: BLE001 - malformed mapping or printable value.
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def _read_info(
|
|
146
|
+
operand: _MappedOperand,
|
|
147
|
+
filesystem: AsyncFileSystem,
|
|
148
|
+
) -> bytes:
|
|
149
|
+
try:
|
|
150
|
+
result = await _drain_current_operation(filesystem._info(operand.path))
|
|
151
|
+
except Exception as error:
|
|
152
|
+
raise _CommandFailureError(operand, error) from error
|
|
153
|
+
payload = _normalize_info(result)
|
|
154
|
+
if payload is None:
|
|
155
|
+
raise _CommandFailureError(operand)
|
|
156
|
+
return payload
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def _run_info(
|
|
160
|
+
command: str,
|
|
161
|
+
operand: _MappedOperand,
|
|
162
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
163
|
+
) -> None:
|
|
164
|
+
async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
|
|
165
|
+
payload = await _read_info(operand, filesystems[operand.name])
|
|
166
|
+
try:
|
|
167
|
+
stdout = _binary_stdout()
|
|
168
|
+
_write_binary(stdout, payload)
|
|
169
|
+
stdout.flush()
|
|
170
|
+
except Exception as error:
|
|
171
|
+
raise _CommandFailureError(error=error) from error
|
|
172
|
+
|
|
173
|
+
await _run_mapped_command(
|
|
174
|
+
command,
|
|
175
|
+
(operand,),
|
|
176
|
+
sources,
|
|
177
|
+
execute,
|
|
178
|
+
broken_pipe_exit_code=1,
|
|
179
|
+
)
|