fix: close the score viewer when the score timeline is cleared - #584
Open
FelipeDefensor wants to merge 5 commits into
Open
fix: close the score viewer when the score timeline is cleared#584FelipeDefensor wants to merge 5 commits into
FelipeDefensor wants to merge 5 commits into
Conversation
`load_svg_data` only treated `set_data` returning success=False as a failure, but `viewer_beat_x` uses `validate_pre_validated`, so the call always succeeds and SCORE_SVG_CREATE_ERROR was unreachable. A musicXML render without usable beat markers silently produced an empty beat-to-x mapping instead of telling the user the file was not set up properly. Check the returned mapping as well.
Reading `svg_view` constructed an SvgViewer whenever none was registered, so any consumer could resurrect a viewer as a side effect of a lookup -- including `on_audio_time_change`, which runs on every playback tick, and `reset_svg`, whose whole job is to get rid of the viewer. `svg_view` now returns None when there is no viewer, and the explicit `get_or_create_svg_view()` handles construction. That also covers the case where the request returns None instead of raising: only one callback can be attached per Get member, so with two score timelines the viewer of one answers for the other's id and returns None. `reset_svg` and `on_audio_time_change` raised AttributeError on that path.
Clearing a score timeline left `svg_data` in place, so the viewer came back with the discarded score: `reset_svg` closed it, but the next playback tick reopened and reloaded it, and even without that the score was still in the saved file and reappeared on the next open. `ScoreTLComponentManager.clear` now discards `svg_data`, and `update_svg_data` closes the viewer instead of loading when there is no score left. That second half also matters for undo/redo: restoring a cleared state pushes an empty string through the update trigger, which `SvgViewer.load_svg_data` fed straight to `etree.fromstring` and raised `XMLSyntaxError: Document is empty`. It now refuses empty data outright. Closes #472
pytest-xdist is already a dev dependency but nothing said to use it, so the documented way to run the suite takes minutes instead of about one. Also record the two intermittent-failure sources that look like real regressions but are not: cross-test interference under -n auto, and undoable() depending on PYTHONHASHSEED through the component ordering in restore_state.
_setup_svg_view now goes through get_or_create_svg_view, which sets the measure tracker's visibility from the viewer instead of calling show() unconditionally. Pin the behaviour it is meant to preserve.
azfoo
reviewed
Aug 27, 2026
Comment on lines
+46
to
+50
| `-n auto`, and which ones fail varies between runs; this predates any given change, so confirm a | ||
| failure reproduces serially before investigating it. The same goes for `undoable()` on a timeline | ||
| with several components: `TimelineComponentManager.restore_state` recreates components in | ||
| set-iteration order while `hash_components()` hashes them in list order, so whether the assertion | ||
| passes depends on `PYTHONHASHSEED`. |
Collaborator
There was a problem hiding this comment.
though also good to check which tests are leaking data / affecting the other tests. could potentially indicate a real world user interaction sequence issue.
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.
Alternative to #481, rebuilt on current
dev. Same diagnosis, deeper fix.Closes #472.
The bug is real and reachable
Clearing a score timeline and then letting playback run for one tick reopens the viewer with the score that was just discarded:
on_audio_time_changereadsScoreTimelineUI.svg_view, whose getter constructs anSvgViewerwhen none is registered, loadssvg_datainto it and shows it. InstrumentingSvgViewer.showconfirms one call per clear-plus-tick ondev. The score also survived into the saved file, so reopening a cleared timeline brought it back.Why not #481 as it stands
#481 guards
reset_svgalone. That leaves the auto-creating property in place at five other call sites, and it introduces a state (svg_data == ""on a live timeline) that the rest of the code cannot handle:svg_datablanked on clear, restoring a cleared state pushes""through the update trigger intoSvgViewer.load_svg_data, which hands it toetree.fromstring—XMLSyntaxError: Document is empty.Getmember, so with two score timelines the viewer of one answers for the other's id and returnsNonerather than raising.except NoReplyToRequestdoes not cover that;reset_svgraisesAttributeError.on_audio_time_changehad the same latent crash.reset_svgdeletes it, and the next playback tick rebuilds it through the property. It stays invisible only becausesvg_datahappens to be empty by then, so the two halves of that PR are load-bearing together rather than independently correct.What this does instead
528eb264—load_svg_datatreated onlysuccess=Falsefromset_dataas a failure, butviewer_beat_xusesvalidate_pre_validated, so the call always succeeds andSCORE_SVG_CREATE_ERRORwas unreachable. A render without usable beat markers silently produced an empty mapping. Check the returned mapping too.6d391c06—svg_viewno longer constructs anything; it returnsNonewhen there is no viewer, and the explicitget_or_create_svg_view()handles creation. All six read sites updated, plusScoreAnnotationUI.svg_view, which keeps the creating behaviour it relies on. This is what actually stops the resurrection, and it fixes theNone-return crash at the same time.9a13afb9— the fix proper.ScoreTLComponentManager.cleardiscardssvg_data(aftersuper().clear(), so the deletion loop still sees the score it is clearing), andupdate_svg_datacloses the viewer instead of loading when there is no score left.load_svg_datarefuses empty data outright.5dd1a7fc— docs; see below.b106beac—_setup_svg_viewnow goes throughget_or_create_svg_view, which sets the measure tracker's visibility from the viewer instead of callingshow()unconditionally. Test pins the behaviour that must be preserved.Tests
16 added, covering: the viewer closing on clear and staying closed through playback,
svg_databeing discarded, restoring a cleared state, undo/redo, save-and-reload,reset_svgwith no viewer and with another timeline owning the slot, and the revived beat-position error. Each fails without its corresponding change.Full suite: 1847 passed, 0 failed.
Two pre-existing bugs found on the way, left for their own PRs
viewer_beat_xis never cleared, so a score imported into a timeline that already had one is laid out against the previous score's beat positions. Clearing it here is not safe yet:_restore_timeline_staterestoressvg_databeforeviewer_beat_x, so an undo would reload the already-marker-stripped SVG while the mapping is empty and recomputation yields nothing.components_hashis nondeterministic across undo.TimelineComponentManager.restore_staterecreates components in set-iteration order whilehash_components()hashes them in list order, so a restored state's hash depends onPYTHONHASHSEED(fails on seeds 0, 1, 4; passes on 2, 3, 5 — same on cleandev). That makesundoable()unusable on any timeline with several components and can mark an unmodified file as modified. The undo/redo test here asserts restored values directly and carries a comment pointing at this.Unrelated: parallel test runs
pytest-xdistwas already a dev dependency but nothing documented it, so the documented way to run the suite takes about eight minutes instead of one and a half.5dd1a7fcaddspytest -n autoto CLAUDE.md, along with the two intermittent-failure sources that look like regressions but are not — cross-test interference under-n auto, and thePYTHONHASHSEEDdependency above.