Skip to content

Conversation

@snejus
Copy link
Member

@snejus snejus commented Jan 5, 2026

  • Update configuration handling to use fully typed confuse API which will be released in confuse v2.2.0.
  • Use Subview, .sequence(), MappingTemplate, and typed OneOf.
  • Replace 'naked' configuration dictionary access with typed .get/.as_* equivalents.
  • Add typing annotations and cached_property where appropriate.
  • Fix related issues in discogs, fetchart, lyrics, playlist, smartplaylist, and titlecase plugins.

Important

Depends on beetbox/confuse#187 being merged and released (as v2.2.0)

Copilot AI review requested due to automatic review settings January 5, 2026 12:57
@snejus snejus requested review from JOJ0 and semohr January 5, 2026 12:57
@snejus snejus requested a review from a team as a code owner January 5, 2026 12:57
@github-actions
Copy link

github-actions bot commented Jan 5, 2026

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 - I've found 3 issues, and left some high level feedback:

  • In DiscogsPlugin.get_album_info, the new genre construction now requires base_genre to be truthy before appending style, which changes behavior when only a style is present; if this change is not intentional, consider restoring the ability to use style alone when no genre is set.
  • The relative_to: bytes | None attribute added to PlaylistPlugin is currently unused; either wire it into the plugin behavior or remove it to avoid confusion about its purpose.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `DiscogsPlugin.get_album_info`, the new `genre` construction now requires `base_genre` to be truthy before appending `style`, which changes behavior when only a style is present; if this change is not intentional, consider restoring the ability to use `style` alone when no genre is set.
- The `relative_to: bytes | None` attribute added to `PlaylistPlugin` is currently unused; either wire it into the plugin behavior or remove it to avoid confusion about its purpose.

## Individual Comments

### Comment 1
<location> `beetsplug/lastgenre/__init__.py:181-185` </location>
<code_context>
-        if source == "artist":
-            return ("artist",)
-        return tuple()
+        return self.config["source"].as_choice(
+            {
+                "track": ("track", "album", "artist"),
+                "album": ("album", "artist"),
+                "artist": ("artist",),
+            }
+        )
</code_context>

<issue_to_address>
**issue (bug_risk):** The use of `as_choice` with a dict here likely returns a single string, not the tuple of sources you expect.

In `confuse`, `as_choice` treats a dict as `{canonical: [aliases...]}` and returns the *key*, not the value. Here that means you’ll only ever get `'track'`, `'album'`, or `'artist'`, never the tuples. The alias lists also mean e.g. `'album'` in the config might be treated as an alias for `'track'`. This changes the previous behaviour and is likely a bug. To map a single configured value to a tuple of sources, use `as_choice` over a simple list of strings and then map that string to the tuple in Python, or add a small helper to do that mapping explicitly.
</issue_to_address>

### Comment 2
<location> `beetsplug/discogs.py:429-431` </location>
<code_context>
-        if self.config["append_style_genre"] and style:
-            genre = self.config["separator"].as_str().join([base_genre, style])
-        else:
-            genre = base_genre
+        genre = base_genre
+        if self.config["append_style_genre"] and genre and style:
+            genre += f"{self.separator}{style}"

</code_context>

<issue_to_address>
**question:** Style is no longer used when `base_genre` is empty, which changes previous behaviour.

With the old logic, when `append_style_genre` was true and `style` was set, an empty `base_genre` still produced a value like `';Rock'`. With the new logic, the same case now yields an empty `genre`, dropping `style` entirely. If `style` should act as a fallback when `base_genre` is missing, consider handling `not base_genre and style` explicitly (e.g., `genre = style`) rather than requiring `genre` to be truthy before appending.
</issue_to_address>

### Comment 3
<location> `beetsplug/fetchart.py:289-290` </location>
<code_context>
         elif check == ImageAction.DEINTERLACE:
             self.path = ArtResizer.shared.deinterlace(self.path)
         elif check == ImageAction.REFORMAT:
+            # TODO: fix this gnarly logic to remove the need for assert
+            assert plugin.cover_format is not None
             self.path = ArtResizer.shared.reformat(
                 self.path,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Relying on an `assert` for `cover_format` being non-None can surface as a user-facing AssertionError.

Using `assert plugin.cover_format is not None` makes an unexpected configuration surface as a generic AssertionError and can be stripped when Python is run with `-O`, potentially bypassing this check entirely. If `cover_format` being `None` is invalid here, prefer raising an explicit, user-facing exception (e.g., `UserError` or a custom error) with a clear message instead of an `assert`.

Suggested implementation:

```python
        elif check == ImageAction.REFORMAT:
            # TODO: fix this gnarly logic to remove the need for this check
            if plugin.cover_format is None:
                # This should not happen in normal operation; surface as a clear user-facing error.
                # Prefer an explicit exception over an assert so it is not stripped with -O and
                # produces a helpful message instead of a bare AssertionError.
                raise ui.UserError(
                    "fetchart: cover_format is unexpectedly unset while attempting to "
                    "reformat album art. Please report this issue and include your "
                    "fetchart configuration."
                )
            self.path = ArtResizer.shared.reformat(
                self.path,
                plugin.cover_format,

```

If `ui` is not already imported in `beetsplug/fetchart.py`, add:

- `from beets import ui`

near the other import statements at the top of the file.

If your codebase prefers a different exception type for plugin/user-facing errors (e.g., `plugins.PluginError`), you can replace `ui.UserError` with that type and adjust the import accordingly.
</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.

Comment on lines +181 to +185
return self.config["source"].as_choice(
{
"track": ("track", "album", "artist"),
"album": ("album", "artist"),
"artist": ("artist",),
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): The use of as_choice with a dict here likely returns a single string, not the tuple of sources you expect.

In confuse, as_choice treats a dict as {canonical: [aliases...]} and returns the key, not the value. Here that means you’ll only ever get 'track', 'album', or 'artist', never the tuples. The alias lists also mean e.g. 'album' in the config might be treated as an alias for 'track'. This changes the previous behaviour and is likely a bug. To map a single configured value to a tuple of sources, use as_choice over a simple list of strings and then map that string to the tuple in Python, or add a small helper to do that mapping explicitly.

@snejus snejus force-pushed the adjust-types-for-fully-typed-confuse branch from 0f6899d to a5b581c Compare January 5, 2026 12:59
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 updates the beets codebase to use the fully typed confuse API that will be released in confuse v2.2.0. The changes modernize configuration handling with better type safety while maintaining backward compatibility.

Key changes:

  • Replaces ConfigView with Subview and adopts typed configuration access methods (.get(), .as_*())
  • Uses MappingTemplate and typed OneOf for validation at the template level
  • Replaces dictionary-style config access with type-safe alternatives like .sequence() and as_choice() with dict mapping
  • Adds type annotations and cached_property decorators for improved type checking

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
beets/init.py Added type annotations to read() method and replaced iteration over config with .sequence() call for include files
beets/plugins.py Updated import and type annotation from ConfigView to Subview for better typing
beets/ui/init.py Refactored color configuration to use MappingTemplate and typed OneOf, moving validation from runtime checks to template-level validation
beetsplug/discogs.py Added cached_property for separator config, updated config access to use .get(bool), and refactored genre string construction
beetsplug/fetchart.py Added type parameter to OneOf and added defensive assert with TODO comment for type checking
beetsplug/lastgenre/init.py Refactored as_choice() to use dict mapping for cleaner code, updated import signatures with proper types
beetsplug/lyrics.py Changed config access to use .get(bool) for synced lyrics option
beetsplug/playlist.py Renamed variable for clarity and added type annotation for instance variable
beetsplug/smartplaylist.py Refactored to combine as_filename() and bytestring_path() calls into single expression
beetsplug/titlecase.py Added default_value parameter to as_pairs() call

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

@snejus snejus force-pushed the adjust-types-for-fully-typed-confuse branch 2 times, most recently from ecb5c72 to 991dd48 Compare January 5, 2026 13:30
* Update configuration handling to use fully typed confuse API which
  will be released in `v2.2.0`.
* Use Subview, .sequence(), MappingTemplate, and typed OneOf.
* Replace 'naked' configuration dictionary access with typed .get/.as_*
  equivalents.
* Add typing annotations and cached_property where appropriate. Fix
  related issues in Discogs, fetchart, lyrics, playlist, smartplaylist,
  and titlecase plugins.
@snejus snejus force-pushed the adjust-types-for-fully-typed-confuse branch from 991dd48 to 46cc9ba Compare January 5, 2026 13:50
@snejus snejus marked this pull request as draft January 7, 2026 23:27
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.

2 participants