diff --git a/meshterm/__init__.py b/meshterm/__init__.py index 08f1831..36f3922 100644 --- a/meshterm/__init__.py +++ b/meshterm/__init__.py @@ -18,5 +18,9 @@ def copyright_notice() -> str: never drift apart. """ year = datetime.now().year - span = str(COPYRIGHT_START_YEAR) if year <= COPYRIGHT_START_YEAR else f"{COPYRIGHT_START_YEAR}-{year}" + span = ( + str(COPYRIGHT_START_YEAR) + if year <= COPYRIGHT_START_YEAR + else f"{COPYRIGHT_START_YEAR}-{year}" + ) return f"Copyright © {span} {__author__}" diff --git a/meshterm/core/device_config.py b/meshterm/core/device_config.py index 8235aae..0a37f36 100644 --- a/meshterm/core/device_config.py +++ b/meshterm/core/device_config.py @@ -300,7 +300,11 @@ def _telemetry_apply(field_name: str) -> Callable[[Device, Any, dict], Awaitable """Build an apply that updates one telemetry mode, preserving the others.""" async def apply(device: Device, value: Any, snapshot: dict) -> None: - base = value if field_name == "telemetry_mode_base" else snapshot.get("telemetry_mode_base", 0) + base = ( + value + if field_name == "telemetry_mode_base" + else snapshot.get("telemetry_mode_base", 0) + ) loc = value if field_name == "telemetry_mode_loc" else snapshot.get("telemetry_mode_loc", 0) env = value if field_name == "telemetry_mode_env" else snapshot.get("telemetry_mode_env", 0) await device.set_telemetry_modes(int(base or 0), int(loc or 0), int(env or 0)) diff --git a/meshterm/core/models.py b/meshterm/core/models.py index 8cefb0f..5c48425 100644 --- a/meshterm/core/models.py +++ b/meshterm/core/models.py @@ -625,7 +625,9 @@ class TraceResult: origin = device_label for hop in self.hops: destination = hop.node or device_label - edges.append(HopEdge(index=hop.index, origin=origin, destination=destination, snr=hop.snr)) + edges.append( + HopEdge(index=hop.index, origin=origin, destination=destination, snr=hop.snr) + ) origin = destination return edges diff --git a/meshterm/services/topology.py b/meshterm/services/topology.py index aecb49f..313d957 100644 --- a/meshterm/services/topology.py +++ b/meshterm/services/topology.py @@ -213,7 +213,8 @@ class Link: if snr is not None: span = _SNR_CEIL_DB - _SNR_FLOOR_DB frac = (snr - _SNR_FLOOR_DB) / span - quality = min(_QUALITY_MAX, max(_QUALITY_MIN, _QUALITY_MIN + frac * (_QUALITY_MAX - _QUALITY_MIN))) + reach = _QUALITY_MIN + frac * (_QUALITY_MAX - _QUALITY_MIN) + quality = min(_QUALITY_MAX, max(_QUALITY_MIN, reach)) return evidence * recency * quality @@ -718,7 +719,9 @@ class MeshTopology: out.sort(key=lambda s: (-s.strength, s.node)) return out - def scenarios(self, target: str, *, device_route: tuple[str, ...] | None = None) -> list[PathScenario]: + def scenarios( + self, target: str, *, device_route: tuple[str, ...] | None = None + ) -> list[PathScenario]: """Rank the candidate outbound routes for reaching ``target``. Three families, deduplicated in this priority order: diff --git a/meshterm/services/trace_runner.py b/meshterm/services/trace_runner.py index efdaf71..daec74b 100644 --- a/meshterm/services/trace_runner.py +++ b/meshterm/services/trace_runner.py @@ -327,8 +327,9 @@ def trace_timeout(hop_count: int) -> float: :data:`TRACE_TIMEOUT_CEILING_S`. ``hop_count`` is the number of hops in the *transmitted* path, which already includes - the mirrored return leg (see :meth:`~meshterm.core.connection.MeshCoreDevice._trace_path_to_contact`), - so it maps one-to-one onto relay transmissions and must not be doubled here. + the mirrored return leg (see + :meth:`~meshterm.core.connection.MeshCoreDevice._trace_path_to_contact`), so it maps + one-to-one onto relay transmissions and must not be doubled here. Args: hop_count: Hops in the walked path (return leg included). ``0`` (or less) means the diff --git a/meshterm/tools/channels.py b/meshterm/tools/channels.py index b78b23b..2547b77 100644 --- a/meshterm/tools/channels.py +++ b/meshterm/tools/channels.py @@ -59,7 +59,8 @@ class ChannelsTool(Tool): changes = await manage_channels(ctx) message = ( - f"[ok]✓[/ok] applied [brand]{changes}[/brand] channel change{'' if changes == 1 else 's'}" + f"[ok]✓[/ok] applied [brand]{changes}[/brand] " + f"channel change{'' if changes == 1 else 's'}" if changes else None ) diff --git a/meshterm/tools/courier.py b/meshterm/tools/courier.py index 4eafb31..9bc6ec9 100644 --- a/meshterm/tools/courier.py +++ b/meshterm/tools/courier.py @@ -233,7 +233,9 @@ class CourierTool(Tool): help=self.help, no_args_is_help=True, rich_markup_mode="rich" ) - @courier_app.command("queue", help="Queue a message for delivery when the contact is next heard") + @courier_app.command( + "queue", help="Queue a message for delivery when the contact is next heard" + ) def _queue_cmd( contact: str = typer.Argument(..., help="The recipient contact's name"), text: list[str] = typer.Argument(..., help="The message to queue"), diff --git a/meshterm/tools/trace.py b/meshterm/tools/trace.py index 17303f8..d7ddb67 100644 --- a/meshterm/tools/trace.py +++ b/meshterm/tools/trace.py @@ -285,7 +285,9 @@ class TraceTool(Tool): """ from ..cli import run_tool_command - @app.command(name=self.name, help="Run a single path trace to a target and show per-hop SNR") + @app.command( + name=self.name, help="Run a single path trace to a target and show per-hop SNR" + ) def _trace( target: str = typer.Option(..., "--target", "-t", help="Target node name/prefix"), path: str | None = typer.Option( diff --git a/meshterm/tools/tx_optimize.py b/meshterm/tools/tx_optimize.py index 9d94611..ac0e7dd 100644 --- a/meshterm/tools/tx_optimize.py +++ b/meshterm/tools/tx_optimize.py @@ -272,7 +272,9 @@ class TxOptimizeTool(Tool): if int(params.get("samples", 3)) > MAX_SAMPLES: ctx.ui.note(f"[warn]capping at {MAX_SAMPLES} traces per level[/warn]") - ctx.ui.note(f"[muted]logging in to[/muted] [brand]{admin_node.name}[/brand] [muted]…[/muted]") + ctx.ui.note( + f"[muted]logging in to[/muted] [brand]{admin_node.name}[/brand] [muted]…[/muted]" + ) await self._login(ctx, admin_node, params) ctx.ui.note( diff --git a/meshterm/ui/logo.py b/meshterm/ui/logo.py index 91b76cf..5148af9 100644 --- a/meshterm/ui/logo.py +++ b/meshterm/ui/logo.py @@ -181,7 +181,8 @@ def _state_intensity(row: str) -> str: bright = False elif part == "1": bright = True - if any(_DIM_FG.fullmatch(p) for p in parts) and not any(p in _SAYS_INTENSITY for p in parts): + dim = any(_DIM_FG.fullmatch(p) for p in parts) + if dim and not any(p in _SAYS_INTENSITY for p in parts): out.append("\x1b[" + ";".join(["1" if bright else "22"] + parts) + "m") else: out.append(found.group()) diff --git a/meshterm/ui/map_render.py b/meshterm/ui/map_render.py index 337761e..10c0dc2 100644 --- a/meshterm/ui/map_render.py +++ b/meshterm/ui/map_render.py @@ -557,7 +557,9 @@ def _draw_tile(frame: _Frame, layers: list[Layer], z: int, x: int, y: int) -> No if len(ring) >= 2: vp.draw_line(project(ring, waterway.extent), rgb, prio) if feat.name and not frame.coarse: - _add_line_label(frame, feat.rings, waterway.extent, z, x, y, feat.name, _WATER_LABEL) + _add_line_label( + frame, feat.rings, waterway.extent, z, x, y, feat.name, _WATER_LABEL + ) # Roads / rail. transportation = by_name.get("transportation") diff --git a/meshterm/ui/menu.py b/meshterm/ui/menu.py index a361c93..6037ea1 100644 --- a/meshterm/ui/menu.py +++ b/meshterm/ui/menu.py @@ -1030,8 +1030,9 @@ async def _auto_reconnect(ctx: AppContext, dialog: ReconnectDialog) -> None: The dead link is released up front (:meth:`~meshterm.context.AppContext.release_link`) so the peripheral is free to advertise while we wait for it. Each transport then waits for its - own evidence that the device is reachable before touching the radio, so an attempt is only ever made against something that is actually there. Serial - waits for the OS to re-enumerate the port the connection was opened on. Bluetooth listens + own evidence that the device is reachable before touching the radio, so an attempt is only + ever made against something that is actually there. Serial waits for the OS to re-enumerate + the port the connection was opened on. Bluetooth listens for the companion's advertisement (:func:`~meshterm.core.discovery.find_ble_device`), which is the same question asked of the only registry BLE has, and hands the resulting live handle to :meth:`~meshterm.context.AppContext.reconnect` — a power-cycled peripheral is a diff --git a/meshterm/ui/node_detail_screen.py b/meshterm/ui/node_detail_screen.py index a108017..9ddf01d 100644 --- a/meshterm/ui/node_detail_screen.py +++ b/meshterm/ui/node_detail_screen.py @@ -365,8 +365,9 @@ class NodeDetailScreen(Screen): :meth:`selected_spec`); Esc resolves :data:`CANCEL` to leave. Two axes of navigation, matching the app's spatial feel: ``Tab``/``Shift+Tab`` switches - which view fills the stage, and ``↑↓`` moves the cursor *within* the active tab — through its route list (on the Routes tab, the selection drives the - graph highlight and Enter arms a trace on the picked route) and action rows. The page + which view fills the stage, and ``↑↓`` moves the cursor *within* the active tab — through + its route list (on the Routes tab, the selection drives the graph highlight and Enter arms + a trace on the picked route) and action rows. The page itself never scrolls: the identity header, tab strip, and stage are pinned, the stage is sized to the viewport, and the route list windows itself into the leftover rows — ``PgUp/PgDn`` page the cursor through it, ``Home/End`` jump it to the ends. diff --git a/meshterm/ui/packet_viewer.py b/meshterm/ui/packet_viewer.py index 9137959..5e3699d 100644 --- a/meshterm/ui/packet_viewer.py +++ b/meshterm/ui/packet_viewer.py @@ -874,7 +874,10 @@ class PacketViewer(Screen): readings = (entry.raw or {}).get("trace_snrs") if isinstance(entry.raw, dict) else None if readings: hops = len(readings) - return f"{hops} hop{'' if hops == 1 else 's'} walked — a trace logs each leg, not its relays" + return ( + f"{hops} hop{'' if hops == 1 else 's'} walked " + "— a trace logs each leg, not its relays" + ) return "direct — no relays" def _packet_rows(self, entry: PacketEntry) -> list[tuple[str, RenderableType]]: diff --git a/meshterm/ui/path_composer.py b/meshterm/ui/path_composer.py index 85f90c7..1c37e5a 100644 --- a/meshterm/ui/path_composer.py +++ b/meshterm/ui/path_composer.py @@ -645,7 +645,10 @@ class PathComposerScreen(Screen): entries.append((i, render_to_ansi(text, width))) if not any(kind == "hop" for kind, _ in rows): if any(kind == "fetch" for kind, _ in rows): - note = "(no observed links from here — fetch the repeater's neighbours, or type a hex hash)" + note = ( + "(no observed links from here — fetch the repeater's neighbours, " + "or type a hex hash)" + ) else: note = "(no observed links from here — type a hex hash to force a hop)" # One entry per wrapped line: a multi-line string in a single entry would diff --git a/meshterm/ui/pathgraph.py b/meshterm/ui/pathgraph.py index d63c1aa..561e071 100644 --- a/meshterm/ui/pathgraph.py +++ b/meshterm/ui/pathgraph.py @@ -155,9 +155,9 @@ _GRAPH_PAD_DOTS = 6 #: the more so the shallower the curve's end tangent. Spanning the whole gap removes the corner, #: so the stroke stays an even, thin arc end to end; the nodes still seat level because the curve's #: end tangents are kept horizontal (see :data:`_BEND_K`), giving each marker a flat point to sit on -#: without a platform run. (In a very tight column a full-lane shift is unavoidably steep right up to -#: the node, so its marker sits on a gentle slope rather than dead level — the honest cost of keeping -#: the stroke thin there.) +#: without a platform run. (In a very tight column a full-lane shift is unavoidably steep right +#: up to the node, so its marker sits on a gentle slope rather than dead level — the honest cost +#: of keeping the stroke thin there.) _CURVE_SPAN = 1.0 #: The shortest horizontal run a lane change is given even for a one-lane hop, so a tight @@ -712,9 +712,9 @@ def _route( the eased level→curve→level of the shift. The shift spans the whole column gap (:data:`_CURVE_SPAN`) rather than a centred stretch flanked by flat platforms: a platform would meet the climbing curve at a corner, and that corner draws a heavy braille *knee*, so the curve - runs continuously node to node instead and the marker seats on the curve's own level end tangent. - The endpoints, sitting at the centre of the lane band, make the origin's diverging peels and - us's converging merges fall out of this one rule — no endpoint special case. + runs continuously node to node instead and the marker seats on the curve's own level end + tangent. The endpoints, sitting at the centre of the lane band, make the origin's diverging + peels and us's converging merges fall out of this one rule — no endpoint special case. ``vias`` are an edge's bypass waypoints (:func:`_bypass_vias`), threaded between the two markers in x order: the run applies the same level-or-shift grammar anchor to anchor — diff --git a/meshterm/ui/pathline.py b/meshterm/ui/pathline.py index 94a5249..81f9022 100644 --- a/meshterm/ui/pathline.py +++ b/meshterm/ui/pathline.py @@ -24,8 +24,9 @@ drawn*, so every surface the survey found can eventually route through it: (:func:`cut_mark`), and that crack is what says the segment continues; only chips crack, an arrow line still ellipsizes. A row that also carries the app-wide opens-further-prompts ``…`` hangs it *outside* the budget - (:func:`with_action_mark`) — chrome may not cost the route cells. ``auto`` (the default) picks powerline exactly - when the terminal can draw it (:func:`~meshterm.ui.termfont.powerline_enabled` — + (:func:`with_action_mark`) — chrome may not cost the route cells. ``auto`` (the default) + picks powerline exactly when the terminal can draw it + (:func:`~meshterm.ui.termfont.powerline_enabled` — a recommended font, a glyph-capable renderer, or the user's override) and falls back to arrows everywhere else, so no terminal ever sees tofu. * **Three overflow answers**, matching the three patterns the surfaces already use: @@ -606,7 +607,8 @@ class PathLine: nothing where the font has none). """ if plain: - return [self._plain_hop(hop).cell_len for hop in hops], cell_len(self._separator), 0, 0, 0 + plain_cells = [self._plain_hop(hop).cell_len for hop in hops] + return plain_cells, cell_len(self._separator), 0, 0, 0 widths = [ cell_len(hop.label) if hop.gap else self._chip(hop, self._chip_fill(hop)).cell_len @@ -916,9 +918,9 @@ class PathLine: The insertion slot outranks everything — it is the one chip that isn't a node, and it wears the ``cursor`` white so it reads as chrome among identities rather than as a hop with an unlucky hue (white sits outside the node spectrum, and the - chip ink is already picked to stay readable on it). The fade comes next, outranking identity including - our own: a dimmed hop is one nobody composed (an automatic landing back home, a - mirrored return leg), and our end must recede with the rest of that automatic + chip ink is already picked to stay readable on it). The fade comes next, outranking + identity including our own: a dimmed hop is one nobody composed (an automatic landing + back home, a mirrored return leg), and our end must recede with the rest of that automatic half rather than keep its yellow among the greys. Plain mode says the same thing by fading the name. """ diff --git a/meshterm/ui/surface.py b/meshterm/ui/surface.py index 33d337f..434f8b7 100644 --- a/meshterm/ui/surface.py +++ b/meshterm/ui/surface.py @@ -201,7 +201,7 @@ class Ui: banner: Any = None, footnote: str | None = None, ) -> str | None: - """Ask for a Bluetooth companion's pairing PIN on the startup splash; ``None`` if cancelled.""" + """Ask for a Bluetooth companion's pairing PIN on the splash; ``None`` if cancelled.""" raise NotImplementedError async def prompt_text_startup( diff --git a/meshterm/ui/timemachine_screen.py b/meshterm/ui/timemachine_screen.py index ccecc31..0127e90 100644 --- a/meshterm/ui/timemachine_screen.py +++ b/meshterm/ui/timemachine_screen.py @@ -388,7 +388,9 @@ def _node_sections( ) out.append(Text()) - out.append(_heading("Rhythm", f"receptions by local time of day · {_slice_note(slice_minutes)}")) + out.append( + _heading("Rhythm", f"receptions by local time of day · {_slice_note(slice_minutes)}") + ) out.extend( axis_chart( timeline_rows(slots, rows=_CHART_ROWS), max(slots), len(slots) // 2, @@ -1152,7 +1154,7 @@ async def _routing_prefix_bytes(ctx: AppContext) -> int: async def _contact_resolvers( ctx: AppContext, ) -> tuple[NodeResolver, Callable[[str | None], int | None], NodeResolver]: - """A ``(name, type, key)`` resolver trio over the device's contacts, best-effort like the prefix read. + """A ``(name, type, key)`` resolver trio over the contacts, best-effort like the prefix read. All three fill the picker's (and arrivals') blanks from what the companion knows but the stored observations never carried: the name resolver names a node the device holds as a diff --git a/meshterm/ui/trace_screen.py b/meshterm/ui/trace_screen.py index 5add13c..2fcb225 100644 --- a/meshterm/ui/trace_screen.py +++ b/meshterm/ui/trace_screen.py @@ -1177,7 +1177,8 @@ class TraceScreen(Screen): rows.append(spin) elif self._status: rows.append(Text(self._status, style="err")) - for number, trace in zip(range(len(self._traces), 0, -1), reversed(self._traces), strict=True): + numbering = range(len(self._traces), 0, -1) + for number, trace in zip(numbering, reversed(self._traces), strict=True): rows.append(self._trace_row(number, trace)) return Group(*rows) diff --git a/meshterm/ui/tui/prompt.py b/meshterm/ui/tui/prompt.py index 51ebe39..0adfbfe 100644 --- a/meshterm/ui/tui/prompt.py +++ b/meshterm/ui/tui/prompt.py @@ -346,7 +346,8 @@ class TextScreen(_KeylessDialog): if self._byte_limit is not None: over = self._used_bytes() - self._byte_limit if over > 0: - self._error = f"Too long by {over} byte{'s' if over != 1 else ''} — trim to send." + plural = "s" if over != 1 else "" + self._error = f"Too long by {over} byte{plural} — trim to send." return if self._validate is not None: result = self._validate(value) diff --git a/meshterm/ui/walk_screen.py b/meshterm/ui/walk_screen.py index c65dfae..fc33b89 100644 --- a/meshterm/ui/walk_screen.py +++ b/meshterm/ui/walk_screen.py @@ -29,8 +29,9 @@ only its immediate neighbourhood: * the **link list** beneath names every way onward as a selectable row, strongest observed link first — so the cursor's home, row 0, is always the strongest link out of here: type glyph, name, hash, SNR with a quality bar, the evidence behind the link - (samples, sources, age), and how many links continue onward from that node. The highlighted row's marker and label light white on the canvas. The - list scrolls *within* the screen — the canvas, legend, and heading hold still, and + (samples, sources, age), and how many links continue onward from that node. The highlighted + row's marker and label light white on the canvas. The list scrolls *within* the screen — the + canvas, legend, and heading hold still, and faint ``↑/↓ n more`` markers bracket the window — with ↑↓ moving one row and PgUp/PgDn a windowful. diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index 13e8bfa..fe5b590 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -1347,7 +1347,9 @@ def tx_opt_summary(result: TxOptResult) -> Panel: (f"{result.original_tx if result.original_tx is not None else '?'}\n", ""), ("status ", "muted"), Text.from_markup(applied), ) - return Panel(body, title="[accent]TX optimization[/accent]", border_style="accent", expand=False) + return Panel( + body, title="[accent]TX optimization[/accent]", border_style="accent", expand=False + ) # -- battery gauge -------------------------------------------------------------------------- diff --git a/scripts/xiao-radio/flash.py b/scripts/xiao-radio/flash.py index 9d81f16..74edf3c 100644 --- a/scripts/xiao-radio/flash.py +++ b/scripts/xiao-radio/flash.py @@ -109,9 +109,10 @@ def check_drive_is_ours(drive, force=False): if "6." in softdev: problems.append(f"{softdev} expects the app at 0x26000; this build is linked for S140 v7") if problems and not force: + listed = "\n - ".join(problems) sys.exit( - "ERROR: refusing to flash {} --\n - {}\n" - "Unplug the other board, or re-run with --force if you are sure.".format(drive, "\n - ".join(problems)) + f"ERROR: refusing to flash {drive} --\n - {listed}\n" + "Unplug the other board, or re-run with --force if you are sure." ) return True diff --git a/tests/test_about.py b/tests/test_about.py index 7da3dcc..ecd29f1 100644 --- a/tests/test_about.py +++ b/tests/test_about.py @@ -4,8 +4,9 @@ The pages carry no state and no controls, so what is worth pinning is what they and how they are framed: the live package facts they must never drift from, the indent block their prose hangs in, the derived footer that only names the pager when there is something to page, the landmarks their sections leave for the sticky heading -and the section jump, and the section they hang under in the main menu. How markdown itself is drawn is ``test_markdown``'s subject; width discipline on -both platforms is the gallery's (``test_gallery``). +and the section jump, and the section they hang under in the main menu. How markdown +itself is drawn is ``test_markdown``'s subject; width discipline on both platforms is +the gallery's (``test_gallery``). """ from __future__ import annotations diff --git a/tests/test_battery.py b/tests/test_battery.py index d00c2de..cda726b 100644 --- a/tests/test_battery.py +++ b/tests/test_battery.py @@ -87,7 +87,10 @@ def test_battery_cell_colours_by_band_while_the_dots_carry_the_detail() -> None: assert _glyph_style(battery_cell(7)) == "batt.low" # The band holds across a band's worth of rungs, so only the fill moves inside it. # (Below 6.25% the alarm takes the cell over, which the next test covers.) - turns = [p for p in range(8, 101) if _glyph_style(battery_cell(p)) != _glyph_style(battery_cell(p - 1))] + turns = [ + p for p in range(8, 101) + if _glyph_style(battery_cell(p)) != _glyph_style(battery_cell(p - 1)) + ] assert turns == [25, 50] assert len({battery_cell(p).plain[0] for p in range(25, 50)}) == 2 # two rungs per band @@ -115,9 +118,12 @@ def test_battery_cell_charging_sweeps_bottom_to_full_holding_the_percent() -> No def test_battery_cell_charging_keeps_the_bands_colour_while_the_fill_sweeps() -> None: """The sweep is an animation, not a reading: colour still answers for the real charge.""" - assert {_glyph_style(battery_cell(20, charging=True, frame=f)) for f in range(5)} == {"batt.low"} - assert {_glyph_style(battery_cell(40, charging=True, frame=f)) for f in range(5)} == {"batt.mid"} - assert {_glyph_style(battery_cell(90, charging=True, frame=f)) for f in range(5)} == {"batt.full"} + charging = {_glyph_style(battery_cell(20, charging=True, frame=f)) for f in range(5)} + assert charging == {"batt.low"} + charging = {_glyph_style(battery_cell(40, charging=True, frame=f)) for f in range(5)} + assert charging == {"batt.mid"} + charging = {_glyph_style(battery_cell(90, charging=True, frame=f)) for f in range(5)} + assert charging == {"batt.full"} def test_battery_cell_charging_never_alarms_however_empty_the_pack_is() -> None: diff --git a/tests/test_channels.py b/tests/test_channels.py index c8b6a36..e88f817 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -193,7 +193,8 @@ def test_decrypt_channel_text_rejects_a_fingerprint_collision() -> None: def test_decrypt_channel_text_rejects_malformed_hex() -> None: """Garbage hex fields fail closed instead of raising.""" assert decrypt_channel_text("zz", "zzzz", "zzzzzzzzzzzzzzzz", [("x", random_secret())]) is None - assert decrypt_channel_text("00", "0000", "00" * 15, [("x", random_secret())]) is None # not block-aligned + # a 15-byte body is not block-aligned + assert decrypt_channel_text("00", "0000", "00" * 15, [("x", random_secret())]) is None # -- QR rendering ------------------------------------------------------------- @@ -374,10 +375,16 @@ class _ScriptedUi: async def select(self, title: str, items: list, *, default=None): # noqa: ANN001, ANN201 return self._selects.pop(0) if self._selects else "__back__" - async def text(self, title: str, *, default: str = "", validate=None, help_text: str = "", password: bool = False): # noqa: ANN001, ANN201 + async def text( # noqa: ANN001, ANN201 + self, title: str, *, default: str = "", validate=None, + help_text: str = "", password: bool = False, + ): return self._texts.pop(0) - async def dialog(self, prompt, buttons, *, title: str = "", default: int = 0, keys=None, danger: bool = False, destructive: bool = False): # noqa: ANN001, ANN201 + async def dialog( # noqa: ANN001, ANN201 + self, prompt, buttons, *, title: str = "", default: int = 0, keys=None, + danger: bool = False, destructive: bool = False, + ): return self._dialogs.pop(0) diff --git a/tests/test_chat.py b/tests/test_chat.py index 6fb034f..78f0493 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -484,7 +484,9 @@ def test_title_preview_column_aligns_regardless_of_label_length(repo: Repository """The preview starts at the same column whether the label is short or (clipped) long.""" ctx = _RowCtx(repo) short = Conversation(label="A", is_channel=True, channel_idx=0, channel_id="c0") - long = Conversation(label="A much longer channel name here", is_channel=True, channel_idx=1, channel_id="c1") + long = Conversation( + label="A much longer channel name here", is_channel=True, channel_idx=1, channel_id="c1" + ) m0 = ChatMessage(text="X: hello", is_channel=True, channel_id="c0") m1 = ChatMessage(text="Y: hello", is_channel=True, channel_id="c1") l0 = _title(ctx, short, {"chan:c0": m0}, _NO_KEYS).plain @@ -493,7 +495,7 @@ def test_title_preview_column_aligns_regardless_of_label_length(repo: Repository def test_title_leads_with_openness_glyph(repo: Repository) -> None: - """Channel rows lead with an openness glyph: # name-derived, 🌐 fixed-key public, 🔒 private.""" + """Channel rows lead with an openness glyph: # name-derived, 🌐 public, 🔒 private.""" ctx = _RowCtx(repo) def head(conv): return _title(ctx, conv, {}, _NO_KEYS).plain.split(" ", 1)[0] @@ -1140,7 +1142,9 @@ def test_channel_transcript_groups_by_sender() -> None: ChatMessage(text="Alice: hi", is_channel=True, channel_idx=0, created_at=at(0)), ChatMessage(text="Alice: again", is_channel=True, channel_idx=0, created_at=at(6)), ChatMessage(text="Bob: yo", is_channel=True, channel_idx=0, created_at=at(8)), - ChatMessage(text="hello all", outbound=True, is_channel=True, channel_idx=0, created_at=at(9)), + ChatMessage( + text="hello all", outbound=True, is_channel=True, channel_idx=0, created_at=at(9) + ), ] screen = ChatScreen(conv, messages, send=None, names={}, session=_StubSession()) rendered = _strip_ansi("\n".join(screen._render_grouped(80))) diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index bf69d68..36b4679 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -296,7 +296,8 @@ def test_recent_observations_rehydrate_the_packet_payload_class(tmp_path: Path) ) repo.record_observation(run, _obs(node="a1b2", kind="advert")) # a plain advert nearby - window = {o.node or "": o for o in repo.recent_observations(since=utcnow() - timedelta(hours=2))} + recent = repo.recent_observations(since=utcnow() - timedelta(hours=2)) + window = {o.node or "": o for o in recent} assert window[""].raw is not None and window[""].raw["payload_typename"] == "TRACE" assert window["a1b2"].raw is None # an advert keeps no packet raw, as before repo.close() diff --git a/tests/test_device_config.py b/tests/test_device_config.py index 8d7912a..585fdb7 100644 --- a/tests/test_device_config.py +++ b/tests/test_device_config.py @@ -115,7 +115,8 @@ def _contacts_names(counts, sort: str, prefix_bytes: int = 3): Contact(name="Alice", public_key="3d63c6" + "00" * 29, key_prefix="3d63c6"), ] group = contacts_table( - "Homestead", "aabbcc" + "00" * 29, contacts, prefix_bytes, counts, ContactsSort.from_name(sort) + "Homestead", "aabbcc" + "00" * 29, contacts, prefix_bytes, counts, + ContactsSort.from_name(sort), ) table = group.renderables[0] # (table, blank line, legend) names = [c.plain if hasattr(c, "plain") else str(c) for c in table.columns[1].cells] diff --git a/tests/test_device_state.py b/tests/test_device_state.py index e21ffc3..e32e701 100644 --- a/tests/test_device_state.py +++ b/tests/test_device_state.py @@ -33,7 +33,8 @@ class FakeDevice: self.contacts_calls += 1 if self.contact_rows is not None: return list(self.contact_rows) - return [Contact(name=f"contact-{self.contacts_calls}")] # fresh identity per fetch, to spot refreshes + # a fresh identity per fetch, so a re-read is visible + return [Contact(name=f"contact-{self.contacts_calls}")] async def get_self_info(self) -> dict: self.self_info_calls += 1 diff --git a/tests/test_emoji_width.py b/tests/test_emoji_width.py index 2561d63..74e28ae 100644 --- a/tests/test_emoji_width.py +++ b/tests/test_emoji_width.py @@ -130,7 +130,8 @@ def test_bare_text_default_emoji_are_left_exactly_as_measured() -> None: for bare in (_ROAD, _WEB): assert cell_len(bare) == 1 and cache[bare] == 1 # A heading built around one costs its own cells and no more, so the row frames flush. - assert cell_len(f"── {_ROAD} Longest distance ──") == len("── ") + 1 + len(" Longest distance ──") + caption = cell_len(f"── {_ROAD} Longest distance ──") + assert caption == len("── ") + 1 + len(" Longest distance ──") def test_flags_measure_two_cells_in_both_authorities() -> None: diff --git a/tests/test_gallery.py b/tests/test_gallery.py index e251a59..611200c 100644 --- a/tests/test_gallery.py +++ b/tests/test_gallery.py @@ -302,7 +302,9 @@ def _chat(cols: int, rows: int) -> Screen: ) messages = [ ChatMessage(text="on my way, should be there soon", outbound=False, peer="d4e5f6a7"), - ChatMessage(text="sounds good, see you shortly", outbound=True, peer="d4e5f6a7", acked=True), + ChatMessage( + text="sounds good, see you shortly", outbound=True, peer="d4e5f6a7", acked=True + ), ] return ChatScreen( conv, messages, send=None, names={"d4e5f6a7": "Alice"}, session=_GallerySession(cols, rows), diff --git a/tests/test_map.py b/tests/test_map.py index ec614a6..53ce051 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -112,7 +112,8 @@ def test_decode_tile_filter_skips_the_layers_the_map_never_draws() -> None: """The layers left undecoded are the ones that made a tile expensive.""" from meshterm.ui.map_render import DRAWN_LAYERS - narrow = {layer.name: layer for layer in decode_tile(_FIXTURE.read_bytes(), layers=DRAWN_LAYERS)} + drawn = decode_tile(_FIXTURE.read_bytes(), layers=DRAWN_LAYERS) + narrow = {layer.name: layer for layer in drawn} for skipped in ("housenumber", "poi", "mountain_peak"): assert skipped in narrow, f"{skipped} should still be named" assert not narrow[skipped].features, f"{skipped} should not have been decoded" @@ -905,7 +906,10 @@ def test_decoded_layers_survive_a_round_trip() -> None: restored = loads_layers(dumps_layers(original, stamp="x"), stamp="x") assert restored is not None - assert [(layer.name, layer.extent) for layer in restored] == [(layer.name, layer.extent) for layer in original] + def shape(layers): + return [(layer.name, layer.extent) for layer in layers] + + assert shape(restored) == shape(original) for before, after in zip(original, restored, strict=True): assert after.features == before.features @@ -2282,7 +2286,10 @@ def test_map_paints_without_waiting_for_the_ground(monkeypatch) -> None: # noqa calls: list[tuple] = [] real = ms.render_map - monkeypatch.setattr(ms, "render_map", lambda vp, tiles, m, **k: calls.append(tiles) or real(vp, tiles, m, **k)) + monkeypatch.setattr( + ms, "render_map", + lambda vp, tiles, m, **k: calls.append(tiles) or real(vp, tiles, m, **k), + ) started: list = [] screen = _async_map(monkeypatch, started) diff --git a/tests/test_message_paths.py b/tests/test_message_paths.py index a644a86..8778957 100644 --- a/tests/test_message_paths.py +++ b/tests/test_message_paths.py @@ -60,9 +60,17 @@ def test_channel_arrivals_match_by_decrypted_content(tmp_path: Path) -> None: repo, run = _repo(tmp_path) now = utcnow() wire = "Alice: hi mesh" - _record_frame(repo, run, when=now - timedelta(seconds=5), path="3d63", raw=_grp_txt_raw(SECRET, wire)) - _record_frame(repo, run, when=now - timedelta(seconds=3), path="3d63,a1b2", raw=_grp_txt_raw(SECRET, wire)) - _record_frame(repo, run, when=now - timedelta(seconds=1), path="", raw=_grp_txt_raw(SECRET, "Alice: other")) + _record_frame( + repo, run, when=now - timedelta(seconds=5), path="3d63", raw=_grp_txt_raw(SECRET, wire) + ) + _record_frame( + repo, run, when=now - timedelta(seconds=3), path="3d63,a1b2", + raw=_grp_txt_raw(SECRET, wire), + ) + _record_frame( + repo, run, when=now - timedelta(seconds=1), path="", + raw=_grp_txt_raw(SECRET, "Alice: other"), + ) message = ChatMessage(text=wire, is_channel=True, created_at=now) arrivals = channel_arrivals(repo, message, channel_name="#general", secret=SECRET) diff --git a/tests/test_node_detail.py b/tests/test_node_detail.py index b7122f5..d5ca81d 100644 --- a/tests/test_node_detail.py +++ b/tests/test_node_detail.py @@ -247,7 +247,7 @@ def test_route_line_hash_width_follows_our_path_hash_mode() -> None: def test_route_line_marks_the_best_route_and_its_context() -> None: - """The winner wears ★ best and trails its bottleneck SNR and sample count, on the context line.""" + """The winner wears ★ best and trails its bottleneck SNR and sample count on the context row.""" _path, context = _route_line( "Far", FAR.public_key, ("3d63c6429436",), "best", 6.5, 4, resolve=make_node_resolver([HUB]), node_known=True, @@ -301,7 +301,7 @@ def _view(topo, suggested, device_route, target, node_label, contacts, name_key= def test_routes_view_draws_evidence_and_notes_its_absence() -> None: - """With route evidence the view carries selectable routes; without any, a muted note stands in.""" + """With route evidence the view carries selectable routes; without, a muted note stands in.""" topo = _topo_with_route() view = _view(topo, topo.suggested("f2c24f54551e"), ("3d63c6429436",), "f2c24f54551e", "Far", [HUB, FAR]) @@ -322,7 +322,7 @@ def test_routes_view_draws_evidence_and_notes_its_absence() -> None: def test_routes_view_puts_the_contact_on_the_left_and_us_on_the_right() -> None: - """The graph reads node → us (the inbound direction): the contact on the left, our star right.""" + """The graph reads node → us (the inbound direction): the contact left, our star right.""" topo = _topo_with_route() view = _view(topo, topo.suggested("f2c24f54551e"), ("3d63c6429436",), "f2c24f54551e", "Far", [HUB, FAR]) @@ -422,7 +422,7 @@ def test_good_alternatives_keeps_observed_routes_and_drops_outliers() -> None: def test_route_freshness_drops_a_route_with_a_long_quiet_hop() -> None: - """A route counts as stale — and is dropped — once its stalest hop goes quiet past the horizon.""" + """A route counts as stale — and is dropped — once its stalest hop falls past the horizon.""" from datetime import timedelta from meshterm.persistence.repository import TracedPath @@ -687,8 +687,14 @@ def test_node_detail_routes_stage_spends_the_caption_and_top_air_on_the_fan() -> screen.handle("tab") # onto Routes screen.note_viewport(viewport) lines = screen.render_body(49) - strip_idx = next(i for i, ln in enumerate(lines) if "Routes" in _plain([ln]) and "│" in _plain([ln])) - rule_idx = next(i for i, ln in enumerate(lines) if set(_plain([ln])) == {"─"} and i > strip_idx) + def is_strip(ln): + return "Routes" in _plain([ln]) and "│" in _plain([ln]) + + strip_idx = next(i for i, ln in enumerate(lines) if is_strip(ln)) + rule_idx = next( + i for i, ln in enumerate(lines) + if set(_plain([ln])) == {"─"} and i > strip_idx + ) return [_plain([ln]) for ln in lines[strip_idx + 2 : rule_idx]] # past the strip's own rule set_platform(PICOCALC) @@ -976,7 +982,10 @@ def test_route_labels_light_through_a_coalesced_hop() -> None: routes = _RoutesView( routes=[ - _Route(draw=("bf61f2fb1d9e", "3d63c6429436"), spec="s0", path=Text("wide"), context=Text("")), + _Route( + draw=("bf61f2fb1d9e", "3d63c6429436"), spec="s0", + path=Text("wide"), context=Text(""), + ), _Route(draw=("bf61f2fb1d9e", "3d"), spec="s1", path=Text("short"), context=Text("")), ], glyph_of=lambda n: ("●", "#ffffff"), @@ -996,7 +1005,10 @@ def test_node_detail_route_list_windows_inside_the_page() -> None: """ routes = _RoutesView( routes=[ - _Route(draw=(f"{i:x}{i:x}" * 6,), spec=f"s{i}", path=Text(f"route {i}"), context=Text("")) + _Route( + draw=(f"{i:x}{i:x}" * 6,), spec=f"s{i}", + path=Text(f"route {i}"), context=Text(""), + ) for i in range(12) ], glyph_of=lambda n: ("●", "#ffffff"), @@ -1026,7 +1038,10 @@ def test_node_detail_route_cursor_clamps_at_both_ends() -> None: """ routes = _RoutesView( routes=[ - _Route(draw=(f"{i:x}{i:x}" * 6,), spec=f"s{i}", path=Text(f"route {i}"), context=Text("")) + _Route( + draw=(f"{i:x}{i:x}" * 6,), spec=f"s{i}", + path=Text(f"route {i}"), context=Text(""), + ) for i in range(12) ], glyph_of=lambda n: ("●", "#ffffff"), diff --git a/tests/test_pathgraph.py b/tests/test_pathgraph.py index e4f3654..d0138ac 100644 --- a/tests/test_pathgraph.py +++ b/tests/test_pathgraph.py @@ -313,8 +313,9 @@ def test_a_shared_relay_draws_a_single_marker() -> None: def _marker_rows(lines, glyph): # noqa: ANN001, ANN202 - """The distinct canvas row indices a marker glyph is drawn on (endpoints ``★``, relays ``●``).""" - return sorted({i for i, ln in enumerate(_ANSI.sub("", "\n".join(lines)).splitlines()) if glyph in ln}) + """The distinct canvas rows a marker glyph is drawn on (endpoints ``★``, relays ``●``).""" + plain = _ANSI.sub("", "\n".join(lines)).splitlines() + return sorted({i for i, ln in enumerate(plain) if glyph in ln}) def test_even_lane_count_centres_the_endpoints() -> None: diff --git a/tests/test_timemachine.py b/tests/test_timemachine.py index 7288d62..b7b63be 100644 --- a/tests/test_timemachine.py +++ b/tests/test_timemachine.py @@ -143,12 +143,21 @@ def test_hourly_series_groups_by_clock_hour(tmp_path: Path) -> None: base = now.replace(minute=0, second=0, microsecond=0) # top of the current UTC hour three_h = base - timedelta(hours=3) # top of a busy hour, three back for node in ("aa" * 6, "bb" * 6): - repo.record_observation(run, Observation(node=node, observed_at=three_h + timedelta(minutes=5))) - repo.record_observation(run, Observation(node="aa" * 6, observed_at=three_h + timedelta(minutes=20))) + repo.record_observation( + run, Observation(node=node, observed_at=three_h + timedelta(minutes=5)) + ) repo.record_observation( - run, Observation(node="cc" * 6, kind="packet", path="", observed_at=three_h + timedelta(minutes=40)) + run, Observation(node="aa" * 6, observed_at=three_h + timedelta(minutes=20)) + ) + repo.record_observation( + run, + Observation( + node="cc" * 6, kind="packet", path="", observed_at=three_h + timedelta(minutes=40) + ), + ) + repo.record_observation( + run, Observation(node="aa" * 6, observed_at=base + timedelta(minutes=1)) ) - repo.record_observation(run, Observation(node="aa" * 6, observed_at=base + timedelta(minutes=1))) series = repo.hourly_series(now - timedelta(days=1)) by_hour = {iso: (pkts, nodes) for iso, pkts, nodes in series} # Keys are local wall-clock hours, so form the expected keys via the same rotation. diff --git a/tests/test_walk.py b/tests/test_walk.py index 64d4c4f..a12b2e2 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -168,8 +168,9 @@ def test_walk_selected_link_lights_the_route_that_reaches_it() -> None: topo = MeshTopology(US, contacts=[hub, alice, bob]) y = topo.canonical(hub.public_key) topo.add_walk([topo.self_id, y], snrs=[10.0], when=stale, source="trace") # approach: green - topo.add_walk([y, topo.canonical(alice.public_key)], snrs=[0.0], when=stale, source="trace") # amber - topo.add_walk([y, topo.canonical(bob.public_key)], snrs=[-15.0], when=stale, source="trace") # red + # amber, then red + topo.add_walk([y, topo.canonical(alice.public_key)], snrs=[0.0], when=stale, source="trace") + topo.add_walk([y, topo.canonical(bob.public_key)], snrs=[-15.0], when=stale, source="trace") screen = WalkScreen( session=_FakeSession(), topo=topo,