Build timeline annotations through the Annotation dataclass on load - #431
Merged
Conversation
TimelineAnnotations.load() hand-built the same interval payload that add_annotation() builds, so the two could drift. It also rebound its own `data` parameter to that per-entry dict inside the loop that iterates `data`, which is harmless today only because the iterator is captured before the first rebind. Route load() through cls.Annotation/add_annotation instead, rename the loop variable to `entry`, and correct the docstring, which described `data` as a dict rather than a list of serialized annotations. Also replace the module's print() calls with the standard logging module per the project's logging conventions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ua3Fkt11kvBCZ6iuVbsbVc
Contributor
There was a problem hiding this comment.
Pull request overview
Refactors TimelineAnnotations.load() to build timeline intervals via the TimelineAnnotations.Annotation dataclass and the existing add_annotation() path, reducing duplicated payload construction and aligning load-time behavior with runtime additions.
Changes:
- Updated
TimelineAnnotations.load()to constructAnnotationobjects and delegate interval creation toadd_annotation(). - Replaced
print(..., file=sys.stderr)warnings with module logging (logger.warning) inload()andserialize(). - Added tests to ensure load/add payload equivalence, continued loading after skipped entries, identity display fallback behavior, and warning emission via logging.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/jabs/project/timeline_annotations.py | Refactors load path to reuse Annotation + add_annotation() and switches warnings from print to logging. |
| tests/project/test_timeline_annotations.py | Adds tests covering payload equivalence, skip/continue behavior, display-identity fallback, and logging warnings. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reasoning
I looked for a real bug first, and didn't find one I could confirm with high confidence, so I moved down the list to duplication and clarity. The strongest candidate was
TimelineAnnotations.load()insrc/jabs/project/timeline_annotations.py, which had three related problems in one ~50-line method:Duplicated payload construction.
load()hand-built the interval data dict (tag,color,description,identity,display_identity) and wroteannotations._tree[start : end + 1] = datadirectly — exactly whatadd_annotation()already does from anAnnotation. Two copies of the same payload shape, in the same class, that can drift: adding a field toAnnotation/add_annotationwould silently not appear on annotations that came in throughload(). TheAnnotationdataclass exists precisely to carry this.The
dataparameter was rebound inside the loop that iterates it. Line 110 diddata = {...}for the per-entry payload while the enclosing loop wasfor annotation in data:. This is not a live bug —forcaptures the iterator before the first rebind, so iteration completes correctly — but it is a trap: adding any second use ofdataafter the loop (a length check, a retry pass, a log line) would read the last entry's payload instead of the input list.Wrong docstring type. The parameter was documented as
data (dict): The dictionary containing serialized timeline annotationswhile the signature islist[dict[str, Any]].Why this is safe:
add_annotation()writesself._tree[annotation.start : annotation.end + 1] = {...}with the same five keys and the same values thatload()was writing inline, so the resulting interval tree is byte-for-byte identical. Validation, skip behavior, and theid_index_to_displayfallback tostr(identity_index)are all unchanged. Addedtest_load_stores_same_payload_as_add_annotationto pin the equivalence directly.The one intentional behavioral difference is the warning destination: the module's
print(..., file=sys.stderr)calls are nowlogger.warning(...)with lazy formatting, per CLAUDE.md's "Never useprint()for operational output". Visibility is preserved in both entry points — the GUI configureslogging.basicConfig(level=logging.WARNING)ingui_entrypoint.py, and the CLI has no handler configured, so Python's last-resort handler still emits WARNING and above to stderr.Other candidates I considered and dropped: the unused
frame_maskparameter onFeature._compute_window_feature/_compute_signal_features(removing it cascades into theidentityarguments of_window_standard/_window_signal, which are protected methods a downstream feature subclass could plausibly override — more risk than a cleanup warrants); andsignal_stats.psd_mean_banddocumentingfreqsas "ignored" when it selects the band (real, but a one-line docstring fix, too thin to stand alone and unrelated to this module).Change
Logic changes
src/jabs/project/timeline_annotations.pyload()now constructs acls.Annotationand callsannotations.add_annotation(...)instead of assembling the interval payload and writing to_treedirectly.annotationtoentryso thedataparameter is no longer shadowed; the per-entry dict is gone entirely.dataparam docstring tolist[dict[str, Any]]and noted thatstart/endare inclusive.end + 1" note ontoadd_annotation(), which is where the+ 1now lives.print()calls inload()andserialize()with a module-levelloggerand lazy%sformatting; dropped the now-unusedimport sys.tests/project/test_timeline_annotations.py— four new tests:test_load_stores_same_payload_as_add_annotation— the two construction paths produce identical trees (this is the guard for the refactor).test_load_continues_after_skipping_invalid_entry— a skipped entry doesn't stop later entries loading.test_load_without_display_mapping_stringifies_identity— thestr(identity_index)fallback when no mapping function is given.test_load_warns_on_skipped_entry— skips are reported through the module logger.Mechanical updates
None.
Verification
ruff check .andruff format .clean. Full root suite: 831 passed, 200 skipped. No files underpackages/were touched.This PR was produced by an automated analysis from Claude Code.
Generated by Claude Code