Skip to content

Conversation

@JOJ0
Copy link
Member

@JOJ0 JOJ0 commented Dec 5, 2025

Catches 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

To Do

  • Documentation.
  • Changelog.
  • Tests.

Catches 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
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.

Comment on lines +42 to +48
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
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

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

Catching bare AssertionError is problematic because:

  1. It's too broad and could mask other assertion failures unrelated to the skip condition
  2. It relies on implementation details of imported_items()
  3. It doesn't explicitly check the actual condition (task being skipped)

A better approach would be to check if the task should be skipped before calling imported_items():

def prevent_suggest_removal(self, session, task):
    if task.skip:
        return
    for item in task.imported_items():
        if "mb_albumid" in item:
            self.stop_suggestions_for_albums.add(item.mb_albumid)

This is more explicit, follows the same pattern used in other stages (e.g., plugin_stage in beets/importer/stages.py), and doesn't catch unexpected assertion failures.

Suggested change
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
if getattr(task, "skip", False):
return
for item in task.imported_items():
if "mb_albumid" in item:
self.stop_suggestions_for_albums.add(item.mb_albumid)

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

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

This looks promision. Should I try it this way?

@codecov
Copy link

codecov bot commented Dec 5, 2025

Codecov Report

❌ Patch coverage is 50.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.92%. Comparing base (2bd77b9) to head (4f95a2f).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/importsource.py 50.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6203      +/-   ##
==========================================
- Coverage   67.93%   67.92%   -0.01%     
==========================================
  Files         137      137              
  Lines       18677    18680       +3     
  Branches     3155     3155              
==========================================
+ Hits        12688    12689       +1     
- Misses       5324     5326       +2     
  Partials      665      665              
Files with missing lines Coverage Δ
beetsplug/importsource.py 59.42% <50.00%> (-1.19%) ⬇️
🚀 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?

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.

4 participants