Skip to content

Conversation

semohr
Copy link
Contributor

@semohr semohr commented Aug 26, 2025

Description

When a metadata plugin raises an exception during the auto-tagger process, the entire operation crashes. This behavior is not desirable, since metadata lookups can legitimately fail for various reasons (e.g., temporary API downtime, network issues, or offline usage).

This PR introduces a safeguard by adding general exception handling around metadata plugin calls. Instead of causing the whole process to fail, exceptions from individual plugins are now caught and logged. This ensures that the auto-tagger continues to function with the remaining available metadata sources.

This replaces the efforts from #5910

Limitations

Sometimes one might not want to continue if an exception is encountered a metadata plugin. Do we need/want a config option to raise if an issue is encountered?

  • Changelog.
  • Tests.

Summary by Sourcery

Wrap all metadata plugin calls in safe handlers to prevent individual plugin failures from aborting the auto-tagging process, update plugin interfaces, add comprehensive error logging, and include tests and a changelog entry.

Bug Fixes:

  • Prevent metadata plugin exceptions from crashing the auto-tagger by catching and logging errors.

Enhancements:

  • Introduce safe_call and safe_yield_from helpers to isolate plugin errors and allow continued processing.
  • Refine plugin interface method signatures to use Iterable types and rename generic type variables.
  • Log plugin exception details at error and debug levels with class context.

Documentation:

  • Add changelog entry for metadata plugin exception handling.

Tests:

  • Add tests to verify that metadata plugin exceptions are caught and logged for all relevant plugin methods.

This comment was marked as resolved.

Copy link

codecov bot commented Aug 26, 2025

Codecov Report

❌ Patch coverage is 96.77419% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.54%. Comparing base (f24beca) to head (7cb1b4c).
⚠️ Report is 33 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beets/metadata_plugins.py 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5965      +/-   ##
==========================================
+ Coverage   66.47%   66.54%   +0.06%     
==========================================
  Files         117      117              
  Lines       18105    18124      +19     
  Branches     3071     3073       +2     
==========================================
+ Hits        12036    12061      +25     
+ Misses       5414     5408       -6     
  Partials      655      655              
Files with missing lines Coverage Δ
beets/metadata_plugins.py 84.10% <96.77%> (+6.83%) ⬆️

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

@semohr semohr added this to the 2.4.1 milestone Aug 26, 2025
@semohr
Copy link
Contributor Author

semohr commented Aug 26, 2025

Related:

@semohr semohr force-pushed the error_handling_metadata_plugins branch from ad7319f to cbf389c Compare September 9, 2025 13:18
@semohr semohr force-pushed the error_handling_metadata_plugins branch from cbf389c to 88741c3 Compare September 9, 2025 13:19
@semohr semohr marked this pull request as ready for review September 9, 2025 13:39
@Copilot Copilot AI review requested due to automatic review settings September 9, 2025 13:39
@semohr semohr requested a review from a team as a code owner September 9, 2025 13:39
Copy link
Contributor

@Copilot 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 adds exception handling to metadata plugin operations to prevent crashes during the auto-tagger process. When individual metadata plugins encounter errors, the system will now log the issues and continue processing with other available metadata sources instead of failing completely.

  • Introduces safe wrapper functions (_safe_call and _safe_yield_from) around metadata plugin method calls
  • Updates all plugin interface methods to use exception handling
  • Adds comprehensive test coverage for error scenarios in metadata plugins

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
beets/metadata_plugins.py Core implementation of exception handling with safe wrapper functions and updated plugin interface calls
test/test_metadata_plugins.py New test file with mock error plugin and comprehensive test coverage for all metadata plugin methods
docs/changelog.rst Documentation of the bug fix in the changelog

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

sourcery-ai bot commented Sep 9, 2025

Reviewer's Guide

This PR implements robust exception handling around metadata plugin calls by introducing safe-call and safe-yield helpers with logging, updating all plugin invocations to use these wrappers, refining related type signatures, and adding tests and a changelog entry to validate and document the new behavior.

Flow diagram for exception handling in plugin calls

flowchart TD
A["AutoTagger calls plugin method"] --> B{Exception raised?}
B -- Yes --> C["Log error"]
C --> D["Continue to next plugin"]
B -- No --> E["Process plugin result"]
E --> D
Loading

File-Level Changes

Change Details Files
Introduce safe-call and safe-yield wrappers for plugin invocations
  • Add global logger instantiation
  • Define _safe_call, _safe_yield_from, and _class_name_from_method helpers
  • Wrap all metadata plugin calls (candidates, item_candidates, album_for_id, track_for_id, track_distance, album_distance) with the new wrappers
beets/metadata_plugins.py
Refine function signatures and generics for metadata methods
  • Import ParamSpec and TypeVar and reorganize autorestag imports
  • Change candidates return type to Iterator
  • Switch batch lookup parameters from Sequence to Iterable
  • Rename generic type parameter from R to Res for IDResponse
beets/metadata_plugins.py
Update changelog
  • Add entry describing the new exception-handling behavior
docs/changelog.rst
Add tests for plugin exception handling
  • Create test_metadata_plugins.py with a mock plugin that raises errors
  • Verify that errors are caught and logged without crashing the process
test/test_metadata_plugins.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

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:

  • The log.error calls use curly-brace placeholders but Python’s logging module expects %-style formatting or preformatted strings—update them to use %s or f-strings so the plugin name and exception actually interpolate.
  • As mentioned in the PR limitations, consider adding a config flag to let users opt in to failing on plugin exceptions instead of always swallowing errors.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The log.error calls use curly-brace placeholders but Python’s logging module expects %-style formatting or preformatted strings—update them to use %s or f-strings so the plugin name and exception actually interpolate.
- As mentioned in the PR limitations, consider adding a config flag to let users opt in to failing on plugin exceptions instead of always swallowing errors.

## Individual Comments

### Comment 1
<location> `test/test_metadata_plugins.py:63` </location>
<code_context>
+        with caplog.at_level("ERROR"):
+            # Call the method to trigger the error
+            ret = getattr(metadata_plugins, method_name)(*args)
+            if isinstance(ret, Iterable):
+                list(ret)
+
</code_context>

<issue_to_address>
Test does not verify that normal (non-error) plugins still work alongside error-raising plugins.

Add a test that registers both an error-raising and a normal plugin to confirm that exceptions in one do not affect the results from others.
</issue_to_address>

### Comment 2
<location> `test/test_metadata_plugins.py:67` </location>
<code_context>
+                list(ret)
+
+            # Check that an error was logged
+            assert len(caplog.records) == 1
+            logs = [record.getMessage() for record in caplog.records]
+            assert logs == ["Error in 'ErrorMetadataMockPlugin': Mocked error"]
</code_context>

<issue_to_address>
Test only checks for error-level logs, but does not verify that debug-level logs (with exception details) are emitted.

Please add assertions to check that debug-level logs with exception details are present, ensuring complete error context is captured.
</issue_to_address>

### Comment 3
<location> `test/test_metadata_plugins.py:69` </location>
<code_context>
+            # Check that an error was logged
+            assert len(caplog.records) == 1
+            logs = [record.getMessage() for record in caplog.records]
+            assert logs == ["Error in 'ErrorMetadataMockPlugin': Mocked error"]
+            caplog.clear()
</code_context>

<issue_to_address>
Test only checks for a single error log per method call, but does not verify that repeated calls do not accumulate unexpected logs.

Add a test to confirm that multiple calls to the method do not produce extra or unexpected log entries.
</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.

@jackwilsdon
Copy link
Member

jackwilsdon commented Sep 10, 2025

It does feel like we should pause the import until the user has acknowledged the issue (maybe providing options to skip importing the album or continue with existing metadata?), as otherwise you could end up with missing metadata that is awkward to fix later if for example you had network issues or the provider was down.

Not sure how this should interact with --quiet and --quiet-fallback though.

@semohr
Copy link
Contributor Author

semohr commented Sep 10, 2025

A prompt makes sense in theory, but I'm really hesitant to add any user interaction because it breaks headless scripts, automation, and make monkey-patching way harder which is a big part of my beets use case. Instead, I'm leaning towards adding a simple config option that lets you tell the plugin to just fail hard if it runs into trouble, so your import would stop and you'd know right away.

On a practical level, I'm also not convinced that skipping these errors is a major issue in the first place. The plugin's job is to suggest candidates, and it's already robust in how it handles failure: if it can't find any candidates, it just doesn't add any, and the import continues with other plugins. Same goes for get_by_id, it's designed to return None, so the higher-level functions are already built to handle that. Imo the introduced behavior should be good enough for most users.

@semohr semohr added the core Pull requests that modify the beets core `beets` label Sep 16, 2025
@semohr semohr mentioned this pull request Oct 2, 2025
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Pull requests that modify the beets core `beets`
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants