Skip to content

plugins: ensure to log the offending plugin on instantiation failure #5739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

wisp3rwind
Copy link
Member

@wisp3rwind wisp3rwind commented Apr 17, 2025

Came up in #5701 (comment) where an error can't easily be traced back to a plugin.

(Draft because I want to avoid creating merge conflicts for #5701 before that is merged, and because I might a few additional improvements here.)

Copy link

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.

@wisp3rwind wisp3rwind force-pushed the plugin-traceback branch 5 times, most recently from bde4397 to 2129eb4 Compare April 20, 2025 08:43
@wisp3rwind wisp3rwind marked this pull request as ready for review April 20, 2025 09:01
Copy link

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.

@wisp3rwind
Copy link
Member Author

This should be ready to go now; it's easiest to review commit-by-commit.

minimize how much code is wrapped in a given try...except statement
…roken error handling

from a quick test, it appears that the old code shouldn't even be working
anymore due to additional quotes in exc.args[0]
also remove a unnecessary check before adding to a set
without an argument (i.e. the default `names=()`), this was a no-op
actual plugin loading is triggered by tests, or by the ui code

-> Turns out it's not an actual no-op in our testing code due to
   patching with attaching some side-effects to load_plugins...
   This is fixed up in the next commit.
Instead of the first call to find_plugins(). This could change behavior
slightly, since plugin load (and errors that happen in the process) could
occur earlier now. But:

- I'd argue this is better anyway, since plugin load errors now happen
  in the initial call to load_plugins() instead of somewhere in the
  middle of the first operation that uses plugins.
- In our ui code, both happen immediately after each other anyway:
  ui._setup calls
  	1) ui._load_plugins -> plugins.load_plugins
	2) plugins.types, plugins.named_queries, ...
_classes.add(obj)

except Exception:
mod = import_module(f".{name}", package=PLUGIN_NAMESPACE)
Copy link
Member

Choose a reason for hiding this comment

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

This may be slightly misleading in some cases, for example:

# beetsplug/discogs.py
...
import beets.ibrary
$ beet import -LI sel
...
** plugin discogs not found


except Exception:
mod = import_module(f".{name}", package=PLUGIN_NAMESPACE)
except ModuleNotFoundError:
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should catch a generic Exception here? Otherwise every other exception will make beets die, for example:

# beetsplug/discogs.py
...
abc
$ beet import -LI sel
...
Traceback (most recent call last):
  File "/home/sarunas/.local/bin/beet", line 5, in <module>
    main()
  File "/home/sarunas/repo/beets/beets/ui/__init__.py", line 1878, in main
    _raw_main(args)
  File "/home/sarunas/repo/beets/beets/ui/__init__.py", line 1853, in _raw_main
    subcommands, plugins, lib = _setup(options, lib)
  File "/home/sarunas/repo/beets/beets/ui/__init__.py", line 1680, in _setup
    plugins = _load_plugins(options, config)
  File "/home/sarunas/repo/beets/beets/ui/__init__.py", line 1669, in _load_plugins
    plugins.load_plugins(plugin_list)
  File "/home/sarunas/repo/beets/beets/plugins.py", line 358, in load_plugins
    mod = import_module(f".{name}", package=PLUGIN_NAMESPACE)
  File "/home/sarunas/.local/share/pyenv/versions/3.9.20/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/home/sarunas/repo/beets/beetsplug/discogs.py", line 32, in <module>
    abc
NameError: name 'abc' is not defined

Comment on lines +373 to +374
and cls != BeetsPlugin
and cls != MetadataSourcePlugin
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
and cls != BeetsPlugin
and cls != MetadataSourcePlugin
and cls not in {BeetsPlugin, MetadataSourcePlugin}

issubclass(cls, BeetsPlugin)
and cls != BeetsPlugin
and cls != MetadataSourcePlugin
and cls not in _instances
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to perform this check? It's unlikely we will come across dupes, and even if we do, classes is defined as a set, so no dupes are allowed.

Comment on lines +384 to +386
if cls in _instances:
return

Copy link
Member

Choose a reason for hiding this comment

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

I doubt this check is required

Suggested change
if cls in _instances:
return

and cls != MetadataSourcePlugin
and cls not in _instances
):
classes.add(cls)
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to store classes in this intermediate set? Can't we just _register_plugin here?

"""Returns a list of BeetsPlugin subclass instances from all
currently loaded beets plugins. Loads the default plugin set
first.
def find_plugins() -> Iterator[BeetsPlugin]:
Copy link
Member

Choose a reason for hiding this comment

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

Why are we returning an iterator here instead of the list?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants