Release v0.11.1: fix --json performance and output#17
Conversation
Addresses review feedback on PR #16: - Load the ID cache once per invocation instead of per-task, eliminating O(N²) scans and N redundant file reads for large task lists. - Switch to click.echo for JSON output so Click's test-capture layer and stdout redirection work correctly. - Emit a stderr hint when the ID cache is empty on a filtered view, so null id values aren't silent. - Drop a redundant conditional on the id field. - Add 8 unit tests covering task-to-dict serialization, cache loading (missing/valid/malformed), and a regression guard against O(N²) reads. Keeps the JSON schema stable (int | null id, uuid always present). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review: PR #17 — Release v0.11.1Overall this is a clean, focused follow-up. The fixes are correct and the reasoning is well-documented. A few observations below. ✅ What's well done
🔍 Suggestions / minor issues1. Write-then-read round-trip on unfiltered JSON runs (minor) On an unfiltered
This is a redundant write+read cycle. It's not a bug (the file is fresh and consistent), but it's wasteful. Consider having 2. Stderr hint doesn't fire on empty-cache unfiltered On an unfiltered run, # Note: on unfiltered runs save_id_cache() above has already populated
# the cache, so uuid_to_id will not be empty here.
if not uuid_to_id and has_filters:3. Missing CLI integration tests (gap) All 8 new tests exercise from click.testing import CliRunner
from taskrepo.cli.commands.list import list_tasks
def test_json_output_captured_by_cli_runner(...):
runner = CliRunner()
result = runner.invoke(list_tasks, ["--json"], obj=...)
data = json.loads(result.output)
assert isinstance(data, list)4. Missing test for the stderr hint
🔒 Security / correctness
SummaryApprove with suggestions. The core fixes (O(N²) → O(N) I/O, Click output, UX hint) are correct and well-tested at the unit level. The two gaps — CLI integration test and stderr hint test — are the only things worth addressing before tagging 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Pull request overview
This PR is a follow-up release to improve tsk list --json by eliminating per-task cache file reads, ensuring JSON output flows through Click’s output layer for correct capture, and adding tests to prevent regressions.
Changes:
- Load the UUID→display-id cache once per
tsk list --jsoninvocation and use an in-memory map during serialization. - Emit JSON via
click.echo(json.dumps(...))and add a one-time stderr hint when the ID cache is empty on filtered views. - Add unit tests covering JSON field serialization and cache-loading behavior; bump version and update changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/taskrepo/cli/commands/list.py |
Adds _load_uuid_to_display_id(), updates JSON serialization path, and prints a stderr hint for empty cache on filtered output. |
tests/unit/test_list_json.py |
Introduces unit tests for JSON serialization and ID-cache loading/regression behavior. |
src/taskrepo/__version__.py |
Bumps version to 0.11.1. |
CHANGELOG.md |
Documents the 0.11.1 changes and the added test coverage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return {entry["uuid"]: int(display_id) for display_id, entry in cache.items()} | ||
| except (json.JSONDecodeError, KeyError, ValueError): |
There was a problem hiding this comment.
_load_uuid_to_display_id()’s docstring says it returns {} when the cache is missing or malformed, but the implementation can still raise exceptions (e.g., OSError if the file exists but can’t be opened, or AttributeError/TypeError if the parsed JSON isn’t a dict and has no .items()). Consider validating cache is a dict and/or broadening the exception handling so tsk list --json doesn’t crash on a corrupt/unreadable cache file.
| return {entry["uuid"]: int(display_id) for display_id, entry in cache.items()} | |
| except (json.JSONDecodeError, KeyError, ValueError): | |
| if not isinstance(cache, dict): | |
| return {} | |
| return { | |
| entry["uuid"]: int(display_id) | |
| for display_id, entry in cache.items() | |
| if isinstance(entry, dict) | |
| } | |
| except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError): |
| # created/modified are set by Task.__post_init__ defaults, so they are | ||
| # always serializable strings — just assert they round-trip through JSON. |
There was a problem hiding this comment.
This comment is inaccurate: Task.created/Task.modified defaults come from the dataclass default_factory=datetime.now (not Task.__post_init__), and in this test helper they’re also explicitly set in _make_task()’s defaults. Please update or remove the comment so it matches the actual source of these values.
| # created/modified are set by Task.__post_init__ defaults, so they are | |
| # always serializable strings — just assert they round-trip through JSON. | |
| # `created`/`modified` are populated for these helper-built tasks, and | |
| # `_task_to_dict()` should serialize them to JSON-safe string values. |
- Harden _load_uuid_to_display_id: reject non-dict top-level, filter entries missing `uuid`, catch OSError/TypeError (Copilot comment). - Add one-line comment clarifying the stderr-hint ordering invariant (claude[bot] suggestion). - Fix inaccurate comment about Task.__post_init__ in the test helper (Copilot comment). - Add CLI integration tests via CliRunner covering the exact regression path this release fixes: --json output end-to-end, empty result → [], and stderr hint on filtered empty-cache runs (claude[bot] suggestion). - Add unit test for non-dict top-level cache files. Deferred as follow-up: the save-then-load round-trip on unfiltered runs (noted by claude[bot], non-blocking). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review: Release v0.11.1 —
|
Summary
Follow-up to PR #16 addressing review comments from
claude[bot]and@Copilot._task_to_dictcalledget_display_id_from_uuid, which re-opened and linearly scanned the cache file on every call — O(N) file reads and O(N²) scans for N tasks.json.dump(..., sys.stdout)toclick.echo(json.dumps(...)), so Click's output layer (and test capture viaCliRunner) sees the output.nullid values.display_id if display_id is not None else None.What I did NOT change (and why)
idtyped asint | nullrather than a mixedint | "uuid-prefix..."string. Theuuidfield is already the stable fallback, and a union type would breakjq/schema consumers.Test plan
tests/unit/test_list_json.py): field coverage, missing-cache → null id, None-date handling, JSON serializability, cache loading (missing/valid/malformed), regression guard against per-task cache reads.uv run pytest tests/ -q).uv run ruff checkclean.tsk list --json -s pending --priority hreturns 25 valid JSON objects.v0.11.1→ PyPI publish, thenjust release taskrepo.🤖 Generated with Claude Code