Skip to content

Type musicbrainz#6329

Merged
snejus merged 4 commits intomasterfrom
type-musicbrainz
Apr 12, 2026
Merged

Type musicbrainz#6329
snejus merged 4 commits intomasterfrom
type-musicbrainz

Conversation

@snejus
Copy link
Copy Markdown
Member

@snejus snejus commented Feb 1, 2026

Add full type coverage to the MusicBrainz plugin

This PR introduces complete static typing for beetsplug/musicbrainz.py, beetsplug/mbpseudo.py, and beetsplug/_utils/musicbrainz.py, and enforces it via mypy strict mode.

Key changes

New TypedDict schema in beetsplug/_utils/musicbrainz.py

A comprehensive set of TypedDict classes now models the full MusicBrainz API response surface — Release, Recording, Track, ReleaseGroup, Artist, ArtistCredit, Work, and all relation types. Public API methods (get_release, get_recording, etc.) now return these typed shapes instead of the opaque JSONDict.

Key normalization: dash-to-underscore

The internal _normalize_data method (previously _group_relations) now also converts all hyphenated keys (e.g. artist-credit, release-group, sort-name) to underscored equivalents (e.g. artist_credit, release_group, sort_name) at parse time. This makes the data structure Python-idiomatic and is what allows the TypedDict definitions to use clean attribute names. All downstream field accesses, test fixtures, and JSON test resources are updated accordingly.

mypy strict mode enabled for the three affected modules via setup.cfg.

Impact

  • No behaviour change for end users — this is purely an internal refactor.
  • Code reading the MusicBrainz response now has IDE completion and type-checking support.
  • The normalization boundary is clearly established at _normalize_data, so callers never see raw hyphenated keys.
  • Test fixtures and resource JSON files are updated to match the new normalized shape.

@snejus snejus requested a review from asardaes as a code owner February 1, 2026 16:15
@snejus snejus requested review from JOJ0 and Copilot February 1, 2026 16:15
@snejus snejus requested a review from a team as a code owner February 1, 2026 16:15
@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 1, 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
Copy Markdown
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 introduces comprehensive typing for MusicBrainz API payloads through TypedDict models, refactors parsing logic into smaller helpers, normalizes API responses at the boundary (converting dashes to underscores and grouping relations), and replaces hand-written test dictionaries with factory-based data generation.

Changes:

  • Introduced typed domain models (Release, Recording, Medium, etc.) in beetsplug/_utils/musicbrainz.py
  • Normalized MusicBrainz API responses by converting dashed keys to underscores and grouping relations
  • Refactored parsing into focused helper methods (_parse_artist_credits, _parse_release_group, etc.)

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/rsrc/mbpseudo/pseudo_release.json Updated test fixture to use underscored keys matching normalized payload format
test/rsrc/mbpseudo/official_release.json Updated test fixture to use underscored keys matching normalized payload format
test/plugins/utils/test_musicbrainz.py Renamed test from test_group_relations to test_normalize_data reflecting the renamed internal method
test/plugins/test_musicbrainz.py Replaced manual test data construction with factory-based approach and updated assertions to match factory defaults
test/plugins/test_mbpseudo.py Updated references from dashed keys to underscored keys
test/plugins/factories/musicbrainz.py Added factory classes for generating typed MusicBrainz test data
setup.cfg Enabled strict mypy checking for musicbrainz-related modules
pyproject.toml Added pytest-factoryboy test dependency
beetsplug/musicbrainz.py Refactored parsing logic into typed helper methods and updated to consume normalized payloads
beetsplug/mbpseudo.py Updated to use typed Release structures and underscored keys
beetsplug/_utils/musicbrainz.py Added comprehensive TypedDict models and renamed/enhanced _group_relations to _normalize_data
Comments suppressed due to low confidence (2)

beetsplug/_utils/musicbrainz.py:1

  • The length checks are redundant. If map(int, date_str.split('-')) produces fewer than three parts, accessing parts[1] or parts[2] will raise an IndexError rather than returning None. The current implementation doesn't actually prevent the error. Consider using parts[i] if i < len(parts) else None or a more concise approach like padding the list.
"""Helpers for communicating with the MusicBrainz webservice.

beetsplug/musicbrainz.py:1

  • Corrected spelling of 'attribute_credits' to 'attribute_values' to match the actual field name in ArtistRelation TypedDict.
# This file is part of beets.

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

Comment thread test/plugins/factories/musicbrainz.py Outdated
Comment thread beetsplug/musicbrainz.py Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 1, 2026

Codecov Report

❌ Patch coverage is 99.56897% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 70.99%. Comparing base (c6409d2) to head (aab4dff).
⚠️ Report is 5 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
beetsplug/mbpseudo.py 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6329      +/-   ##
==========================================
+ Coverage   70.67%   70.99%   +0.31%     
==========================================
  Files         150      150              
  Lines       18918    19124     +206     
  Branches     3078     3078              
==========================================
+ Hits        13371    13577     +206     
  Misses       4896     4896              
  Partials      651      651              
Files with missing lines Coverage Δ
beetsplug/_utils/musicbrainz.py 97.29% <100.00%> (+4.38%) ⬆️
beetsplug/listenbrainz.py 53.84% <100.00%> (ø)
beetsplug/mbcollection.py 92.85% <100.00%> (ø)
beetsplug/parentwork.py 70.65% <100.00%> (ø)
beetsplug/mbpseudo.py 79.87% <83.33%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@amogus07
Copy link
Copy Markdown
Contributor

amogus07 commented Feb 2, 2026

I also did a similar sort of refactoring recently, and this is what I ended up with: https://github.com/prTopi/beets-vocadb/tree/eeb047027a54d331cfe1d54bc1126046c5382a9b/src/beetsplug/_utils/vocadb/vocadb_api_client

Feel free to take inpiration!

Here are some suggestions:

  1. TypedDicts are only referenced in type annotations, so they can all be put under if TYPE_CHECKING:
  2. Using StrEnums instead of Literals (or a custom Enum type like this that overrides auto()).
  3. Perhaps it's worth putting the models into their own module or even package.

Copy link
Copy Markdown
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

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Comment thread test/plugins/factories/musicbrainz.py Outdated
Comment thread test/plugins/test_musicbrainz.py Outdated
Comment thread test/plugins/factories/musicbrainz.py Outdated
Comment thread beetsplug/musicbrainz.py Outdated
Comment thread beetsplug/musicbrainz.py Outdated
Comment thread beetsplug/musicbrainz.py Outdated
@snejus snejus marked this pull request as draft February 23, 2026 05:22
@snejus snejus added the musicbrainz musicbrainz plugin label Apr 5, 2026
@snejus snejus force-pushed the type-musicbrainz branch 4 times, most recently from a6a3aa5 to ec1ce1d Compare April 6, 2026 08:22
@snejus snejus marked this pull request as ready for review April 6, 2026 08:23
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 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.

@snejus snejus force-pushed the type-musicbrainz branch 4 times, most recently from 7d0a0e5 to fa3e5ac Compare April 6, 2026 09:59
@snejus snejus requested a review from Copilot April 6, 2026 10:01
@snejus
Copy link
Copy Markdown
Member Author

snejus commented Apr 6, 2026

1. `TypedDict`s are only referenced in type annotations, so they can all be put under `if TYPE_CHECKING:`

I don't see any benefit for doing so (this may actually increase verbosity because of shorter line lengths and reformatting). Sometimes they should be defined outside the TYPE_CHECKING block, e.g. see beets.util.color.get_color_config.

2. Using `StrEnum`s instead of Literals (or a custom `Enum` type [like this](https://github.com/prTopi/beets-vocadb/blob/65d21d3c752691f8da34e200232e3fa6919a3ddc/beetsplug/vocadb/vocadb_api_client/models/__init__.py#L22) that overrides auto()).

Again, I don't see any benefit for this - these are just static definitions without any functionality attached to them.

3. Perhaps it's worth putting the models into their own module or even package.

These definitions belong to the MusicBrainzAPI and I think they should be tightly coupled. I don't want to force users of this API to import the definitions form another module.

Copy link
Copy Markdown
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

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

Comment thread beetsplug/musicbrainz.py Outdated
Comment thread beetsplug/musicbrainz.py
Comment thread beetsplug/_utils/musicbrainz.py
Comment thread beetsplug/_utils/musicbrainz.py Outdated
@snejus snejus force-pushed the type-musicbrainz branch from fa3e5ac to 5bc9462 Compare April 6, 2026 11:37
Copy link
Copy Markdown
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

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

Comment thread setup.cfg
Comment thread beetsplug/_utils/musicbrainz.py Outdated
Comment thread beetsplug/_utils/musicbrainz.py
Comment thread beetsplug/_utils/musicbrainz.py
Comment thread beetsplug/parentwork.py
@snejus snejus force-pushed the type-musicbrainz branch 5 times, most recently from 1bbc6a7 to c6592ac Compare April 11, 2026 13:17
Copy link
Copy Markdown
Contributor

@semohr semohr left a comment

Choose a reason for hiding this comment

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

Code changes look good to me. Am a big fan of typing the api responses!

I was wondering if one could have omitted the normalization of the hypens by using the TypeDict functional syntax (see here).

I dont mind either way but the normalization step adds a bit of complexity.

Comment thread beetsplug/_utils/musicbrainz.py
Comment thread beetsplug/_utils/musicbrainz.py
@amogus07
Copy link
Copy Markdown
Contributor

2. Using ``StrEnum``s instead of Literals (or a custom ``Enum`` type [like this](https://github.com/prTopi/beets-vocadb/blob/65d21d3c752691f8da34e200232e3fa6919a3ddc/beetsplug/vocadb/vocadb_api_client/models/__init__.py#L22) that overrides auto()).

Again, I don't see any benefit for this - these are just static definitions without any functionality attached to them.

Enums can be trivially enforced at runtime, which makes it easier to catch typos. But I can see how that's not relevant when we're using TypedDict which isn't enforced at runtime either.

@snejus
Copy link
Copy Markdown
Member Author

snejus commented Apr 12, 2026

I was wondering if one could have omitted the normalization of the hypens by using the TypeDict functional syntax (see here).

Back in the day when I started this work, I created these types by hand using functional syntax but found the definitions too awkward to read. Thus I decided to just normalise the dashes to underscores for consistency, hoping that this is going to be easier to maintain going forward.

@semohr
Copy link
Copy Markdown
Contributor

semohr commented Apr 12, 2026

Enums can be trivially enforced at runtime, which makes it easier to catch typos. But I can see how that's not relevant when we're using TypedDict which isn't enforced at runtime either.

The added types are not used at runtime at all or am I missing something?

Copy link
Copy Markdown
Contributor

@semohr semohr left a comment

Choose a reason for hiding this comment

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

All my questions here were thoughtfully answered! I'm a bit unsure about the _utils folder convention in general now 😆 This is nothing that should block this here tho.

Should be very straight forward to change the structure in the future anyways. From my side this is a go ahead.

@amogus07 Do you have any strong opinions which could block this?

@snejus snejus force-pushed the type-musicbrainz branch from c6592ac to aab4dff Compare April 12, 2026 15:21
@amogus07
Copy link
Copy Markdown
Contributor

amogus07 commented Apr 12, 2026

@amogus07 Do you have any strong opinions which could block this?

no, feel free to merge it if you want

@snejus snejus merged commit ff9f93e into master Apr 12, 2026
22 checks passed
@snejus snejus deleted the type-musicbrainz branch April 12, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

musicbrainz musicbrainz plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants