Skip to content

typing: type the rest of plugins - #6940

Open
snejus wants to merge 6 commits into
type-mpdstats-thumbnails-pluginsfrom
type-the-rest-of-codebase
Open

typing: type the rest of plugins#6940
snejus wants to merge 6 commits into
type-mpdstats-thumbnails-pluginsfrom
type-the-rest-of-codebase

Conversation

@snejus

@snejus snejus commented Aug 18, 2026

Copy link
Copy Markdown
Member

Fixes #6924.

  • Enables repo-wide annotation checks in pyproject.toml via ruff's flake8-annotations rules, with practical carve-outs for test/**, docs/conf.py, Any, and noisy variadic args. This adds a clear enforcement layer without forcing annotations in low-value areas.

  • Finishes typing the entire codebase, including command entrypoints, import hooks, config-driven code, helper utilities, and library/model-facing APIs. Architecturally, plugin boundaries now describe their inputs and outputs explicitly instead of relying on implicit contracts.

  • Includes a small set of follow-on fixes uncovered by typing, mainly around bytes vs. strings, optional values, and collection shapes. These are mostly correctness and consistency fixes rather than intentional behavior changes.

  • Overall impact: this completes typing coverage for the full codebase. The result is stronger static guarantees, clearer extension points, and safer future refactors across both core and plugin code.

Copilot AI lite review requested due to automatic review settings August 18, 2026 09:27
@snejus
snejus requested review from a team, JOJ0, asardaes and henry-oberholtzer as code owners August 18, 2026 09:27
@github-actions

Copy link
Copy Markdown

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.

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
2751 1 2750 120
View the top 1 failed test(s) by shortest run time
test\plugins\test_badfiles.py::plugins::test_badfiles::BadfilesOnImportTest::test_play_on_import
Stack Traces | 0.017s run time
self = <test.plugins.test_badfiles.BadfilesOnImportTest testMethod=test_play_on_import>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mtest_play_on_import#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m):#x1B[90m#x1B[39;49;00m
        BadFiles()#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m.importer.add_choice(#x1B[33m"#x1B[39;49;00m#x1B[33mc#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        #x1B[96mself#x1B[39;49;00m.io.addinput(#x1B[33m"#x1B[39;49;00m#x1B[33mc#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        checker = #x1B[96mself#x1B[39;49;00m.temp_path / #x1B[33m"#x1B[39;49;00m#x1B[33mchecker#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m#x1B[90m#x1B[39;49;00m
        checker.write_text(#x1B[33m"#x1B[39;49;00m#x1B[33m#!/bin/sh#x1B[39;49;00m#x1B[33m\n#x1B[39;49;00m#x1B[33mexit 1#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        checker.chmod(#x1B[94m0o755#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
        #x1B[94mwith#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.configure_plugin(#x1B[90m#x1B[39;49;00m
            {#x1B[33m"#x1B[39;49;00m#x1B[33mcheck_on_import#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m: #x1B[94mTrue#x1B[39;49;00m, #x1B[33m"#x1B[39;49;00m#x1B[33mcommands#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m: {#x1B[33m"#x1B[39;49;00m#x1B[33mmp3#x1B[39;49;00m#x1B[33m"#x1B[39;49;00m: #x1B[96mstr#x1B[39;49;00m(checker)}}#x1B[90m#x1B[39;49;00m
        ):#x1B[90m#x1B[39;49;00m
>           #x1B[96mself#x1B[39;49;00m.importer.run()#x1B[90m#x1B[39;49;00m

#x1B[1m#x1B[31mtest\plugins\test_badfiles.py#x1B[0m:62: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
#x1B[1m#x1B[31mbeets\importer\session.py#x1B[0m:239: in run
    #x1B[0mpl.run_sequential()#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\util\pipeline.py#x1B[0m:468: in run_sequential
    #x1B[0m#x1B[96mlist#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m.pull())#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\util\pipeline.py#x1B[0m:558: in pull
    #x1B[0mout = coro.send(msg)#x1B[90m#x1B[39;49;00m
          ^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\util\pipeline.py#x1B[0m:199: in coro
    #x1B[0mtask = func(*args, task)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\importer\stages.py#x1B[0m:164: in user_query
    #x1B[0mtask.choose_match(session)#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\importer\tasks.py#x1B[0m:828: in choose_match
    #x1B[0mchoice = session.choose_match(#x1B[96mself#x1B[39;49;00m)#x1B[90m#x1B[39;49;00m
             ^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\test\helper.py#x1B[0m:715: in choose_match
    #x1B[0m#x1B[94mreturn#x1B[39;49;00m #x1B[96msuper#x1B[39;49;00m().choose_match(task)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\ui\commands\import_\session.py#x1B[0m:88: in choose_match
    #x1B[0mchoice = choose_candidate(#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\ui\commands\import_\session.py#x1B[0m:462: in choose_candidate
    #x1B[0msel = ui.input_options(choice_opts, numrange=(#x1B[94m1#x1B[39;49;00m, #x1B[96mlen#x1B[39;49;00m(candidates)))#x1B[90m#x1B[39;49;00m
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\ui\__init__.py#x1B[0m:356: in input_options
    #x1B[0mresp = input_(fallback_prompt)#x1B[90m#x1B[39;49;00m
           ^^^^^^^^^^^^^^^^^^^^^^^#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mbeets\ui\__init__.py#x1B[0m:174: in input_
    #x1B[0mresp = #x1B[96minput#x1B[39;49;00m()#x1B[90m#x1B[39;49;00m
           ^^^^^^^#x1B[90m#x1B[39;49;00m
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <beets.test._common.DummyIn object at 0x00000149AA617E00>

    #x1B[0m#x1B[94mdef#x1B[39;49;00m#x1B[90m #x1B[39;49;00m#x1B[92mreadline#x1B[39;49;00m(#x1B[96mself#x1B[39;49;00m) -> #x1B[96mstr#x1B[39;49;00m:#x1B[90m#x1B[39;49;00m
        #x1B[94mif#x1B[39;49;00m #x1B[95mnot#x1B[39;49;00m #x1B[96mself#x1B[39;49;00m.buf:#x1B[90m#x1B[39;49;00m
>           #x1B[94mraise#x1B[39;49;00m InputError#x1B[90m#x1B[39;49;00m
#x1B[1m#x1B[31mE           beets.test._common.InputError: Attempt to read with no input provided.#x1B[0m

#x1B[1m#x1B[31mbeets\test\_common.py#x1B[0m:121: InputError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

grug see PR want finish typing for rest of beets plugins + turn on ruff flake8-annotations (ANN). goal good: make plugin boundary less mystery meat, catch bugs earlier.

Changes:

  • turn on ruff ANN rules repo-wide, with per-file ignores and noise carve-outs
  • add type hints across many bundled plugins + helpers + scripts
  • small runtime fixes found by typing (bytes/str, optionals, shapes)

Reviewed changes

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

Show a summary per file
File Description
test/plugins/test_listenbrainz.py update MusicBrainz request expectation
pyproject.toml enable ruff ANN + ignores/config
extra/release.py add return types to click cmds
docs/conf.py add types to Sphinx hooks
beetsplug/zero.py add plugin API types + JSON typing
beetsplug/unimported.py type plugin init + commands
beetsplug/types.py type plugin type map building
beetsplug/titlecase.py type callback args
beetsplug/the.py type patterns + template funcs
beetsplug/substitute.py type template substitute func
beetsplug/subsonicupdate.py type token/url helpers
beetsplug/subsonicplaylist.py type playlist parsing + requests
beetsplug/smartplaylist.py type JSON playlist config parsing
beetsplug/scrub.py type scrub entrypoints + mutagen classes
beetsplug/rewrite.py add annotations + safer names
beetsplug/replaygain.py add many annotations + small refactors
beetsplug/replace.py type CLI args + return values
beetsplug/random.py type random command plumbing
beetsplug/plexupdate.py type plex request helpers
beetsplug/playlist.py type playlist iter/update APIs
beetsplug/play.py type play helpers + plugin hooks
beetsplug/permissions.py type perms helpers + collections
beetsplug/parentwork.py type import hooks + JSON results
beetsplug/mpdupdate.py type socket wrapper + MPD update
beetsplug/missing.py type missing-track helpers
beetsplug/metasync/itunes.py type iTunes source + helpers
beetsplug/metasync/amarok.py type amarok source + import
beetsplug/metasync/init.py type metasync base + loader
beetsplug/mbsync.py type sync command helpers
beetsplug/mbpseudo.py type pseudo info + deepcopy
beetsplug/mbcollection.py type commands return
beetsplug/lyrics.py type backends + JSON helpers
beetsplug/limit.py type query plugin return
beetsplug/lastimport.py type pylast wrappers + parsing
beetsplug/lastgenre/client.py add missing return annotation
beetsplug/kodiupdate.py type kodi request helper
beetsplug/keyfinder.py type import hooks + find_key
beetsplug/ipfs.py type ipfs helpers + bytes/str fixes
beetsplug/inline.py type inline compiler + env dict
beetsplug/info.py type data emitters + printing
beetsplug/importsource.py type import stage hook
beetsplug/importfeeds.py type m3u helpers + recording
beetsplug/importadded.py type mtimes + fix int cast
beetsplug/ihate.py type classmethod hook
beetsplug/hook.py type event choice + signatures
beetsplug/fuzzy.py type query mapping return
beetsplug/fromfilename.py type filename matcher helpers
beetsplug/freedesktop.py type commands return
beetsplug/fish.py type fish completion generators
beetsplug/filefilter.py type file filter predicate
beetsplug/fetchart.py type returns for sources/config
beetsplug/embyupdate.py type emby API helpers
beetsplug/edit.py type edit YAML + importer hooks
beetsplug/duplicates.py type duplicate processing pipeline
beetsplug/convert.py type parallel convert helper
beetsplug/bucket.py type bucket span parsing/format
beetsplug/bpsync.py type beatport sync plugin
beetsplug/bpm.py type bpm helpers + plugin
beetsplug/bench.py type benchmark entrypoints
beetsplug/bareasc.py type query + usage set call
beetsplug/badfiles.py type badfiles checker plumbing
beetsplug/albumtypes.py type template field return
beetsplug/advancedrewrite.py type rewrite factory + ignore
beetsplug/absubmit.py type AB submit plugin + logging
beetsplug/_utils/vfs.py type helper return
Suppressed comments (3)

beetsplug/badfiles.py:44

  • grug think cmd is str here, but code treat cmd like list. cmd[0] become first char, cmd[-1] become last char. error report wrong. make cmd be list[str].

This issue also appears in the following locations of the same file:

  • line 65
  • line 82
    def __init__(self, cmd: str, oserror: OSError) -> None:
        self.checker = cmd[0]
        self.path = cmd[-1]
        self.errno = oserror.errno
        self.msg = str(oserror)

beetsplug/badfiles.py:69

  • grug see run_command take str, then list2cmdline(cmd) and check_output(cmd) both wrong. check_output with str (no shell) try run whole string as program, so command with spaces fail. take list[str] and pass list to subprocess.
    def run_command(self, cmd: str) -> tuple[int, int, list[str]]:
        self._log.debug(
            "running command: {}", displayable_path(list2cmdline(cmd))
        )
        try:

beetsplug/badfiles.py:90

  • grug see command builder now glue path into one string. if file path have space, shlex split break and checker fail. pass argv list to run_command instead.
    def check_mp3val(self, path: str) -> tuple[int, int, list[str]]:
        status, errors, output = self.run_command(f"mp3val {path}")
        if status == 0:
            output = [line for line in output if line.startswith("WARNING:")]
            errors = len(output)
        return status, errors, output

    def check_flac(self, path: str) -> tuple[int, int, list[str]]:
        return self.run_command(f"flac -wst {path}")

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyproject.toml Outdated
Comment thread beetsplug/badfiles.py Outdated
Comment thread beetsplug/absubmit.py
Comment thread beetsplug/bpsync.py Outdated
Comment thread beetsplug/permissions.py
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from cc060d4 to c7dbd0d Compare August 18, 2026 09:42
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from f8ca952 to 9bbf769 Compare August 18, 2026 09:42
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from c7dbd0d to 60d7763 Compare August 18, 2026 09:57
@snejus
snejus requested a review from semohr as a code owner August 18, 2026 09:57
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from 9bbf769 to 96ad42b Compare August 18, 2026 09:57
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from 60d7763 to 050c93e Compare August 19, 2026 02:25
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from 96ad42b to 344300e Compare August 19, 2026 02:26
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from 050c93e to d4aced5 Compare August 19, 2026 10:00
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from 344300e to 07d4565 Compare August 19, 2026 10:01
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from d4aced5 to 71eea34 Compare August 19, 2026 15:40
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from 07d4565 to 34db120 Compare August 19, 2026 15:40
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from 71eea34 to 85affd9 Compare August 19, 2026 16:07
@snejus
snejus force-pushed the type-the-rest-of-codebase branch 2 times, most recently from 90f77c1 to 97d5634 Compare August 19, 2026 16:24
@snejus
snejus force-pushed the type-mpdstats-thumbnails-plugins branch from 85affd9 to da994dc Compare August 19, 2026 16:42
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from 97d5634 to eaf3d58 Compare August 19, 2026 16:45
@snejus
snejus requested a lite review from Copilot August 19, 2026 16:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 68 out of 68 changed files in this pull request and generated 7 comments.

Suppressed comments (4)

beetsplug/the.py:1

  • patterns is now only an annotation with no class or instance default, but __init__ reads self.patterns. This will raise AttributeError unless something else sets it before __init__. Restore a concrete default (e.g., keep a ClassVar[list[str]] = []) or initialize self.patterns in __init__.
    beetsplug/the.py:1
  • patterns is now only an annotation with no class or instance default, but __init__ reads self.patterns. This will raise AttributeError unless something else sets it before __init__. Restore a concrete default (e.g., keep a ClassVar[list[str]] = []) or initialize self.patterns in __init__.
    test/plugins/test_play.py:1
  • os is used here but is not imported in the shown updated import section. Add an import os at the top of this test module to avoid a NameError.
    beets/util/init.py:414
  • This change no longer handles general os.PathLike objects (e.g., pathlib.PurePath) as a single path; they will fall through to the iterable branch and be joined by components, producing incorrect output. It also removes the previous fallback for non-iterable, non-path objects (previously str(path)), which can now raise TypeError when such objects are passed. Consider decoding via os.fsdecode(os.fspath(path)) in a try block and only falling back to iterating when os.fspath fails.
    if isinstance(path, (Path, str, bytes)):
        return os.fsdecode(path)

    return separator.join(displayable_path(p) for p in path)

Comment thread beetsplug/badfiles.py Outdated
Comment thread beetsplug/badfiles.py Outdated
Comment thread beetsplug/badfiles.py Outdated
Comment thread beetsplug/badfiles.py Outdated
Comment thread beetsplug/absubmit.py Outdated
Comment thread beetsplug/absubmit.py Outdated
Comment thread beetsplug/absubmit.py
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from eaf3d58 to fd71b30 Compare August 19, 2026 19:59
@snejus
snejus force-pushed the type-the-rest-of-codebase branch from fd71b30 to a73f582 Compare August 19, 2026 20:08
@snejus
snejus requested a lite review from Copilot August 19, 2026 20:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 69 out of 69 changed files in this pull request and generated no new comments.

Suppressed comments (6)

beets/dbcore/query.py:54

  • grug see query can be bytes. bytes is Sequence but not str. then code do " ".join(query) and crash (join want str, bytes give int). grug say exclude bytes from this branch.
        if isinstance(query, Sequence) and not isinstance(query, str):
            query = " ".join(query)

beets/util/init.py:961

  • grug see targets allow Path, but execlp get Path object and may TypeError. also bytes targets + str args mix not good for windows. grug say fsdecode all targets to str before os.execlp.
    first, *rest = args

    # 'first' is duplicated because of argv[0]
    os.execlp(*[first, first, *rest, *targets])

beetsplug/missing.py:49

  • grug see return type say bool | int, but function always make int (subtract ints). wrong type make type checker sad.
    test/plugins/test_badfiles.py:52
  • grug see test name say play, but this file test badfiles. name confuse future grug.
    beetsplug/play.py:65
  • grug see keep_open path args is bytes but shlex.split give str. list have mix str+bytes. windows subprocess no like, go boom. grug say convert open_args to str (util.syspath / os.fsdecode) before subprocess.call, and update tests.
    test/plugins/test_play.py:212
  • grug see this test assert subprocess called with ["echo", ]. that bake in str+bytes mix. if code fix for windows (all str), this test need update to expect str path.

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