Skip to content

Conversation

@JOJ0
Copy link
Member

@JOJ0 JOJ0 commented Dec 5, 2025

Prevents a crash when "skip" is selected in the importer and task.imported_items() runs into a condition branch that supposedly should never be reached:

  File "beets/beets/importer/tasks.py", line 254, in imported_items
    assert False
           ^^^^^
AssertionError
  • Since for items/albums that should be skipped, looping through task.imported_items() is not required anyway, the fix here is to exit early from the function that calls it.
  • Additionally this PR fixes the original changelog entry which was located at an older releases "new features list". Also now it briefly explains to changelog readers what the plugin actually does.
  • Two new tests were added that proof that "skip doesn't crash" and reimports never "suggest removal of source files"

To Do

  • Documentation.
  • Changelog (Fixed original changelog entry since it was at wrong release position).
  • Tests.

Copilot AI review requested due to automatic review settings December 5, 2025 16:09
@JOJ0 JOJ0 requested a review from a team as a code owner December 5, 2025 16:09
@github-actions
Copy link

github-actions bot commented Dec 5, 2025

Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • Catching a bare AssertionError here risks hiding unrelated bugs in imported_items; consider guarding this logic based on task state (e.g., task choice/skip status) or checking for the presence of imported items instead of relying on the assertion.
  • Swallowing the AssertionError silently makes debugging harder if this path is hit unexpectedly; it would be helpful to at least log a debug message when the exception is caught so that it’s visible during troubleshooting.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Catching a bare AssertionError here risks hiding unrelated bugs in imported_items; consider guarding this logic based on task state (e.g., task choice/skip status) or checking for the presence of imported items instead of relying on the assertion.
- Swallowing the AssertionError silently makes debugging harder if this path is hit unexpectedly; it would be helpful to at least log a debug message when the exception is caught so that it’s visible during troubleshooting.

## Individual Comments

### Comment 1
<location> `beetsplug/importsource.py:42-47` </location>
<code_context>
-        for item in task.imported_items():
-            if "mb_albumid" in item:
-                self.stop_suggestions_for_albums.add(item.mb_albumid)
+        try:
+            for item in task.imported_items():
+                if "mb_albumid" in item:
+                    self.stop_suggestions_for_albums.add(item.mb_albumid)
+        except AssertionError:
+            # No imported items - nothing to do
+            pass

</code_context>

<issue_to_address>
**issue (bug_risk):** Catching AssertionError around the whole loop risks masking unrelated bugs.

Because the `try`/`except AssertionError` wraps the whole loop, any assertion triggered inside `task.imported_items()`, the membership check, or `add()` will be swallowed, not just the “no imported items” case. Please limit the `try` to the precise call that can raise for the empty-items condition (e.g. only `task.imported_items()`) or, if possible, detect “no imported items” without relying on catching `AssertionError`, so real programming errors are not suppressed.
</issue_to_address>

### Comment 2
<location> `beetsplug/importsource.py:46-48` </location>
<code_context>
+            for item in task.imported_items():
+                if "mb_albumid" in item:
+                    self.stop_suggestions_for_albums.add(item.mb_albumid)
+        except AssertionError:
+            # No imported items - nothing to do
+            pass

     def import_stage(self, _, task):
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Silently ignoring AssertionError may hinder debugging when unexpected conditions occur.

Catching `AssertionError` and doing nothing can mask genuine issues (e.g., unexpected `task`/`item` state). If the "no imported items" scenario is expected, consider narrowing the condition (e.g., checking for that case explicitly) or at least logging the exception at debug level so other assertion failures are visible.

Suggested implementation:

```python
    def prevent_suggest_removal(self, session, task):
        try:
            for item in task.imported_items():
                if "mb_albumid" in item:
                    self.stop_suggestions_for_albums.add(item.mb_albumid)
        except AssertionError as exc:
            # No imported items - nothing to do; log in case this masks a real issue.
            log.debug(
                "AssertionError while processing imported items for task %r: %s",
                task,
                exc,
                exc_info=True,
            )

```

If `log` is not already defined in `beetsplug/importsource.py`, you should:
1. Import the logging facility used by other beets plugins (for example, `from beets import logging`).
2. Initialize a module-level logger consistent with the rest of the codebase, e.g. `log = logging.getLogger(__name__)` or whatever convention other plugins in this project follow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a crash in the importsource plugin when the user selects "skip" during import, which previously caused an AssertionError when task.imported_items() was called with an unsupported action.

Key Changes:

  • Added try-except block around task.imported_items() call in prevent_suggest_removal method to catch the AssertionError when tasks are skipped

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codecov
Copy link

codecov bot commented Dec 5, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.20%. Comparing base (c1904b1) to head (9ffae4b).
⚠️ Report is 4 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6203      +/-   ##
==========================================
- Coverage   68.36%   68.20%   -0.17%     
==========================================
  Files         138      138              
  Lines       18773    18775       +2     
  Branches     3172     3173       +1     
==========================================
- Hits        12834    12805      -29     
- Misses       5263     5296      +33     
+ Partials      676      674       -2     
Files with missing lines Coverage Δ
beetsplug/importsource.py 69.11% <100.00%> (+8.51%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JOJ0
Copy link
Member Author

JOJ0 commented Dec 5, 2025

I'm sure this is not the right way to fix this. Some eyes here @semohr or @snejus.

Here we assert False, I suppose because this should never happen right?

def imported_items(self):
"""Return a list of Items that should be added to the library.
If the tasks applies an album match the method only returns the
matched items.
"""
if self.choice_flag in (Action.ASIS, Action.RETAG):
return list(self.items)
elif self.choice_flag == Action.APPLY and isinstance(
self.match, autotag.AlbumMatch
):
return list(self.match.mapping.keys())
else:
assert False

@Serene-Arc
Copy link
Contributor

Personally, I don't think we should have any assert False statements in the codebase at all, so we should phase them out when necessary. There are so many better ways to handle this. Clearly it is happening, so that assumption is false.

I think we should either raise a dedicated exception with debugging details so we can identify which assumption is false, or else have that function return a None or empty list and the calling functions can deal with it. But having the program crash is the worst thing for a user facing tool.

@semohr
Copy link
Contributor

semohr commented Dec 7, 2025

@JOJ0 Do you have the full trace-back here? How do we even run into this? This is a quite old part of the codebase, how was this never an issue beforehand?

Personally, I don't think we should have any assert False statements in the codebase at all, so we should phase them out when necessary. There are so many better ways to handle this. Clearly it is happening, so that assumption is false.

I think we should either raise a dedicated exception with debugging details so we can identify which assumption is false, or else have that function return a None or empty list and the calling functions can deal with it. But having the program crash is the worst thing for a user facing tool.

I agree fully here but I would like to figure out how and why we are now running into this issue now. We did not run into this for the previous 12 years afaik. (Empty list or none makes sense in my opinion)

@JOJ0
Copy link
Member Author

JOJ0 commented Dec 8, 2025

@JOJ0 Do you have the full trace-back here? How do we even run into this? This is a quite old part of the codebase, how was this never an issue beforehand?

Personally, I don't think we should have any assert False statements in the codebase at all, so we should phase them out when necessary. There are so many better ways to handle this. Clearly it is happening, so that assumption is false.

I think we should either raise a dedicated exception with debugging details so we can identify which assumption is false, or else have that function return a None or empty list and the calling functions can deal with it. But having the program crash is the worst thing for a user facing tool.

I agree fully here but I would like to figure out how and why we are now running into this issue now. We did not run into this for the previous 12 years afaik. (Empty list or none makes sense in my opinion)

Sure. Here it is:

$ beet import --copy -t ~/Music/import-devbeets/

/Users/jojo/Music/import-devbeets (1 items)

  Match (42.8%):
  Dr. Ring-Ding & The Senior Allstars - Dandimite!
  ≠ missing tracks, id, data source, tracks
  Discogs, Vinyl, 1995, Germany, Grover Records, GRO-LP 004, None
  https://www.discogs.com/release/675198-Dr-Ring-Ding-The-Senior-Allstars-Dandimite
  * Artist: Dr. Ring-Ding & The Senior Allstars
  * Album: Dandimite!
     * (#2) Dandimite Ska (3:58)
Missing tracks (13/14 - 92.9%):
 ! Phone Talk (#1) (0:47)
 ! Big Man (#3) (3:31)
 ! Medley: Save A Bread / Save A Toast (#4) (4:42)
 ! (Want Me) Money Back (#5) (3:25)
 ! Got My Boogaloo (#6) (3:28)
 ! Bellevue Asylum (#7) (5:33)
 ! What A Day (#8) (2:03)
 ! Latin Goes Ska (#9) (3:41)
 ! Rudeboy Style (#10) (5:39)
 ! Stay Out Late (#11) (2:38)
 ! Knocking On My Door (#12) (4:12)
 ! One Scotch, One Bourbon, One Beer (#13) (4:03)
 ! Gloria (#14) (3:42)
➜ [A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums,
Enter search, enter Id, aBort, eDit, edit Candidates, plaY? s
Traceback (most recent call last):
  File "/Users/jojo/.pyenv/versions/jtb311/bin/beet", line 6, in <module>
    sys.exit(main())
             ^^^^^^
  File "/Users/jojo/git/beets/beets/ui/__init__.py", line 1631, in main
    _raw_main(args)
  File "/Users/jojo/git/beets/beets/ui/__init__.py", line 1610, in _raw_main
    subcommand.func(lib, suboptions, subargs)
  File "/Users/jojo/git/beets/beets/ui/commands/import_/__init__.py", line 131, in import_func
    import_files(lib, byte_paths, query)
  File "/Users/jojo/git/beets/beets/ui/commands/import_/__init__.py", line 75, in import_files
    session.run()
  File "/Users/jojo/git/beets/beets/importer/session.py", line 236, in run
    pl.run_parallel(QUEUE_SIZE)
  File "/Users/jojo/git/beets/beets/util/pipeline.py", line 471, in run_parallel
    raise exc_info[1].with_traceback(exc_info[2])
  File "/Users/jojo/git/beets/beets/util/pipeline.py", line 336, in run
    out = self.coro.send(msg)
          ^^^^^^^^^^^^^^^^^^^
  File "/Users/jojo/git/beets/beets/util/pipeline.py", line 195, in coro
    task = func(*(args + (task,)))
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jojo/git/beets/beets/importer/stages.py", line 171, in user_query
    plugins.send("import_task_choice", session=session, task=task)
  File "/Users/jojo/git/beets/beets/plugins.py", line 646, in send
    return [
           ^
  File "/Users/jojo/git/beets/beets/plugins.py", line 649, in <listcomp>
    if (r := handler(**arguments)) is not None
             ^^^^^^^^^^^^^^^^^^^^
  File "/Users/jojo/git/beets/beets/plugins.py", line 333, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jojo/git/beets/beetsplug/importsource.py", line 42, in prevent_suggest_removal
    for item in task.imported_items():
                ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jojo/git/beets/beets/importer/tasks.py", line 254, in imported_items
    assert False
           ^^^^^
AssertionError
$

@JOJ0
Copy link
Member Author

JOJ0 commented Dec 8, 2025

There might be some design-flaws that slipped through my initial review of this plugin. One thing is that the listener here is of no use if the configuration of the plugin does not want to use that feature anyway:

https://github.com/beetbox/beets/pull/6203/files#diff-2f4f7f8754e928450b8b006758b96ce931b391f184d586592fe7bfc499c27eedR37-R41

But still if we'd prevent registering the listener when not configured, it does not cure the problem that task.imported_items() can't be called safely. Even though it might be "the wrong moment" trying to ask for imported items and trying to loop through them, I agree that it would be an easy and elegant fix to simply return an empty list. That would fix the error at hand and also prevent any sanity checks on the callers end.

Should I try to include an "empty list or none fix" as you both suggested within this PR?

@semohr
Copy link
Contributor

semohr commented Dec 10, 2025

Have you seen #6211? Would that approach also fully fix this issue?

@JOJ0
Copy link
Member Author

JOJ0 commented Dec 10, 2025

Have you seen #6211? Would that approach also fully fix this issue?

Yes definitely. Closed that one already. It's definitely correct to simply "not do prevent suggest stuff" if skip is chosen anyway.

But I thought we wanted to maybe change the assert false to allow empty list/none in the importer code? Or better not and leave it as is?

@JOJ0 JOJ0 force-pushed the importsource_fix_skip_crash branch from 4f95a2f to 2b902fa Compare December 10, 2025 22:33
@JOJ0
Copy link
Member Author

JOJ0 commented Dec 10, 2025

I applied the mentioned fix and rebased. Remains to be decided if we want to change things in the importer's imported_items() method or not @semohr @Serene-Arc

@JOJ0 JOJ0 force-pushed the importsource_fix_skip_crash branch from 7b8e4f0 to b5ffb8d Compare December 21, 2025 11:37
@JOJ0 JOJ0 changed the title importsource: Catch importer crash when skipping importsource: Catch importer crash when skipping; Fix original changelog entry Dec 21, 2025
@JOJ0 JOJ0 changed the title importsource: Catch importer crash when skipping; Fix original changelog entry importsource: Catch importer crash when skipping; Fix original changelog entry; Add new tests Dec 21, 2025
@JOJ0 JOJ0 force-pushed the importsource_fix_skip_crash branch from b015fdb to 1e6a6ba Compare December 21, 2025 12:03
@JOJ0 JOJ0 force-pushed the importsource_fix_skip_crash branch from 1e6a6ba to 9ffae4b Compare December 21, 2025 12:07
@JOJ0
Copy link
Member Author

JOJ0 commented Dec 21, 2025

I applied the mentioned fix and rebased. Remains to be decided if we want to change things in the importer's imported_items() method or not @semohr @Serene-Arc

For now I would like to move on with getting this crash fixed and want to make the call here: Let's not touch the importer logic for now and move that decision to another time. Fixing this crash is my priority.

See my updated PR description bulletpoints. I fixed the original changelog and added two IMO essential test cases.

I'd like to request a final look here @semohr @Serene-Arc

Thanks!

Copy link
Contributor

@semohr semohr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a good way forward to me!

@JOJ0 JOJ0 merged commit 5d1210a into master Dec 21, 2025
21 checks passed
@JOJ0 JOJ0 deleted the importsource_fix_skip_crash branch December 21, 2025 19:35
@doronbehar
Copy link
Contributor

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants