Fix three small CLI rough edges: records --width, the simulator's label, two help pages
records --width now refuses anything but 1, 2, or 4 with a usage error, the way --category already refused an unknown discipline. A width that doesn't exist used to come back as exit 5, indistinguishable from an empty database, and --width 0 was falsy and so listed every width. (#15) A device with no port is labelled by its name alone. The simulator fell through to the serial branch and came out as "… (nothing transmits) ()". (#14) platform and specimen pass their own one-line help, so Typer stops printing their docstrings — notes for whoever reads cli.py, Sphinx roles and source paths included — and their summaries fit in meshterm --help. A new test runs --help on every registered command and refuses those markers anywhere. (#16) Built from the issues alone; the pull requests filed against them were closed unreviewed and their code was never read. Closes #14, closes #15, closes #16. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1949d5b1a3
commit
0cd40bbd6c
6 files changed
+103
-5
No files matched your search
@@ -9,6 +9,18 @@ one caveat SemVer makes for a leading zero: while the major version is still `0`
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`meshterm records --width` refuses a width that doesn't exist.** Hash widths are 1, 2,
|
||||
or 4. Any other number used to look like an empty result, so a typo and an empty
|
||||
database gave the same answer, and `--width 0` was ignored and listed every width. Now
|
||||
it's a usage error that names the three choices, like `--category` already was.
|
||||
- **The simulator's row in `meshterm --mock devices` no longer ends in an empty `()`.**
|
||||
The brackets hold a serial port, and the simulator doesn't have one.
|
||||
- **`meshterm platform --help` and `meshterm specimen --help` read like the others.** They
|
||||
used to print notes meant for someone reading the source code, markup and all, and cut
|
||||
their summaries short in `meshterm --help`. Now each has one plain line.
|
||||
|
||||
## [0.10.0] — 2026-09-25
|
||||
|
||||
**Regions, a uConsole with no bridge, and Bluetooth that works on Linux.** MeshTerm now
|
||||
|
||||
+8
-2
@@ -603,7 +603,10 @@ def _report_no_windows_console(console: Console) -> None:
|
||||
console.print("try [accent]meshterm --help[/accent].")
|
||||
|
||||
|
||||
@app.command(name="platform")
|
||||
@app.command(
|
||||
name="platform",
|
||||
help="Show which platform MeshTerm runs as, and why",
|
||||
)
|
||||
def platform_command() -> None:
|
||||
"""Print the resolved platform and every input the resolution looked at.
|
||||
|
||||
@@ -650,7 +653,10 @@ def platform_command() -> None:
|
||||
)
|
||||
|
||||
|
||||
@app.command(name="specimen")
|
||||
@app.command(
|
||||
name="specimen",
|
||||
help="Print every mark, icon, and colour MeshTerm draws",
|
||||
)
|
||||
def specimen_command() -> None:
|
||||
"""Print the visual-language specimen for the active platform.
|
||||
|
||||
|
||||
@@ -273,6 +273,8 @@ class DiscoveredDevice:
|
||||
if self.is_spi:
|
||||
return f"{self.name or 'SPI radio'} ({self.port})"
|
||||
name = self.product or self.description or self.vendor_label or "Serial device"
|
||||
if not self.port: # the simulator has nothing to point at, so no "()"
|
||||
return name
|
||||
suffix = f"({self.port})"
|
||||
if name.endswith(suffix): # avoid "… (COM11) (COM11)"
|
||||
name = name[: -len(suffix)].rstrip()
|
||||
|
||||
@@ -26,6 +26,9 @@ from ..services.records import CATEGORIES, CATEGORY_BY_ID, Category
|
||||
from ..ui.script import NONE
|
||||
from .base import Tool, ToolResult, register
|
||||
|
||||
#: The hash widths a record can be set at: MeshCore's path hashes are 1, 2 or 4 bytes.
|
||||
WIDTHS = (1, 2, 4)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - typing only
|
||||
from ..persistence.repository import DiscoveredPath
|
||||
from ..ui.fields import NodeRef
|
||||
@@ -183,7 +186,13 @@ class TrophyCaseTool(Tool):
|
||||
f"--category must be one of: {choices} (got {category!r})"
|
||||
)
|
||||
tool_params["category"] = category
|
||||
if width:
|
||||
if width is not None:
|
||||
# The same closed set as --category, for the same reason: `--width 3`
|
||||
# found nothing and exited 5, indistinguishable from an empty database,
|
||||
# and `--width 0` was falsy and so quietly listed every width.
|
||||
if width not in WIDTHS:
|
||||
choices = ", ".join(map(str, WIDTHS))
|
||||
raise typer.BadParameter(f"--width must be one of: {choices} (got {width})")
|
||||
tool_params["width"] = width
|
||||
run_tool_command(self, tool_params)
|
||||
|
||||
|
||||
@@ -566,8 +566,19 @@ def test_a_destructive_command_without_yes_is_a_silent_usage_error(run, args) ->
|
||||
[
|
||||
(("contacts", "--sort", "bogus"), "--sort"),
|
||||
(("records", "--category", "long_hual"), "--category"),
|
||||
(("records", "--width", "3"), "--width"),
|
||||
(("records", "--width", "0"), "--width"),
|
||||
(("records", "--width", "-1"), "--width"),
|
||||
(("records", "--json", "--width", "3"), "--width"),
|
||||
],
|
||||
ids=[
|
||||
"contacts-sort",
|
||||
"records-category",
|
||||
"records-width-3",
|
||||
"records-width-0",
|
||||
"records-width-negative",
|
||||
"records-width-3-json",
|
||||
],
|
||||
ids=["contacts-sort", "records-category"],
|
||||
)
|
||||
def test_a_value_outside_a_closed_set_is_refused_rather_than_ignored(run, args, flag) -> None: # noqa: ANN001
|
||||
"""A typo in a closed-set option used to be answered with a different question.
|
||||
@@ -754,6 +765,49 @@ def test_a_closing_message_goes_to_stderr_too(run) -> None: # noqa: ANN001
|
||||
assert "records stored" in result.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("width", ["1", "2", "4"])
|
||||
def test_every_real_hash_width_is_accepted(run, width) -> None: # noqa: ANN001
|
||||
"""Refusing the widths that don't exist must not refuse the three that do."""
|
||||
result = run("records", "--width", width)
|
||||
assert result.exit_code == exitcodes.NO_RESULT, result.stderr
|
||||
assert "records stored" in result.stderr
|
||||
|
||||
|
||||
def test_the_simulator_is_listed_without_empty_brackets(run) -> None: # noqa: ANN001
|
||||
"""``--mock devices`` named the simulator ``… (nothing transmits) ()``: a port it lacks."""
|
||||
result = run("devices")
|
||||
assert result.exit_code == exitcodes.OK, result.stderr
|
||||
assert "the built-in simulator (nothing transmits)" in result.stdout
|
||||
assert "()" not in result.stdout
|
||||
|
||||
|
||||
#: What a docstring written for the source leaks into ``--help`` when Typer is left to
|
||||
#: use it: a Sphinx role, reST's double backticks, or a path into the package.
|
||||
_SOURCE_NOTES = re.compile(r":(func|mod|class|meth|attr|data):`|``|meshterm/[\w/]+\.py")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("leaf", _leaf_commands(), ids=lambda leaf: "-".join(leaf))
|
||||
def test_every_command_help_is_written_for_the_terminal(run, leaf) -> None: # noqa: ANN001
|
||||
"""A help page is for someone at a prompt asking what a command does.
|
||||
|
||||
``platform`` and ``specimen`` printed their whole docstrings — notes for whoever reads
|
||||
``cli.py``, Sphinx roles and source paths included — and the same markup cut their
|
||||
one-line summaries short in ``meshterm --help``.
|
||||
"""
|
||||
result = run(*leaf, "--help")
|
||||
assert result.exit_code == exitcodes.OK, result.output
|
||||
assert not _SOURCE_NOTES.search(result.output), result.output
|
||||
|
||||
|
||||
def test_the_command_list_has_no_source_notes(run) -> None: # noqa: ANN001
|
||||
"""The one-line summaries in ``meshterm --help`` come from the same help text."""
|
||||
result = run("--help")
|
||||
assert not _SOURCE_NOTES.search(result.output), result.output
|
||||
for line in result.output.splitlines():
|
||||
if line.split()[:1] in (["platform"], ["specimen"]):
|
||||
assert not line.rstrip().endswith("..."), f"summary cut short: {line!r}"
|
||||
|
||||
|
||||
# -- the simulator's own history ------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
+16
-1
@@ -14,7 +14,7 @@ import pytest
|
||||
|
||||
from meshterm.core.config import DeviceProfile
|
||||
from meshterm.core.device_store import DeviceStore
|
||||
from meshterm.core.discovery import DiscoveredDevice, discover_devices
|
||||
from meshterm.core.discovery import TRANSPORT_MOCK, DiscoveredDevice, discover_devices
|
||||
from meshterm.core.selection import DeviceSelectionError, resolve_device
|
||||
|
||||
|
||||
@@ -112,6 +112,21 @@ def test_label_does_not_repeat_the_port() -> None:
|
||||
assert DiscoveredDevice("COM5", product="Wio SX1262").label == "Wio SX1262 (COM5)"
|
||||
|
||||
|
||||
def test_a_device_with_no_port_gets_no_empty_brackets() -> None:
|
||||
"""The simulator has nothing to point at, so its label ends at its name, not in "()".
|
||||
|
||||
The serial branch is also where every transport without one of its own lands, and it
|
||||
appended ``({port})`` whether or not there was a port.
|
||||
"""
|
||||
sim = DiscoveredDevice(
|
||||
transport=TRANSPORT_MOCK,
|
||||
description="the built-in simulator (nothing transmits)",
|
||||
name="simulator",
|
||||
)
|
||||
assert sim.label == "the built-in simulator (nothing transmits)"
|
||||
assert DiscoveredDevice(product="Wio SX1262").label == "Wio SX1262"
|
||||
|
||||
|
||||
def test_stable_id_precedence() -> None:
|
||||
"""stable_id prefers serial number, then vid:pid, then the port name."""
|
||||
assert DiscoveredDevice("COM5", serial_number="SN1", vid=1, pid=2).stable_id == "sn:SN1"
|
||||
|
||||
Reference in new issue
Block a user