Let --context expand any selection past its range, drawing from the whole file
This commit is contained in:
1 parent
c2821dedb5
commit
788d285e38
3 files changed
+90
-11
No files matched your search
+1
-1
@@ -82,7 +82,7 @@ A revision number identifies a *state position between diffs*, not a diff:
|
||||
|
||||
Like repeated `--grep` patterns (which must all match), repeated ranges are restrictive: `-r r1:r5 -r 0:2` prints only the first two records of those in the revision window, regardless of flag order. Plain ranges truncate Python-style and print nothing when the intersection is empty; a single plain index must exist and fails with exit code 2 otherwise.
|
||||
|
||||
`--context N` (default 0) adds GNU grep-style context as the final step: each printed item is expanded by N surrounding records drawn from the records in the processed range, free of the `--grep` and plain-number filters, with overlapping expansions merged so nearby matches do not duplicate context records.
|
||||
`--context N` (default 0) adds GNU grep-style context as the final step: each printed item is expanded by N surrounding records drawn from the whole file — expanding whatever would be shown past any range restriction, so `-r s3 --context 2` shows that snapshot plus the two records before and after it. Overlapping expansions merge so nearby matches do not duplicate context records.
|
||||
|
||||
The JSON dump (`-o`) and migrations operate on the state after the **last printed record**, whatever filtering produced the printout — so a plain `-r 2:5` dump ends at the 5th filtered record, and `--grep` or `--context` shift the dump to their last printed record as well. When nothing is printed (an empty filter result or `-q`), the dump falls back to the state at the end of the unit-spec window (or the end of the file).
|
||||
|
||||
|
||||
+27
-8
@@ -289,9 +289,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
help=(
|
||||
"Print N surrounding records around each printed item, GNU grep"
|
||||
" style. Applied last, after --grep filtering and plain-number"
|
||||
" -r slicing: context records are drawn from the records in the"
|
||||
" processed range, free of those filters, and overlapping"
|
||||
" expansions merge. Without --grep this has no visible effect."
|
||||
" -r slicing: context records are drawn from the whole file,"
|
||||
" expanding whatever would be shown past any range restriction,"
|
||||
" and overlapping expansions merge. Without any other filter"
|
||||
" this has no visible effect."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
@@ -547,7 +548,7 @@ async def _run(args: argparse.Namespace) -> int:
|
||||
|
||||
grep_patterns = [GrepPattern.parse(p) for p in args.grep or ()]
|
||||
|
||||
if selection.snapshot is not None:
|
||||
if selection.snapshot is not None and not args.context:
|
||||
snap_event = selection.snapshot
|
||||
state = snap_event.snap.state
|
||||
version = snap_event.snap.v
|
||||
@@ -560,16 +561,31 @@ async def _run(args: argparse.Namespace) -> int:
|
||||
)
|
||||
_print()
|
||||
else:
|
||||
# Replay up to the range end, collecting the in-window item pool
|
||||
# and tracking which items survive --grep.
|
||||
# Replay, collecting the item pool and tracking which items
|
||||
# survive the range window and --grep. With --context the pool
|
||||
# spans the whole file, so context can expand whatever would be
|
||||
# shown past any range restriction; otherwise the pool is
|
||||
# limited to the window.
|
||||
state = {}
|
||||
version = 0
|
||||
window_state: dict[str, Any] = {}
|
||||
window_version = 0
|
||||
pool_end = end_of_file(events) if args.context else selection.end_line
|
||||
pool: list[tuple[Event, dict[str, Any] | None, dict[str, Any], Any]] = []
|
||||
matched: list[int] = []
|
||||
for event, previous, current in replay_events(events, selection.end_line):
|
||||
for event, previous, current in replay_events(events, pool_end):
|
||||
if event.line_number < selection.end_line:
|
||||
window_state = current
|
||||
window_version = event.version
|
||||
state = current
|
||||
version = event.version
|
||||
if event.line_number < selection.start_line or args.quiet:
|
||||
if (
|
||||
args.quiet
|
||||
or event.line_number < selection.start_line
|
||||
or event.line_number >= selection.end_line
|
||||
):
|
||||
if not args.quiet and args.context:
|
||||
pool.append((event, previous, current, None))
|
||||
continue
|
||||
highlight = None
|
||||
if isinstance(event, SnapshotEvent):
|
||||
@@ -629,6 +645,9 @@ async def _run(args: argparse.Namespace) -> int:
|
||||
last_event, _, last_current, _ = items[-1]
|
||||
state = last_current
|
||||
version = last_event.version
|
||||
else:
|
||||
state = window_state
|
||||
version = window_version
|
||||
for event, previous, current, highlight in items:
|
||||
label = record_label(event.line_number, event.record_index)
|
||||
if isinstance(event, SnapshotEvent):
|
||||
|
||||
+62
-2
@@ -2,13 +2,17 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from kanta.serialization import JsonSerializer
|
||||
from kanta.structs import META_FORMAT, ChangeRecord, Snapshot
|
||||
from tests.support import Data, make_kanta
|
||||
|
||||
# The CLI reads files with its own JSON Kanta instance unless -k is given.
|
||||
JSON = ("json", JsonSerializer)
|
||||
|
||||
T0 = datetime(2026, 1, 1, tzinfo=UTC)
|
||||
|
||||
|
||||
def _run_cli(path, *args):
|
||||
from kanta.__main__ import main
|
||||
@@ -148,7 +152,7 @@ def test_unit_and_plain_combine(tmp_path, capsys):
|
||||
|
||||
|
||||
def test_context_expands_after_grep(tmp_path, capsys):
|
||||
"""Context records come from the in-window pool, free of --grep."""
|
||||
"""Context records come from the whole file, free of --grep."""
|
||||
path = tmp_path / "data.kantadb"
|
||||
_build(path, increments=5)
|
||||
capsys.readouterr()
|
||||
@@ -168,7 +172,7 @@ def test_context_expands_after_grep(tmp_path, capsys):
|
||||
assert "inc1" not in err
|
||||
assert "inc2" in err
|
||||
|
||||
# Context is clamped at the start of the range.
|
||||
# Context is clamped at the start of the file.
|
||||
assert _run_cli(path, "--grep", "bootstrap", "--context", "2") == 0
|
||||
err = capsys.readouterr().err
|
||||
assert "bootstrap" in err
|
||||
@@ -177,6 +181,62 @@ def test_context_expands_after_grep(tmp_path, capsys):
|
||||
assert "inc2" not in err
|
||||
|
||||
|
||||
def test_context_expands_past_unit_window(tmp_path, capsys):
|
||||
"""Context expands whatever a unit selection would show, past the window."""
|
||||
path = tmp_path / "data.kantadb"
|
||||
_build(path)
|
||||
capsys.readouterr()
|
||||
|
||||
# -r r2 alone shows only inc0 (the record producing state 2); context 1
|
||||
# expands past the window to bootstrap and inc1, and the dump follows
|
||||
# the last printed record.
|
||||
assert _run_cli(path, "-r", "r2", "--context", "1", "-o", "-") == 0
|
||||
out = capsys.readouterr()
|
||||
assert "bootstrap" in out.err
|
||||
assert "inc0" in out.err
|
||||
assert "inc1" in out.err
|
||||
assert "inc2" not in out.err
|
||||
assert json.loads(out.out) == {"users": {}, "counter": 2}
|
||||
|
||||
|
||||
def test_context_expands_snapshot_selection(tmp_path, capsys):
|
||||
"""-r sN --context N shows the snapshot plus its neighbors."""
|
||||
path = tmp_path / "data.kantadb"
|
||||
serializer = JsonSerializer()
|
||||
framer = serializer.framer_cls()
|
||||
out_bytes = bytearray()
|
||||
out_bytes += framer.frame_change(
|
||||
serializer.encode(ChangeRecord(ts=T0, a="inc0", v=0, diff={"counter": 1})),
|
||||
record_offset=len(out_bytes),
|
||||
)
|
||||
snap = Snapshot(ts=T0, v=0, state={"counter": 1}, r=1, k=META_FORMAT)
|
||||
out_bytes += framer.frame_snapshot(
|
||||
serializer.encode(snap), record_offset=len(out_bytes)
|
||||
)
|
||||
out_bytes += framer.frame_change(
|
||||
serializer.encode(ChangeRecord(ts=T0, a="inc1", v=0, diff={"counter": 2})),
|
||||
record_offset=len(out_bytes),
|
||||
)
|
||||
path.write_bytes(bytes(out_bytes))
|
||||
capsys.readouterr()
|
||||
|
||||
# Without context, s1 (the file starts with a change, so s0 is l0)
|
||||
# shows just the snapshot and its state.
|
||||
assert _run_cli(path, "-r", "s1", "-o", "-") == 0
|
||||
out = capsys.readouterr()
|
||||
assert "snapshot s1" in out.err
|
||||
assert "inc0" not in out.err
|
||||
assert json.loads(out.out) == {"counter": 1}
|
||||
|
||||
# With context, the selection expands past the snapshot line.
|
||||
assert _run_cli(path, "-r", "s1", "--context", "1", "-o", "-") == 0
|
||||
out = capsys.readouterr()
|
||||
assert "inc0" in out.err
|
||||
assert "snapshot s1" in out.err
|
||||
assert "inc1" in out.err
|
||||
assert json.loads(out.out) == {"counter": 2}
|
||||
|
||||
|
||||
def test_context_merges_overlaps(tmp_path, capsys):
|
||||
"""Nearby matches share context instead of duplicating records."""
|
||||
path = tmp_path / "data.kantadb"
|
||||
|
||||
Reference in new issue
Block a user