oaknut-cli 12.0.0__tar.gz
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.
- oaknut_cli-12.0.0/LICENSE +21 -0
- oaknut_cli-12.0.0/PKG-INFO +41 -0
- oaknut_cli-12.0.0/README.md +13 -0
- oaknut_cli-12.0.0/pyproject.toml +52 -0
- oaknut_cli-12.0.0/setup.cfg +4 -0
- oaknut_cli-12.0.0/src/oaknut/cli/__init__.py +51 -0
- oaknut_cli-12.0.0/src/oaknut/cli/commands.py +55 -0
- oaknut_cli-12.0.0/src/oaknut/cli/help.py +71 -0
- oaknut_cli-12.0.0/src/oaknut/cli/reports.py +58 -0
- oaknut_cli-12.0.0/src/oaknut_cli.egg-info/PKG-INFO +41 -0
- oaknut_cli-12.0.0/src/oaknut_cli.egg-info/SOURCES.txt +14 -0
- oaknut_cli-12.0.0/src/oaknut_cli.egg-info/dependency_links.txt +1 -0
- oaknut_cli-12.0.0/src/oaknut_cli.egg-info/requires.txt +5 -0
- oaknut_cli-12.0.0/src/oaknut_cli.egg-info/top_level.txt +1 -0
- oaknut_cli-12.0.0/tests/test_commands.py +47 -0
- oaknut_cli-12.0.0/tests/test_help.py +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Smallshire
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oaknut-cli
|
|
3
|
+
Version: 12.0.0
|
|
4
|
+
Summary: Shared CLI toolkit for the oaknut family: the contributed-command axis and report-rendering helpers a disc command needs, below the filesystem packages.
|
|
5
|
+
Author-email: Robert Smallshire <robert@smallshire.org.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/rob-smallshire/oaknut
|
|
9
|
+
Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: oaknut-exception>=10.0
|
|
23
|
+
Requires-Dist: oaknut-extension>=10.0
|
|
24
|
+
Requires-Dist: oaknut-file>=10.0
|
|
25
|
+
Requires-Dist: click>=8.1.7
|
|
26
|
+
Requires-Dist: asyoulikeit>=1.2.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# oaknut-cli
|
|
30
|
+
|
|
31
|
+
Shared CLI toolkit for the [oaknut](https://github.com/rob-smallshire/oaknut)
|
|
32
|
+
family. It sits *below* the filesystem packages so that both the `disc`
|
|
33
|
+
CLI (`oaknut-disc`) and a filesystem's own contributed commands can
|
|
34
|
+
depend on it without a dependency cycle.
|
|
35
|
+
|
|
36
|
+
It provides the **contributed-command axis** — discovery of Click
|
|
37
|
+
commands a filesystem package registers on the `oaknut.command`
|
|
38
|
+
entry-point namespace — and report-rendering helpers shared between the
|
|
39
|
+
generic `disc` commands and the contributed ones.
|
|
40
|
+
|
|
41
|
+
See `docs/dev/contributed-commands.md` for the design.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# oaknut-cli
|
|
2
|
+
|
|
3
|
+
Shared CLI toolkit for the [oaknut](https://github.com/rob-smallshire/oaknut)
|
|
4
|
+
family. It sits *below* the filesystem packages so that both the `disc`
|
|
5
|
+
CLI (`oaknut-disc`) and a filesystem's own contributed commands can
|
|
6
|
+
depend on it without a dependency cycle.
|
|
7
|
+
|
|
8
|
+
It provides the **contributed-command axis** — discovery of Click
|
|
9
|
+
commands a filesystem package registers on the `oaknut.command`
|
|
10
|
+
entry-point namespace — and report-rendering helpers shared between the
|
|
11
|
+
generic `disc` commands and the contributed ones.
|
|
12
|
+
|
|
13
|
+
See `docs/dev/contributed-commands.md` for the design.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oaknut-cli"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [{ name = "Robert Smallshire", email = "robert@smallshire.org.uk" }]
|
|
9
|
+
description = "Shared CLI toolkit for the oaknut family: the contributed-command axis and report-rendering helpers a disc command needs, below the filesystem packages."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: System :: Filesystems",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"oaknut-exception>=10.0",
|
|
27
|
+
"oaknut-extension>=10.0",
|
|
28
|
+
"oaknut-file>=10.0",
|
|
29
|
+
"click>=8.1.7",
|
|
30
|
+
"asyoulikeit>=1.2.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-cli"
|
|
35
|
+
Repository = "https://github.com/rob-smallshire/oaknut"
|
|
36
|
+
Issues = "https://github.com/rob-smallshire/oaknut/issues"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
test = [
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
]
|
|
42
|
+
dev = [
|
|
43
|
+
"bump-my-version>=0.28.0",
|
|
44
|
+
"pre-commit>=3.0",
|
|
45
|
+
{include-group = "test"},
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = { attr = "oaknut.cli.__version__" }
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared CLI toolkit for the oaknut family.
|
|
2
|
+
|
|
3
|
+
This package sits *below* the filesystem packages (oaknut-dfs/adfs/afs)
|
|
4
|
+
so that both the ``disc`` CLI (``oaknut-disc``) and a filesystem's own
|
|
5
|
+
contributed commands can depend on it without a cycle — ``oaknut-disc``
|
|
6
|
+
depends on the filesystem packages, so they must never depend on it.
|
|
7
|
+
|
|
8
|
+
It provides:
|
|
9
|
+
|
|
10
|
+
- the **contributed-command axis** — discovery of Click commands
|
|
11
|
+
registered by filesystem packages on the ``oaknut.command`` entry-point
|
|
12
|
+
namespace (see :func:`contributed_commands`); and
|
|
13
|
+
- report-rendering helpers shared between the generic ``disc`` commands
|
|
14
|
+
and the contributed ones.
|
|
15
|
+
|
|
16
|
+
See ``docs/dev/contributed-commands.md`` for the design.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from oaknut.cli.commands import (
|
|
22
|
+
COMMAND_KIND,
|
|
23
|
+
COMMAND_NAMESPACE,
|
|
24
|
+
contributed_commands,
|
|
25
|
+
)
|
|
26
|
+
from oaknut.cli.help import (
|
|
27
|
+
PlainHelpFormatter,
|
|
28
|
+
strip_rst,
|
|
29
|
+
use_plain_help,
|
|
30
|
+
)
|
|
31
|
+
from oaknut.cli.reports import (
|
|
32
|
+
SECTOR_SIZE,
|
|
33
|
+
address_cell,
|
|
34
|
+
kv_table,
|
|
35
|
+
size_cell,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "12.0.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"COMMAND_KIND",
|
|
42
|
+
"COMMAND_NAMESPACE",
|
|
43
|
+
"contributed_commands",
|
|
44
|
+
"PlainHelpFormatter",
|
|
45
|
+
"strip_rst",
|
|
46
|
+
"use_plain_help",
|
|
47
|
+
"SECTOR_SIZE",
|
|
48
|
+
"address_cell",
|
|
49
|
+
"kv_table",
|
|
50
|
+
"size_cell",
|
|
51
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The contributed-command axis.
|
|
2
|
+
|
|
3
|
+
A filesystem package contributes admin subcommands to the ``disc`` CLI by
|
|
4
|
+
registering a Click command (or group) on the ``oaknut.command``
|
|
5
|
+
entry-point namespace::
|
|
6
|
+
|
|
7
|
+
[project.entry-points."oaknut.command"]
|
|
8
|
+
afs = "oaknut.afs.cli:afs" # afs is a click.Group
|
|
9
|
+
|
|
10
|
+
:func:`contributed_commands` discovers them; the ``disc`` root group
|
|
11
|
+
attaches each with ``cli.add_command(...)``. An install sees exactly the
|
|
12
|
+
commands its installed filesystems contribute.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
import stevedore
|
|
20
|
+
from oaknut.extension import namespace_for
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import click
|
|
24
|
+
|
|
25
|
+
#: The extension *kind* (axis) contributed CLI commands belong to.
|
|
26
|
+
COMMAND_KIND = "command"
|
|
27
|
+
|
|
28
|
+
#: The entry-point namespace commands register under: ``"oaknut.command"``.
|
|
29
|
+
COMMAND_NAMESPACE = namespace_for(COMMAND_KIND)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _skip_unloadable(manager, entrypoint, exception) -> None:
|
|
33
|
+
"""Ignore a command whose package cannot be imported.
|
|
34
|
+
|
|
35
|
+
A filesystem installed without its optional ``[cli]`` extra (so Click
|
|
36
|
+
is absent) leaves its command entry point unloadable. That command is
|
|
37
|
+
simply unavailable — not a fatal error — so the CLI degrades
|
|
38
|
+
gracefully rather than crashing.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def contributed_commands() -> list["click.Command"]:
|
|
43
|
+
"""Every Click command contributed on the ``oaknut.command`` axis.
|
|
44
|
+
|
|
45
|
+
Each entry point's target is a ready-made Click ``Command`` or
|
|
46
|
+
``Group`` object. Returned sorted by name so the CLI's command
|
|
47
|
+
listing is deterministic regardless of discovery order.
|
|
48
|
+
"""
|
|
49
|
+
manager = stevedore.ExtensionManager(
|
|
50
|
+
namespace=COMMAND_NAMESPACE,
|
|
51
|
+
invoke_on_load=False,
|
|
52
|
+
on_load_failure_callback=_skip_unloadable,
|
|
53
|
+
)
|
|
54
|
+
commands = [extension.plugin for extension in manager]
|
|
55
|
+
return sorted(commands, key=lambda command: getattr(command, "name", "") or "")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Terminal-friendly help: strip RST inline markup as Click renders it.
|
|
2
|
+
|
|
3
|
+
Command docstrings and option help are dual-purpose — Click prints them
|
|
4
|
+
verbatim at the terminal, while the ``disc`` Sphinx command reference
|
|
5
|
+
renders them as reStructuredText. RST inline markup (``literal`` spans)
|
|
6
|
+
that reads as code in the manual shows as stray backticks in a terminal.
|
|
7
|
+
|
|
8
|
+
:func:`use_plain_help` makes a command (and, for a group, its whole
|
|
9
|
+
subtree) render help through a formatter that reduces that markup to
|
|
10
|
+
plain text at display time, leaving the docstrings untouched for Sphinx.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from collections.abc import Iterable
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
__all__ = ["strip_rst", "PlainHelpFormatter", "use_plain_help"]
|
|
21
|
+
|
|
22
|
+
# ``literal`` and `interpreted` spans — one or two backticks either side.
|
|
23
|
+
_BACKTICK_SPAN = re.compile(r"`{1,2}([^`]+)`{1,2}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def strip_rst(text: str) -> str:
|
|
27
|
+
"""Reduce the RST inline markup used in help text to plain text.
|
|
28
|
+
|
|
29
|
+
``code`` and `code` both collapse to ``code`` — readable at a
|
|
30
|
+
terminal. Only backtick spans are touched; everything else is left
|
|
31
|
+
as written.
|
|
32
|
+
"""
|
|
33
|
+
return _BACKTICK_SPAN.sub(r"\1", text)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PlainHelpFormatter(click.HelpFormatter):
|
|
37
|
+
"""A Click help formatter that strips RST markup as it writes.
|
|
38
|
+
|
|
39
|
+
Every piece of help text — the body paragraphs (:meth:`write_text`)
|
|
40
|
+
and the option / subcommand definition lists (:meth:`write_dl`) —
|
|
41
|
+
passes through :func:`strip_rst` on the way out.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def write_text(self, text: str) -> None:
|
|
45
|
+
super().write_text(strip_rst(text))
|
|
46
|
+
|
|
47
|
+
def write_dl(self, rows: Iterable[tuple[str, str]], *args: object, **kwargs: object) -> None:
|
|
48
|
+
super().write_dl(
|
|
49
|
+
[(strip_rst(term), strip_rst(definition)) for term, definition in rows],
|
|
50
|
+
*args,
|
|
51
|
+
**kwargs,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _PlainHelpContext(click.Context):
|
|
56
|
+
"""A Click context whose help is formatted by :class:`PlainHelpFormatter`."""
|
|
57
|
+
|
|
58
|
+
formatter_class = PlainHelpFormatter
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def use_plain_help(command: click.Command) -> None:
|
|
62
|
+
"""Render *command*'s help with RST markup stripped.
|
|
63
|
+
|
|
64
|
+
For a group, applies to the whole subtree, so a single call on a
|
|
65
|
+
fully-assembled CLI covers every subcommand — including ones
|
|
66
|
+
contributed by other packages, regardless of how they were built.
|
|
67
|
+
"""
|
|
68
|
+
command.context_class = _PlainHelpContext
|
|
69
|
+
if isinstance(command, click.Group):
|
|
70
|
+
for sub in command.commands.values():
|
|
71
|
+
use_plain_help(sub)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Report-rendering helpers shared across ``disc`` commands.
|
|
2
|
+
|
|
3
|
+
Audience-aware cells (a friendly string for humans, a raw value for
|
|
4
|
+
machine formatters) and a transposed key-value table, used by both the
|
|
5
|
+
generic ``disc`` commands and the filesystem-contributed ones. Built on
|
|
6
|
+
asyoulikeit; kept here, below the filesystem packages, so a contributed
|
|
7
|
+
command can render output without depending on ``oaknut-disc``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from asyoulikeit import ByAudience
|
|
13
|
+
from oaknut.file.capacity import format_capacity
|
|
14
|
+
|
|
15
|
+
#: Acorn discs are addressed in 256-byte sectors throughout.
|
|
16
|
+
SECTOR_SIZE = 256
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def size_cell(sectors: int) -> ByAudience:
|
|
20
|
+
"""A capacity (given in sectors) as an audience-aware cell.
|
|
21
|
+
|
|
22
|
+
Humans read friendly IEC units (``800.0 KiB``); machine formatters
|
|
23
|
+
get the raw byte count as an integer, so the presentation base is
|
|
24
|
+
irrelevant to a consumer.
|
|
25
|
+
"""
|
|
26
|
+
num_bytes = sectors * SECTOR_SIZE
|
|
27
|
+
return ByAudience(machine=num_bytes, human=format_capacity(num_bytes))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def address_cell(address: int) -> ByAudience:
|
|
31
|
+
"""A 32-bit Acorn address as an audience-aware cell.
|
|
32
|
+
|
|
33
|
+
Humans read the conventional ``0x``-prefixed 8-hex-digit form;
|
|
34
|
+
machine formatters (JSON, TSV) get the raw integer, so a consumer
|
|
35
|
+
never has to parse a base back out of a string.
|
|
36
|
+
"""
|
|
37
|
+
return ByAudience(machine=address, human=f"0x{address:08X}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def kv_table(title: str, pairs: list[tuple[str, str, object]]):
|
|
41
|
+
"""Build a transposed single-row table from (key, label, value) tuples.
|
|
42
|
+
|
|
43
|
+
Each tuple becomes a column whose sole row holds the value. A value
|
|
44
|
+
may be a plain string, an integer (for machine-readable counts), or
|
|
45
|
+
a :class:`~asyoulikeit.ByAudience` cell that renders one way for
|
|
46
|
+
humans and another for machine formatters. Transposed presentation
|
|
47
|
+
turns the one-row table into a key-value report in the display
|
|
48
|
+
formatter.
|
|
49
|
+
"""
|
|
50
|
+
from asyoulikeit.tabular_data import TableContent
|
|
51
|
+
|
|
52
|
+
tc = TableContent(title=title, present_transposed=True)
|
|
53
|
+
row: dict = {}
|
|
54
|
+
for index, (key, label, value) in enumerate(pairs):
|
|
55
|
+
tc.add_column(key, label, header=(index == 0))
|
|
56
|
+
row[key] = value
|
|
57
|
+
tc.add_row(**row)
|
|
58
|
+
return tc
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oaknut-cli
|
|
3
|
+
Version: 12.0.0
|
|
4
|
+
Summary: Shared CLI toolkit for the oaknut family: the contributed-command axis and report-rendering helpers a disc command needs, below the filesystem packages.
|
|
5
|
+
Author-email: Robert Smallshire <robert@smallshire.org.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rob-smallshire/oaknut/tree/master/packages/oaknut-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/rob-smallshire/oaknut
|
|
9
|
+
Project-URL: Issues, https://github.com/rob-smallshire/oaknut/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: oaknut-exception>=10.0
|
|
23
|
+
Requires-Dist: oaknut-extension>=10.0
|
|
24
|
+
Requires-Dist: oaknut-file>=10.0
|
|
25
|
+
Requires-Dist: click>=8.1.7
|
|
26
|
+
Requires-Dist: asyoulikeit>=1.2.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# oaknut-cli
|
|
30
|
+
|
|
31
|
+
Shared CLI toolkit for the [oaknut](https://github.com/rob-smallshire/oaknut)
|
|
32
|
+
family. It sits *below* the filesystem packages so that both the `disc`
|
|
33
|
+
CLI (`oaknut-disc`) and a filesystem's own contributed commands can
|
|
34
|
+
depend on it without a dependency cycle.
|
|
35
|
+
|
|
36
|
+
It provides the **contributed-command axis** — discovery of Click
|
|
37
|
+
commands a filesystem package registers on the `oaknut.command`
|
|
38
|
+
entry-point namespace — and report-rendering helpers shared between the
|
|
39
|
+
generic `disc` commands and the contributed ones.
|
|
40
|
+
|
|
41
|
+
See `docs/dev/contributed-commands.md` for the design.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/oaknut/cli/__init__.py
|
|
5
|
+
src/oaknut/cli/commands.py
|
|
6
|
+
src/oaknut/cli/help.py
|
|
7
|
+
src/oaknut/cli/reports.py
|
|
8
|
+
src/oaknut_cli.egg-info/PKG-INFO
|
|
9
|
+
src/oaknut_cli.egg-info/SOURCES.txt
|
|
10
|
+
src/oaknut_cli.egg-info/dependency_links.txt
|
|
11
|
+
src/oaknut_cli.egg-info/requires.txt
|
|
12
|
+
src/oaknut_cli.egg-info/top_level.txt
|
|
13
|
+
tests/test_commands.py
|
|
14
|
+
tests/test_help.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oaknut
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Tests for the contributed-command axis."""
|
|
2
|
+
|
|
3
|
+
from oaknut.cli import COMMAND_NAMESPACE, contributed_commands
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_command_namespace():
|
|
7
|
+
assert COMMAND_NAMESPACE == "oaknut.command"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_contributed_commands_is_a_sorted_list():
|
|
11
|
+
# Whatever command-contributing packages are installed, discovery
|
|
12
|
+
# returns a list and does not raise. (Empty until a filesystem
|
|
13
|
+
# registers an oaknut.command entry point.)
|
|
14
|
+
commands = contributed_commands()
|
|
15
|
+
assert isinstance(commands, list)
|
|
16
|
+
names = [getattr(c, "name", "") for c in commands]
|
|
17
|
+
assert names == sorted(names)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestSkipsUnloadable:
|
|
21
|
+
def test_a_failing_entry_point_does_not_crash_discovery(self, monkeypatch):
|
|
22
|
+
# Simulate one entry point importing nothing usable (e.g. a
|
|
23
|
+
# filesystem installed without its [cli] extra): discovery must
|
|
24
|
+
# skip it, not raise.
|
|
25
|
+
import stevedore
|
|
26
|
+
from oaknut.cli import commands as commands_module
|
|
27
|
+
|
|
28
|
+
class _FakeExtension:
|
|
29
|
+
name = "ok"
|
|
30
|
+
plugin = type("Cmd", (), {"name": "ok"})()
|
|
31
|
+
|
|
32
|
+
class _FakeManager:
|
|
33
|
+
def __init__(self, *args, on_load_failure_callback=None, **kwargs):
|
|
34
|
+
# Exercise the skip callback with a synthetic failure, then
|
|
35
|
+
# yield one good extension.
|
|
36
|
+
if on_load_failure_callback is not None:
|
|
37
|
+
on_load_failure_callback(self, _FakeEntryPoint(), ImportError("no click"))
|
|
38
|
+
|
|
39
|
+
def __iter__(self):
|
|
40
|
+
return iter([_FakeExtension()])
|
|
41
|
+
|
|
42
|
+
class _FakeEntryPoint:
|
|
43
|
+
name = "broken"
|
|
44
|
+
|
|
45
|
+
monkeypatch.setattr(stevedore, "ExtensionManager", _FakeManager)
|
|
46
|
+
result = commands_module.contributed_commands()
|
|
47
|
+
assert [c.name for c in result] == ["ok"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Tests for the terminal-friendly help machinery."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
from oaknut.cli import PlainHelpFormatter, strip_rst, use_plain_help
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestStripRst:
|
|
9
|
+
def test_double_backticks_become_plain(self):
|
|
10
|
+
assert strip_rst("pass ``--geometry`` to set it") == "pass --geometry to set it"
|
|
11
|
+
|
|
12
|
+
def test_single_backticks_become_plain(self):
|
|
13
|
+
assert strip_rst("run `disc list-filesystems`") == "run disc list-filesystems"
|
|
14
|
+
|
|
15
|
+
def test_text_without_markup_is_unchanged(self):
|
|
16
|
+
assert strip_rst("plain text, nothing to do") == "plain text, nothing to do"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestUsePlainHelp:
|
|
20
|
+
def _cli(self):
|
|
21
|
+
@click.group()
|
|
22
|
+
def root():
|
|
23
|
+
"""Root group with ``markup`` in its help."""
|
|
24
|
+
|
|
25
|
+
@root.command()
|
|
26
|
+
@click.option("--thing", help="A thing; see `other-command`.")
|
|
27
|
+
def sub(thing):
|
|
28
|
+
"""Do a ``thing`` to the disc."""
|
|
29
|
+
|
|
30
|
+
return root
|
|
31
|
+
|
|
32
|
+
def test_help_text_is_stripped(self):
|
|
33
|
+
root = self._cli()
|
|
34
|
+
use_plain_help(root)
|
|
35
|
+
result = CliRunner().invoke(root, ["sub", "--help"])
|
|
36
|
+
assert result.exit_code == 0
|
|
37
|
+
assert "`" not in result.output
|
|
38
|
+
# Content survives, only the markup is gone.
|
|
39
|
+
assert "Do a thing to the disc." in result.output
|
|
40
|
+
assert "see other-command." in result.output
|
|
41
|
+
|
|
42
|
+
def test_applies_to_the_whole_subtree(self):
|
|
43
|
+
root = self._cli()
|
|
44
|
+
use_plain_help(root)
|
|
45
|
+
# Both the group's own help and the subcommand's use the formatter.
|
|
46
|
+
assert root.context_class.formatter_class is PlainHelpFormatter
|
|
47
|
+
assert root.commands["sub"].context_class.formatter_class is PlainHelpFormatter
|