-
Notifications
You must be signed in to change notification settings - Fork 2k
Adjust types for fully typed confuse #6268
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
base: master
Are you sure you want to change the base?
Conversation
|
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. |
There was a problem hiding this 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 newgenreconstruction now requiresbase_genreto be truthy before appendingstyle, which changes behavior when only a style is present; if this change is not intentional, consider restoring the ability to usestylealone when no genre is set. - The
relative_to: bytes | Noneattribute added toPlaylistPluginis 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| return self.config["source"].as_choice( | ||
| { | ||
| "track": ("track", "album", "artist"), | ||
| "album": ("album", "artist"), | ||
| "artist": ("artist",), |
There was a problem hiding this comment.
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.
0f6899d to
a5b581c
Compare
There was a problem hiding this 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
ConfigViewwithSubviewand adopts typed configuration access methods (.get(),.as_*()) - Uses
MappingTemplateand typedOneOffor validation at the template level - Replaces dictionary-style config access with type-safe alternatives like
.sequence()andas_choice()with dict mapping - Adds type annotations and
cached_propertydecorators 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.
ecb5c72 to
991dd48
Compare
* 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.
991dd48 to
46cc9ba
Compare
v2.2.0.Subview,.sequence(),MappingTemplate, and typedOneOf..get/.as_*equivalents.cached_propertywhere appropriate.discogs,fetchart,lyrics,playlist,smartplaylist, andtitlecaseplugins.Important
Depends on beetbox/confuse#187 being merged and released (as
v2.2.0)