From 61b6387269233bd89e0db076fc7e58aa7b2f8599 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 6 Sep 2026 20:12:07 -0400 Subject: [PATCH] Every zip says whether its sides are the same length MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `B905` over 56 sites, and the answer differs by idiom rather than by file. 21 were the sliding window `zip(xs, xs[1:])` — ragged by exactly one, on purpose, so `strict=` is the wrong question entirely. Those become `itertools.pairwise`, which is what they always meant and needs no flag: the walk's SNR stops, the heat ladder, the route chains in `topology`/`records`, every polyline in `mapcanvas`, and the graph's edge sets in `pathgraph`. The other 35 pair sequences that must line up, so they now say `strict=True` and a mismatch raises instead of silently truncating the longer side. Running them under that assertion is what sorted the two groups: the first pass put `strict=True` everywhere and 208 tests failed, all of them a sliding window. One goes the other way. `mvt._decode_feature` reads tags off a network tile, where an odd-length list means the tile is malformed — and this decoder answers that with a shrug everywhere else (`if not geom_ints: return None`, bounds- checked key and value indices). It keeps `strict=False`, now with a comment saying why, so a bad basemap tile still can't crash the map. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/add-narrow-emoji/classify.py | 2 +- .../profile-picocalc/scripts/bench_ablate.py | 4 ++-- meshterm/core/contact_score.py | 2 +- meshterm/core/mvt.py | 4 +++- meshterm/services/records.py | 5 +++-- meshterm/services/topology.py | 6 +++--- meshterm/ui/braillechart.py | 4 ++-- meshterm/ui/map_screen.py | 2 +- meshterm/ui/mapcanvas.py | 5 +++-- meshterm/ui/menu.py | 2 +- meshterm/ui/menus.py | 2 +- meshterm/ui/node_detail_screen.py | 3 ++- meshterm/ui/pathgraph.py | 18 +++++++++--------- meshterm/ui/purge_screen.py | 2 +- meshterm/ui/records_screen.py | 4 ++-- meshterm/ui/timemachine_screen.py | 8 ++++---- meshterm/ui/trace_screen.py | 2 +- meshterm/ui/tui/frame.py | 2 +- meshterm/ui/tui/session.py | 6 +++--- meshterm/ui/walk_screen.py | 7 ++++--- meshterm/ui/widgets.py | 5 +++-- tests/test_chat.py | 3 ++- tests/test_contact_score.py | 2 +- tests/test_map.py | 6 +++--- tests/test_menus.py | 8 ++++---- tests/test_pathgraph.py | 13 +++++++------ tests/test_pathline.py | 2 +- tests/test_timemachine.py | 3 ++- tests/test_tui.py | 4 ++-- tests/test_walk.py | 2 +- tests/test_watchtower.py | 9 ++++++--- 31 files changed, 80 insertions(+), 67 deletions(-) diff --git a/.claude/skills/add-narrow-emoji/classify.py b/.claude/skills/add-narrow-emoji/classify.py index 632be1a..ccbeccf 100644 --- a/.claude/skills/add-narrow-emoji/classify.py +++ b/.claude/skills/add-narrow-emoji/classify.py @@ -1,4 +1,4 @@ -"""Diagnose one emoji for the MeshTerm terminal-aligned width machinery. +r"""Diagnose one emoji for the MeshTerm terminal-aligned width machinery. Given an emoji on the command line, report its codepoints and how the three width authorities measure it (wcwidth, Rich, prompt_toolkit), then classify it and print the diff --git a/.claude/skills/profile-picocalc/scripts/bench_ablate.py b/.claude/skills/profile-picocalc/scripts/bench_ablate.py index 252f060..179fd25 100644 --- a/.claude/skills/profile-picocalc/scripts/bench_ablate.py +++ b/.claude/skills/profile-picocalc/scripts/bench_ablate.py @@ -131,9 +131,9 @@ def measure(name, cols, rows, reps): # How different are the two frames, in rows? rows_a, rows_b = f_a.split("\n"), f_b.split("\n") - changed = sum(1 for x, y in zip(rows_a, rows_b) if x != y) + changed = sum(1 for x, y in zip(rows_a, rows_b, strict=True) if x != y) rows_far = f_far.split("\n") - changed_far = sum(1 for x, y in zip(rows_a, rows_far) if x != y) + changed_far = sum(1 for x, y in zip(rows_a, rows_far, strict=True) if x != y) return { "compose": compose_ms, "parse": parse_ms, diff --git a/meshterm/core/contact_score.py b/meshterm/core/contact_score.py index 9dbe277..cc2c173 100644 --- a/meshterm/core/contact_score.py +++ b/meshterm/core/contact_score.py @@ -576,7 +576,7 @@ def rank_contacts( reasons=_reasons(sig, terms, weights), ) for contact, sig, score, terms in zip( - contacts, gathered, scores, per_contact_terms + contacts, gathered, scores, per_contact_terms, strict=True ) ] ranked.sort(key=lambda scored: (-scored.score, scored.contact.name.casefold())) diff --git a/meshterm/core/mvt.py b/meshterm/core/mvt.py index c549df6..1cde365 100644 --- a/meshterm/core/mvt.py +++ b/meshterm/core/mvt.py @@ -234,7 +234,9 @@ def _decode_feature(buf: bytes, keys: list[str], values: list[Any]) -> Feature | if not geom_ints: return None tags: dict[str, Any] = {} - for k, v in zip(tag_ints[0::2], tag_ints[1::2]): + # A malformed tile may carry an odd tag list; drop the dangling key rather than + # raise — the rest of this decoder is deliberately lenient about bad tiles. + for k, v in zip(tag_ints[0::2], tag_ints[1::2], strict=False): if 0 <= k < len(keys) and 0 <= v < len(values): tags[keys[k]] = values[v] return Feature(geom_type=geom_type, rings=_decode_geometry(geom_ints), tags=tags) diff --git a/meshterm/services/records.py b/meshterm/services/records.py index b4c4381..f0934e6 100644 --- a/meshterm/services/records.py +++ b/meshterm/services/records.py @@ -34,6 +34,7 @@ from __future__ import annotations import math from collections.abc import Callable, Sequence from dataclasses import dataclass +from itertools import pairwise from ..core.geo import haversine_km from ..core.models import Hop, TraceResult @@ -69,7 +70,7 @@ def first_repeated_edge(nodes: Sequence[str]) -> tuple[str, str] | None: """ walked = [n.strip().lower() for n in nodes if n and n.strip()] seen: set[tuple[str, str]] = set() - for pair in zip(walked, walked[1:]): + for pair in pairwise(walked): if pair in seen: return pair seen.add(pair) @@ -306,7 +307,7 @@ def compute_walk_stats( complete = True leg_km: float | None = None leg_link: tuple[str | None, str | None] | None = None - for i, (a, b) in enumerate(zip(points, points[1:])): + for i, (a, b) in enumerate(pairwise(points)): if a is None or b is None: complete = False continue diff --git a/meshterm/services/topology.py b/meshterm/services/topology.py index dd35c12..aecb49f 100644 --- a/meshterm/services/topology.py +++ b/meshterm/services/topology.py @@ -35,7 +35,7 @@ import math import statistics from dataclasses import dataclass, field from datetime import datetime -from itertools import chain +from itertools import chain, pairwise from ..core.models import Contact, utcnow from ..persistence.repository import NeighbourLink, PacketPath, TracedPath @@ -823,7 +823,7 @@ class MeshTopology: weakest_strength: float | None = None weakest_snr: float | None = None samples = 0 - for a, b in zip(chain, chain[1:]): + for a, b in pairwise(chain): link = self.link(a, b) if link is None: return 0.0, None, samples @@ -912,7 +912,7 @@ class MeshTopology: def path_cost(path: list[str]) -> float: total = 0.0 - for a, b in zip(path, path[1:]): + for a, b in pairwise(path): total += min((c for v, c in neighbors.get(a, ()) if v == b), default=math.inf) return total diff --git a/meshterm/ui/braillechart.py b/meshterm/ui/braillechart.py index a03178a..e5fdaeb 100644 --- a/meshterm/ui/braillechart.py +++ b/meshterm/ui/braillechart.py @@ -431,7 +431,7 @@ def axis_caption( taken: list[tuple[int, int]] = [] # placed [start, end) spans, in order ticks: list[int] = [] # the chart column each label points at ok = True - for frac, label in zip(fractions, labels): + for frac, label in zip(fractions, labels, strict=True): if frac == 0.0: start = 0 ref = 0 @@ -626,7 +626,7 @@ def axis_chart( marks = y_axis_labels(peak, len(chart_rows), lo=floor) label_w = label_w or max([1, *(len(mark) for mark in marks)]) out: list[Text] = [] - for mark, row in zip(marks, chart_rows): + for mark, row in zip(marks, chart_rows, strict=True): line = Text(f"{mark:>{label_w}} " + ("┤" if mark else "│"), style=style) line.append_text(row) line.append("├" if mark else "│", style=style) diff --git a/meshterm/ui/map_screen.py b/meshterm/ui/map_screen.py index 16ce28d..a10907e 100644 --- a/meshterm/ui/map_screen.py +++ b/meshterm/ui/map_screen.py @@ -141,7 +141,7 @@ class MapScreen(Screen): @property def fkey_lane(self): - """The PicoCalc lane: three ways to frame the view, then the zoom rocker. + r"""The PicoCalc lane: three ways to frame the view, then the zoom rocker. The map is the one screen that repurposes the nav actions wholesale: PgUp/PgDn zoom, Home reframes, and ``end`` is bound to nothing at all — so the shared lane's diff --git a/meshterm/ui/mapcanvas.py b/meshterm/ui/mapcanvas.py index a610328..99b6258 100644 --- a/meshterm/ui/mapcanvas.py +++ b/meshterm/ui/mapcanvas.py @@ -16,6 +16,7 @@ from __future__ import annotations import unicodedata from dataclasses import dataclass +from itertools import pairwise from ..platforms import Platform, on_platform from .marks import RGB, parse_hex # noqa: F401 - canonical home; re-exported for importers @@ -177,7 +178,7 @@ class MapCanvas: self, points: list[tuple[float, float]], color: RGB, priority: int ) -> None: """Rasterize a polyline through ``points`` (dot coordinates) as braille dots.""" - for (x0, y0), (x1, y1) in zip(points, points[1:]): + for (x0, y0), (x1, y1) in pairwise(points): self._segment(x0, y0, x1, y1, color, priority) def _segment( @@ -232,7 +233,7 @@ class MapCanvas: edges: list[tuple[float, float, float, float]] = [] ys: list[float] = [] for ring in rings: - for (x0, y0), (x1, y1) in zip(ring, ring[1:]): + for (x0, y0), (x1, y1) in pairwise(ring): if y0 != y1: edges.append((x0, y0, x1, y1)) ys.extend((y0, y1)) diff --git a/meshterm/ui/menu.py b/meshterm/ui/menu.py index 8974ccb..a361c93 100644 --- a/meshterm/ui/menu.py +++ b/meshterm/ui/menu.py @@ -522,7 +522,7 @@ async def _menu_loop(ctx: AppContext, session: TuiSession) -> None: name_w = max((label.cell_len for label in labels), default=0) items: list = [] current_category: str | None = None - for tool, row in zip(tools, labels): + for tool, row in zip(tools, labels, strict=True): if tool.category != current_category: current_category = tool.category items.append(section_heading(current_category)) diff --git a/meshterm/ui/menus.py b/meshterm/ui/menus.py index b09df94..8c41a91 100644 --- a/meshterm/ui/menus.py +++ b/meshterm/ui/menus.py @@ -333,7 +333,7 @@ def column_header(lanes: Sequence[Lane], width: int, *, indent: int = 2) -> str: def line() -> str: out = " " * indent - for lane, form in zip(lanes, picked): + for lane, form in zip(lanes, picked, strict=True): label = lane.forms[form] out += label + " " * max(0, lane.width - cell_len(label)) return out diff --git a/meshterm/ui/node_detail_screen.py b/meshterm/ui/node_detail_screen.py index 1f4bbe7..a108017 100644 --- a/meshterm/ui/node_detail_screen.py +++ b/meshterm/ui/node_detail_screen.py @@ -78,6 +78,7 @@ import re import time from dataclasses import dataclass, field, replace from datetime import datetime +from itertools import pairwise from typing import TYPE_CHECKING from rich.cells import cell_len @@ -2013,7 +2014,7 @@ def _route_is_fresh(topo, hops: tuple[str, ...], target: str, now: datetime) -> is treated as fresh — we have no age to hold against it. """ chain = [topo.self_id, *hops, target] - for a, b in zip(chain, chain[1:]): + for a, b in pairwise(chain): link = topo.link(a, b) if link is None: return False diff --git a/meshterm/ui/pathgraph.py b/meshterm/ui/pathgraph.py index 2b92dd5..4ac9321 100644 --- a/meshterm/ui/pathgraph.py +++ b/meshterm/ui/pathgraph.py @@ -104,7 +104,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence from dataclasses import dataclass -from itertools import permutations, product +from itertools import pairwise, permutations, product from math import ceil from .mapcanvas import MapCanvas @@ -375,7 +375,7 @@ def revisited_hops(hops: Sequence[str]) -> tuple[str, ...]: def _split_revisits(layers: Sequence[PathLayer]) -> list[PathLayer]: - """Give every revisit *within* a path its own node id, so no walk folds into a cycle. + r"""Give every revisit *within* a path its own node id, so no walk folds into a cycle. The k-th occurrence of a hop in one layer becomes ``hop\\x00#k`` (the first keeps the bare id), which leaves the flow acyclic and lets the balanced rank spread the walk evenly again. @@ -555,7 +555,7 @@ def render_path_graph( if node not in seen: seen.add(node) ordered_nodes.append(node) - edges.update(zip(seq, seq[1:])) + edges.update(pairwise(seq)) xfrac = _balanced_x(ordered_nodes, edges) @@ -667,9 +667,9 @@ def render_path_graph( # dropping out where it overlaps another. A two-way pair (``bidir``, above) draws as the # one honest vertical rather than a lane change. edge_style: dict[frozenset[str], tuple[int, RGB]] = {} - for layer, seq in sorted(zip(drawn, seqs), key=lambda ls: _draw_rank(ls[0])): + for layer, seq in sorted(zip(drawn, seqs, strict=True), key=lambda ls: _draw_rank(ls[0])): rank = _draw_rank(layer) - for u, v in zip(seq, seq[1:]): + for u, v in pairwise(seq): key = frozenset((u, v)) prev = edge_style.get(key) if prev is None or rank > prev[0]: @@ -731,7 +731,7 @@ def _route( (xu, yu), (xv, yv) = (xv, yv), (xu, yu) anchors: list[tuple[float, float]] = [(xu, yu), *sorted(vias), (xv, yv)] pts: list[tuple[float, float]] = [anchors[0]] - for (xa, ya), (xb, yb) in zip(anchors, anchors[1:]): + for (xa, ya), (xb, yb) in pairwise(anchors): if ya == yb: pts.append((xb, yb)) continue @@ -845,7 +845,7 @@ def bidir_clusters(sequences: Sequence[tuple[str, ...]]) -> list[tuple[str, ...] Returns each cluster's member ids in first-appearance order (endpoints excluded); a lone node or the tidy two-node pair is not a cluster and is not returned. """ - edges = {pair for seq in sequences for pair in zip(seq, seq[1:])} + edges = {pair for seq in sequences for pair in pairwise(seq)} ordered = list(dict.fromkeys(node for seq in sequences for node in seq)) rep = _merge_bidir_pairs(ordered, edges) groups: dict[str, list[str]] = {} @@ -1047,7 +1047,7 @@ def _bypass_vias( level: list[tuple[frozenset[str], float, int, int]] = [] seen: set[frozenset[str]] = set() for seq in seqs: - for u, v in zip(seq, seq[1:]): + for u, v in pairwise(seq): key = frozenset((u, v)) if key in seen or key in bidir: continue @@ -1328,7 +1328,7 @@ def _balance_sides( best_assign: dict[int, int] | None = None best_key: tuple[int, int, int] | None = None for combo in product((-1, 1), repeat=len(routes)): - assign = dict(zip(routes, combo)) + assign = dict(zip(routes, combo, strict=True)) if any(assign[d] != assign[s] for d, s in tie.items()): # a nest split off its sibling continue deep_above, deep_below = flanks(assign) diff --git a/meshterm/ui/purge_screen.py b/meshterm/ui/purge_screen.py index 277c890..01a875f 100644 --- a/meshterm/ui/purge_screen.py +++ b/meshterm/ui/purge_screen.py @@ -211,7 +211,7 @@ async def _rank(ctx: AppContext, contacts: list[Contact]) -> list[ScoredContact] watch = getattr(ctx, "watch_store", None) admin = getattr(ctx, "admin_store", None) - for contact, node in zip(contacts, nodes): + for contact, node in zip(contacts, nodes, strict=True): found = signals.get(node) if found is None: continue diff --git a/meshterm/ui/records_screen.py b/meshterm/ui/records_screen.py index f66fa1c..e05a216 100644 --- a/meshterm/ui/records_screen.py +++ b/meshterm/ui/records_screen.py @@ -446,10 +446,10 @@ class RecordDialog(Screen): canvas.draw_line(closed, _AREA_TONE, priority=0) # Hops first, then our own node last, so the star always wins its cell — a hop that # projects onto the same cell can never hide us. - for v, (dx, dy) in zip(verts, ring): + for v, (dx, dy) in zip(verts, ring, strict=True): if not v.is_self: canvas.marker(int(round(dx)), int(round(dy)), v.glyph, v.color) - for v, (dx, dy) in zip(verts, ring): + for v, (dx, dy) in zip(verts, ring, strict=True): if v.is_self: canvas.marker(int(round(dx)), int(round(dy)), v.glyph, v.color) return canvas.to_ansi_lines() diff --git a/meshterm/ui/timemachine_screen.py b/meshterm/ui/timemachine_screen.py index 935e5ea..ccecc31 100644 --- a/meshterm/ui/timemachine_screen.py +++ b/meshterm/ui/timemachine_screen.py @@ -668,7 +668,7 @@ def _ticks_fit(picks: list[int], labels: list[str], centers: list[int], chars: i label would drop ticks a variable-width axis has ample room for. """ last_end = -_TICK_GAP - for i, label in zip(picks, labels): + for i, label in zip(picks, labels, strict=True): cell = max(0, min(chars - 1, centers[i])) start = max(0, min(chars - len(label), cell - len(label) // 2)) if start < last_end + _TICK_GAP: @@ -707,9 +707,9 @@ def _fit_ticks( continue # rounding fused two picks — this many will not fit cleanly labels = label_of(picks) if _ticks_fit(picks, labels, centers, chars): - return list(zip((centers[i] for i in picks), labels)) + return list(zip((centers[i] for i in picks), labels, strict=True)) picks = [0] if n == 1 else [0, n - 1] - return list(zip((centers[i] for i in picks), label_of(picks))) + return list(zip((centers[i] for i in picks), label_of(picks), strict=True)) def _day_ticks(shown: list, chars: int) -> list[tuple[int, str]]: @@ -912,7 +912,7 @@ def _day_columns(values: list[int], chars: int) -> list: :data:`~meshterm.ui.braillechart.GAP` marks a day-boundary notch). """ out: list = [GAP] * (chars * 2) - for value, (start, width) in zip(values, _day_spans(len(values), chars)): + for value, (start, width) in zip(values, _day_spans(len(values), chars), strict=True): out[start : start + width] = [value] * width return out diff --git a/meshterm/ui/trace_screen.py b/meshterm/ui/trace_screen.py index 04fe516..5add13c 100644 --- a/meshterm/ui/trace_screen.py +++ b/meshterm/ui/trace_screen.py @@ -1177,7 +1177,7 @@ 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)): + for number, trace in zip(range(len(self._traces), 0, -1), reversed(self._traces), strict=True): rows.append(self._trace_row(number, trace)) return Group(*rows) diff --git a/meshterm/ui/tui/frame.py b/meshterm/ui/tui/frame.py index ec1ddb8..b24b107 100644 --- a/meshterm/ui/tui/frame.py +++ b/meshterm/ui/tui/frame.py @@ -372,7 +372,7 @@ def _banner_lines(banner: Sequence[str], cols: int) -> list[str]: return [] widths = [cell_len(Text.from_ansi(row).plain) for row in banner] width = max(widths) - padded = [fold_text(row) + " " * (width - w) for row, w in zip(banner, widths)] + padded = [fold_text(row) + " " * (width - w) for row, w in zip(banner, widths, strict=True)] return _center(padded, cols) diff --git a/meshterm/ui/tui/session.py b/meshterm/ui/tui/session.py index 9d4dd20..3e38957 100644 --- a/meshterm/ui/tui/session.py +++ b/meshterm/ui/tui/session.py @@ -267,7 +267,7 @@ def _changed_rows(before: str, after: str) -> list[int] | None: old, new = before.split("\n"), after.split("\n") if len(old) != len(new): return None - return [i for i, (a, b) in enumerate(zip(old, new)) if a != b] + return [i for i, (a, b) in enumerate(zip(old, new, strict=True)) if a != b] def _has_wide_glyph(text: str) -> bool: @@ -680,7 +680,7 @@ class TuiSession: self._scrub_columns(cols - count, cols) def _scrub_rows(self, rows: Sequence[int]) -> bool: - """Force prompt_toolkit to rewrite these whole terminal rows on the next diff. + r"""Force prompt_toolkit to rewrite these whole terminal rows on the next diff. The row-wise twin of :meth:`_scrub_columns`, and the cheap form of the wide-glyph repaint (:meth:`_emit`): every column of each listed row is sentinelled, so pt finds @@ -1539,7 +1539,7 @@ class TuiSession: return bool(self._float_layers()) def _emit(self, text: str, layer: str = "base") -> ANSI: - """Wrap a composed frame as prompt_toolkit :class:`ANSI`, repainting whole *rows* + r"""Wrap a composed frame as prompt_toolkit :class:`ANSI`, repainting whole *rows* when it holds a glyph the terminal may draw narrower than pt reserves for it. prompt_toolkit paints differentially: it rewrites only the cells that changed since diff --git a/meshterm/ui/walk_screen.py b/meshterm/ui/walk_screen.py index 4569026..c65dfae 100644 --- a/meshterm/ui/walk_screen.py +++ b/meshterm/ui/walk_screen.py @@ -57,6 +57,7 @@ from __future__ import annotations import math from collections import deque from datetime import datetime +from itertools import pairwise from typing import TYPE_CHECKING from rich.cells import cell_len @@ -201,10 +202,10 @@ def _snr_rgb(snr: float | None) -> RGB: if snr >= _SNR_STOPS[-1][0]: return _SNR_STOPS[-1][1] (x0, c0), (x1, c1) = next( - (lo, hi) for lo, hi in zip(_SNR_STOPS, _SNR_STOPS[1:]) if lo[0] <= snr <= hi[0] + (lo, hi) for lo, hi in pairwise(_SNR_STOPS) if lo[0] <= snr <= hi[0] ) f = (snr - x0) / (x1 - x0) - return tuple(round(a + (b - a) * f) for a, b in zip(c0, c1)) # type: ignore[return-value] + return tuple(round(a + (b - a) * f) for a, b in zip(c0, c1, strict=True)) # type: ignore[return-value] def _scaled(rgb: RGB, factor: float) -> RGB: @@ -1039,7 +1040,7 @@ class WalkScreen(Screen): # marker, in cells, doubled into dot space. rx = max(floor, 2 * (width - 2 - _FAN_BASE_LABEL_W)) - ax phi = _FAN_HALF_ANGLE * min(1.0, (len(keys) - 1) / 5.0) - for i, (node, row, label) in enumerate(zip(keys, rows, labels)): + for i, (node, row, label) in enumerate(zip(keys, rows, labels, strict=True)): angle = 0.0 if len(keys) == 1 else -phi + (2 * phi) * i / (len(keys) - 1) x = ax + rx * math.cos(angle * _FAN_X_FLATTEN) room = 2 * (width - 2 - min(cell_len(label), _FAN_LABEL_W)) diff --git a/meshterm/ui/widgets.py b/meshterm/ui/widgets.py index 48e2c09..13e8bfa 100644 --- a/meshterm/ui/widgets.py +++ b/meshterm/ui/widgets.py @@ -6,6 +6,7 @@ import math from collections.abc import Callable, Sequence from dataclasses import dataclass, field from datetime import datetime +from itertools import pairwise from typing import TYPE_CHECKING from rich import box @@ -935,10 +936,10 @@ def _recency_gradient(secs: float | None) -> str: r, g, b = _HEAT_STOPS[-1][1] else: (x0, c0), (x1, c1) = next( - (lo, hi) for lo, hi in zip(_HEAT_STOPS, _HEAT_STOPS[1:]) if lo[0] <= x <= hi[0] + (lo, hi) for lo, hi in pairwise(_HEAT_STOPS) if lo[0] <= x <= hi[0] ) f = (x - x0) / (x1 - x0) - r, g, b = (round(a + (bb - a) * f) for a, bb in zip(c0, c1)) + r, g, b = (round(a + (bb - a) * f) for a, bb in zip(c0, c1, strict=True)) return f"#{r:02x}{g:02x}{b:02x}" diff --git a/tests/test_chat.py b/tests/test_chat.py index e8f1a12..c724cd6 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -495,7 +495,8 @@ 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.""" ctx = _RowCtx(repo) - head = lambda conv: _title(ctx, conv, {}, _NO_KEYS).plain.split(" ", 1)[0] + def head(conv): + return _title(ctx, conv, {}, _NO_KEYS).plain.split(" ", 1)[0] named = Conversation( label="#general", is_channel=True, channel_id="c0", secret=derive_secret("#general") ) diff --git a/tests/test_contact_score.py b/tests/test_contact_score.py index 1c232e2..7524420 100644 --- a/tests/test_contact_score.py +++ b/tests/test_contact_score.py @@ -41,7 +41,7 @@ def _rank(*specs) -> list: # noqa: ANN001 """Rank ``(name, signal-overrides)`` pairs, returning the scored contacts strongest first.""" contacts = [_contact(name) for name, _ in specs] signals = {} - for contact, (_, overrides) in zip(contacts, specs): + for contact, (_, overrides) in zip(contacts, specs, strict=True): node = contact.public_key[:12] signals[node] = ContactSignals(node=node, **{**_BASE, **overrides}) return rank_contacts(contacts, signals) diff --git a/tests/test_map.py b/tests/test_map.py index d00d9fc..97ec21e 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -615,7 +615,7 @@ def test_render_map_brightens_piled_markers() -> None: crowd = [MapMarker(f"n{i}", 45.50, -73.57) for i in range(8)] piled = _glyph_color(render_map(vp, {}, crowd), "●") # Washed toward white: every channel is brighter than the base cyan. - assert all(p > b for p, b in zip(piled, base)) + assert all(p > b for p, b in zip(piled, base, strict=True)) assert piled != base @@ -684,7 +684,7 @@ def test_the_ghost_ground_lands_where_the_pan_put_it() -> None: assert len(before) == len(after) matched = sum( 1 - for old, new in zip(before, after) + for old, new in zip(before, after, strict=True) if old[shift:].rstrip() and new.rstrip() == old[shift:].rstrip() ) assert matched > len(before) // 2, "the ground did not slide with the view" @@ -906,7 +906,7 @@ def test_decoded_layers_survive_a_round_trip() -> None: assert restored is not None assert [(l.name, l.extent) for l in restored] == [(l.name, l.extent) for l in original] - for before, after in zip(original, restored): + for before, after in zip(original, restored, strict=True): assert after.features == before.features diff --git a/tests/test_menus.py b/tests/test_menus.py index ebdbcd8..bc61128 100644 --- a/tests/test_menus.py +++ b/tests/test_menus.py @@ -56,7 +56,7 @@ def test_menu_rows_align_descriptions_in_display_cells() -> None: from rich.cells import cell_len starts = {cell_len(r.title.plain[: r.title.plain.index(d)]) for r, d in - zip(rows, ["Restart it", "Second"])} + zip(rows, ["Restart it", "Second"], strict=True)} assert len(starts) == 1 # one shared description column @@ -219,14 +219,14 @@ def test_marked_label_lines_up_a_mixed_list_when_told_its_lane() -> None: marked_label(_WIDE, "View archived contacts", "", lane=lane), ] starts = {cell_len(row.plain[: row.plain.index(word)]) - for row, word in zip(rows, ("Purge", "View"))} + for row, word in zip(rows, ("Purge", "View"), strict=True)} assert starts == {lane + 1} # Without the lane each mark measures itself, which is right for a list whose rows all # lead with the same icon — and is exactly what misaligns a mixed one. solo = [marked_label(_NARROW, "A", "err"), marked_label(_WIDE, "B", "")] assert len({cell_len(row.plain[: row.plain.index(letter)]) - for row, letter in zip(solo, ("A", "B"))}) == 2 + for row, letter in zip(solo, ("A", "B"), strict=True)}) == 2 def test_the_main_menu_starts_every_title_in_the_same_cell() -> None: @@ -252,7 +252,7 @@ def test_the_main_menu_starts_every_title_in_the_same_cell() -> None: assert widths == {1, 2}, "a menu of one icon width would prove nothing" starts = { label.cell_len - cell_len(tool.title or tool.name) - for tool, label in zip(tools, labels) + for tool, label in zip(tools, labels, strict=True) } assert len(starts) == 1, "a title starts a column early" diff --git a/tests/test_pathgraph.py b/tests/test_pathgraph.py index 63d83b2..e4f3654 100644 --- a/tests/test_pathgraph.py +++ b/tests/test_pathgraph.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +from itertools import pairwise import pytest @@ -197,7 +198,7 @@ def test_revisited_path_folds_into_a_cycle_that_piles_its_relays() -> None: hops = ("7f", "c1", "8e", "da", "ee", "c1", "27") seq = (SRC_NODE, *hops, DST_NODE) ordered = list(dict.fromkeys(seq)) - x = _balanced_x(ordered, set(zip(seq, seq[1:]))) + x = _balanced_x(ordered, set(pairwise(seq))) # Evenly spread, these eight nodes would sit 1/7 apart and the four loop members would # span three of those gaps; folded, the whole loop fits inside a single one. loop = [x[node] for node in ("c1", "8e", "da", "ee")] @@ -217,12 +218,12 @@ def test_allow_duplicate_nodes_keeps_x_strictly_rising_along_the_walk() -> None: [layer] = _split_revisits([PathLayer(hops, WHITE, 3)]) seq = (SRC_NODE, *layer.hops, DST_NODE) ordered = list(dict.fromkeys(seq)) - x = _balanced_x(ordered, set(zip(seq, seq[1:]))) + x = _balanced_x(ordered, set(pairwise(seq))) walked = [x[node] for node in seq] assert walked == sorted(walked) and len(set(walked)) == len(walked) assert x[SRC_NODE] == 0.0 and x[DST_NODE] == 1.0 # A single walk with nothing else to balance against spreads dead evenly end to end. - gaps = [b - a for a, b in zip(walked, walked[1:])] + gaps = [b - a for a, b in pairwise(walked)] assert max(gaps) - min(gaps) < 1e-9 @@ -390,7 +391,7 @@ def _packed_lanes(layers, width=60, max_rows=15): # noqa: ANN001 if node not in seen: seen.add(node) ordered.append(node) - edges.update(zip(seq, seq[1:])) + edges.update(pairwise(seq)) xfrac = _balanced_x(ordered, edges) best = max(range(len(drawn)), key=lambda j: drawn[j].priority) owner: dict[str, int] = {} @@ -632,7 +633,7 @@ def _vias_for(layers, width=60, max_rows=15): # noqa: ANN001 if node not in seen: seen.add(node) ordered.append(node) - edges.update(zip(seq, seq[1:])) + edges.update(pairwise(seq)) xfrac = _balanced_x(ordered, edges) best = max(range(len(drawn)), key=lambda j: drawn[j].priority) owner: dict[str, int] = {} @@ -774,7 +775,7 @@ def test_bidir_pair_does_not_jam_a_sibling_relay_against_the_origin() -> None: ] seqs = [(SRC_NODE, *layer.hops, DST_NODE) for layer in layers] ordered = list(dict.fromkeys(n for seq in seqs for n in seq)) - edges = {pair for seq in seqs for pair in zip(seq, seq[1:])} + edges = {pair for seq in seqs for pair in pairwise(seq)} x = _balanced_x(ordered, edges) assert x[SRC_NODE] == 0.0 and x[DST_NODE] == 1.0 assert x["cc"] == pytest.approx(1 / 3) # its natural spot, not jammed toward 0 diff --git a/tests/test_pathline.py b/tests/test_pathline.py index 814cc64..a3020df 100644 --- a/tests/test_pathline.py +++ b/tests/test_pathline.py @@ -57,7 +57,7 @@ def _char_styles(text) -> list[tuple[str, str]]: # noqa: ANN001 for span in text.spans: for i in range(span.start, span.end): styles[i] = str(span.style) - return list(zip(text.plain, styles)) + return list(zip(text.plain, styles, strict=True)) def test_plain_mode_matches_the_app_wide_arrow_presentation() -> None: diff --git a/tests/test_timemachine.py b/tests/test_timemachine.py index 44f3caa..7288d62 100644 --- a/tests/test_timemachine.py +++ b/tests/test_timemachine.py @@ -8,6 +8,7 @@ from __future__ import annotations import re from datetime import timedelta +from itertools import pairwise from pathlib import Path from types import SimpleNamespace @@ -344,7 +345,7 @@ def test_day_columns_widths_differ_by_at_most_one_dot_and_interleave() -> None: notches = [i for i, c in enumerate(cols) if c is GAP] # one per day boundary assert len(notches) == 12 edges = [-1, *notches, len(cols)] # each bar runs between two notches - widths = [b - a - 1 for a, b in zip(edges, edges[1:])] + widths = [b - a - 1 for a, b in pairwise(edges)] assert len(widths) == 13 and set(widths) == {3, 4} first_wide = widths.index(4) last_wide = len(widths) - 1 - widths[::-1].index(4) diff --git a/tests/test_tui.py b/tests/test_tui.py index acba2a2..68570ad 100644 --- a/tests/test_tui.py +++ b/tests/test_tui.py @@ -1241,7 +1241,7 @@ def test_device_picker_builds_aligned_columns(tmp_path) -> None: assert rows[-2].strip().endswith("Add a network device…") # no caveat tag trailing it assert rows[-1].strip().endswith("Quit") device_rows = rows[:2] - assert all(port in row for port, row in zip(("COM5", "/dev/ttyUSB0"), device_rows)) + assert all(port in row for port, row in zip(("COM5", "/dev/ttyUSB0"), device_rows, strict=True)) assert device_rows[0].index("COM5") == device_rows[1].index("/dev/ttyUSB0") @@ -2765,7 +2765,7 @@ def test_the_rows_a_dialog_does_not_reach_come_back_unchanged() -> None: session.push(ButtonDialog("Sure?", [("Cancel", 0), ("Yes", 1)])) second = session._plain_frame().split("\n") - unchanged = sum(1 for a, b in zip(first, second) if a == b) + unchanged = sum(1 for a, b in zip(first, second, strict=True) if a == b) assert unchanged >= 10, "the box should only rewrite the rows it covers" diff --git a/tests/test_walk.py b/tests/test_walk.py index 9f8bfac..7cc0269 100644 --- a/tests/test_walk.py +++ b/tests/test_walk.py @@ -135,7 +135,7 @@ def test_walk_graph_labels_names_in_full_when_the_canvas_has_room() -> None: ] topo = MeshTopology(US, contacts=long_names) when = utcnow() - for c, snr in zip(long_names, (8.0, 1.0)): + for c, snr in zip(long_names, (8.0, 1.0), strict=True): topo.add_walk([topo.self_id, topo.canonical(c.public_key)], snrs=[snr], when=when, source="trace") screen = WalkScreen( diff --git a/tests/test_watchtower.py b/tests/test_watchtower.py index 879a9e4..28c6f06 100644 --- a/tests/test_watchtower.py +++ b/tests/test_watchtower.py @@ -238,7 +238,8 @@ def test_alert_row_hues_the_label_by_resolved_key(tmp_path: Path) -> None: from meshterm.ui.theme import name_style from meshterm.ui.watchtower_screen import _alert_lanes - key_of = lambda label: "d4" * 6 if label == "Roof" else None + def key_of(label): + return "d4" * 6 if label == "Roof" else None alert = Alert(ident=1, when=utcnow(), kind="silence", label="Roof", message="quiet") row = _alert_lanes(alert, key_of) at = row.plain.index("Roof") @@ -261,8 +262,10 @@ def test_alert_row_leads_node_name_with_type_glyph(tmp_path: Path) -> None: from meshterm.ui.watchtower_screen import _alert_lanes from meshterm.ui.widgets import _DEFAULT_GLYPH, _NODE_GLYPHS - key_of = lambda label: None - type_of = lambda label: 2 if label == "Roof" else None # Roof advertises as a repeater + def key_of(label): + return None + def type_of(label): + return 2 if label == "Roof" else None # Roof advertises as a repeater glyph, glyph_style = _NODE_GLYPHS[2] alert = Alert(ident=1, when=utcnow(), kind="silence", label="Roof", message="quiet")