Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,5 @@ f37306b0cc17f17291ea5010178f190a95b29fd5
27900f58fbeba06483b77c677045ad1df8e9af45
# typing: rename AnyLibModel -> AlbumOrItem
33a01fe0b37942a38c8b88d0fc8167edd02ca81b
# typing: add types to command handlers
14eabc60600ff3c8ef1908100ae7abd3aa998bc4
14 changes: 8 additions & 6 deletions beets/dbcore/sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from collections.abc import Sequence

from beets.dbcore.db import AnyModel, Model


Expand All @@ -19,8 +21,8 @@ def order_clause(self) -> str | None:
"""
return None

def sort(self, items: list[AnyModel]) -> list[AnyModel]:
"""Sort the list of objects and return a list."""
def sort(self, items: Sequence[AnyModel]) -> Sequence[AnyModel]:
"""Sort the given sequence of model objects."""
return sorted(items)

def is_slow(self) -> bool:
Expand Down Expand Up @@ -72,7 +74,7 @@ def is_slow(self) -> bool:
return True
return False

def sort(self, items: list[AnyModel]) -> list[AnyModel]:
def sort(self, items: Sequence[AnyModel]) -> Sequence[AnyModel]:
slow_sorts = []
switch_slow = False
for sort in reversed(self.sorts):
Expand Down Expand Up @@ -114,7 +116,7 @@ def __init__(
self.ascending = ascending
self.case_insensitive = case_insensitive

def sort(self, objs: list[AnyModel]) -> list[AnyModel]:
def sort(self, objs: Sequence[AnyModel]) -> Sequence[AnyModel]:
# TODO: Support flexible attributes with different types (e.g. a mix
# of strings and numbers) without falling over.

Expand Down Expand Up @@ -186,7 +188,7 @@ def is_slow(self) -> bool:
class NullSort(Sort):
"""No sorting. Leave results unsorted."""

def sort(self, items: list[AnyModel]) -> list[AnyModel]:
def sort(self, items: Sequence[AnyModel]) -> Sequence[AnyModel]:
return items

def __nonzero__(self) -> bool:
Expand Down Expand Up @@ -214,7 +216,7 @@ def order_clause(self) -> str:

return f"COALESCE(NULLIF({field}_sort, ''), {field}) {collate} {order}"

def sort(self, objs: list[AnyModel]) -> list[AnyModel]:
def sort(self, objs: Sequence[AnyModel]) -> Sequence[AnyModel]:
def key(obj: Model) -> str | bytes:
val = obj[f"{self.field}_sort"] or obj[self.field]
return val.lower() if self.case_insensitive else val
Expand Down
7 changes: 5 additions & 2 deletions beets/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,10 @@ def add_album_option(self, flags=("-a", "--album")):
Sets the album property on the options extracted from the CLI.
"""
album = optparse.Option(
*flags, action="store_true", help="match albums instead of tracks"
*flags,
action="store_true",
default=False,
help="match albums instead of tracks",
)
self.add_option(album)
self._album_flags = set(flags)
Expand Down Expand Up @@ -581,7 +584,7 @@ class Subcommand:
invoked by a SubcommandOptionParser.
"""

func: Callable[[library.Library, optparse.Values, list[str]], Any]
func: Callable[[library.Library, Any, list[str]], Any]

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.

optparse.Values was more concise. I guess this is needed now since we use protocols for typing?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Indeed

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.

Why was Protocol chosen instead of using optparse.Values as a base class?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

optparse always constructs a plain Values, not instances of our command-specific subclasses. A Protocol lets each handler declare the fields it consumes without incorrectly claiming that the runtime object is an instance of a custom Values subclass.

We also don't need to import optparse this way :)


def __init__(self, name, parser=None, help="", aliases=(), hide=False): # noqa: A002
"""Creates a new subcommand. name is the primary way to invoke
Expand Down
13 changes: 12 additions & 1 deletion beets/ui/commands/completion.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"""The 'completion' command: print shell script for command line completion."""

from __future__ import annotations

import os
import re
from typing import TYPE_CHECKING

from beets import library, logging, plugins, ui
from beets.util import syspath

if TYPE_CHECKING:
import optparse

from beets.library import Library


# Global logger.
log = logging.getLogger("beets")


def print_completion(*args):
def print_completion(
lib: Library, opts: optparse.Values, args: list[str]
) -> None:
from beets.ui.commands import default_commands

for line in completion_script(default_commands + plugins.commands()):
Expand Down
17 changes: 16 additions & 1 deletion beets/ui/commands/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
"""The 'config' command: show and edit user configuration."""

from __future__ import annotations

import os
from typing import TYPE_CHECKING, Protocol

from beets import config, ui
from beets.exceptions import UserError
from beets.util import displayable_path, editor_command, interactive_open

if TYPE_CHECKING:
from beets.library import Library


class ConfigCLIOpts(Protocol):
paths: bool | None
defaults: bool
edit: bool | None
redact: bool
config: str | None


def config_func(lib, opts, args):
def config_func(lib: Library, opts: ConfigCLIOpts, args: list[str]) -> None:
# Make sure lazy configuration is loaded
config.resolve()

Expand Down Expand Up @@ -84,6 +98,7 @@ def config_edit(cli_options):
"-d",
"--defaults",
action="store_true",
default=False,
help="include the default configuration",
)
config_cmd.parser.add_option(
Expand Down
10 changes: 9 additions & 1 deletion beets/ui/commands/fields.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"""The `fields` command: show available fields for queries and format strings."""

from __future__ import annotations

import textwrap
from typing import TYPE_CHECKING

from beets import library, ui

if TYPE_CHECKING:
import optparse

from beets.library import Library


def _print_keys(query):
"""Given a SQLite query result, print the `key` field of each
Expand All @@ -13,7 +21,7 @@ def _print_keys(query):
ui.print_(f" {row['key']}")


def fields_func(lib, opts, args):
def fields_func(lib: Library, opts: optparse.Values, args: list[str]) -> None:
Comment thread
semohr marked this conversation as resolved.
def _print_rows(names):
ui.print_(textwrap.indent("\n".join(sorted(names)), " "))

Expand Down
13 changes: 12 additions & 1 deletion beets/ui/commands/help.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
"""The 'help' command: show help information for commands."""

from __future__ import annotations

from typing import TYPE_CHECKING

from beets import ui
from beets.exceptions import UserError

if TYPE_CHECKING:
import optparse

from beets.library import Library


class HelpCommand(ui.Subcommand):
def __init__(self):
Expand All @@ -12,7 +21,9 @@ def __init__(self):
help="give detailed help on a specific sub-command",
)

def func(self, lib, opts, args):
def func(
self, lib: Library, opts: optparse.Values, args: list[str]
) -> None:
if args:
cmdname = args[0]
helpcommand = self.root_parser._subcommand_for_name(cmdname)
Expand Down
12 changes: 9 additions & 3 deletions beets/ui/commands/import_/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import os
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Protocol

from beets import config, logging, plugins, ui
from beets.exceptions import UserError
Expand All @@ -22,6 +22,12 @@
log = logging.getLogger("beets")


class ImportCLIOpts(Protocol):
copy: bool | None
library: bool | None
from_logfiles: list[str] | None


def paths_from_logfile(path: str) -> Iterator[str]:
"""Parse the logfile and yield skipped paths to pass to the `import`
command.
Expand Down Expand Up @@ -91,8 +97,8 @@ def import_files(
plugins.send("import", lib=lib, paths=paths)


def import_func(lib: Library, opts: optparse.Values, args: list[str]) -> None:
config["import"].set_args(opts)
def import_func(lib: Library, opts: ImportCLIOpts, args: list[str]) -> None:
config["import"].set_args(vars(opts))

# Special case: --copy flag suppresses import_move (which would
# otherwise take precedence).
Expand Down
13 changes: 12 additions & 1 deletion beets/ui/commands/list.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
"""The 'list' command: query and show library contents."""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol

from beets import ui

if TYPE_CHECKING:
from beets.library import Library


class ListCLIOpts(Protocol):
album: bool


def list_items(lib, query, album, fmt=""):
"""Print out items in lib matching query. If album, then search for
Expand All @@ -15,7 +26,7 @@ def list_items(lib, query, album, fmt=""):
ui.print_(format(item, fmt))


def list_func(lib, opts, args):
def list_func(lib: Library, opts: ListCLIOpts, args: list[str]) -> None:
list_items(lib, args, opts.album)


Expand Down
14 changes: 11 additions & 3 deletions beets/ui/commands/modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, NamedTuple
from typing import TYPE_CHECKING, NamedTuple, Protocol

from beets import library, ui
from beets.dbcore import types
Expand All @@ -12,7 +12,15 @@
from .utils import do_query

if TYPE_CHECKING:
from beets.library import LibModel
from beets.library import LibModel, Library


class ModifyCLIOpts(Protocol):
album: bool
inherit: bool
move: bool | None
write: bool | None
yes: bool | None


class ModifyOperation(NamedTuple):
Expand Down Expand Up @@ -150,7 +158,7 @@ def modify_parse_args(args, is_album: bool):
return query, mods, dels


def modify_func(lib, opts, args):
def modify_func(lib: Library, opts: ModifyCLIOpts, args: list[str]) -> None:
query, mods, dels = modify_parse_args(args, is_album=opts.album)
if not mods and not dels:
raise UserError("no modifications specified")
Expand Down
24 changes: 16 additions & 8 deletions beets/ui/commands/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import os
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Protocol

from beets import logging, ui
from beets.exceptions import UserError
Expand All @@ -13,12 +13,21 @@
from .utils import do_query

if TYPE_CHECKING:
from beets.util import PathLike
from beets.library import Library

# Global logger.
log = logging.getLogger("beets")


class MoveCLIOpts(Protocol):
album: bool
copy: bool
dest: str | None
export: bool
pretend: bool
timid: bool


def show_path_changes(path_changes):
"""Given a list of tuples (source, destination) that indicate the
path changes, log the changes as INFO-level output to the beets log.
Expand Down Expand Up @@ -62,7 +71,7 @@ def show_path_changes(path_changes):

def move_items(
lib,
dest_path: PathLike,
dest: bytes | None,
query,
copy,
album,
Expand All @@ -74,7 +83,6 @@ def move_items(
dest is None, then the library's base directory is used, making the
command "consolidate" files.
"""
dest = os.fsencode(dest_path) if dest_path else dest_path
items, albums = do_query(lib, query, album, False)
objs = albums if album else items
num_objs = len(objs)
Expand Down Expand Up @@ -147,10 +155,9 @@ def isalbummoved(album):
obj.move(operation=MoveOperation.MOVE, basedir=dest)


def move_func(lib, opts, args):
dest = opts.dest
def move_func(lib: Library, opts: MoveCLIOpts, args: list[str]) -> None:
dest = normpath(opts.dest) if opts.dest else None
if dest is not None:
dest = normpath(dest)
if not os.path.isdir(syspath(dest)):
raise UserError(f"no such directory: {displayable_path(dest)}")

Expand Down Expand Up @@ -188,14 +195,15 @@ def move_func(lib, opts, args):
"-t",
"--timid",
dest="timid",
default=False,
action="store_true",
help="always confirm all actions",
)
move_cmd.parser.add_option(
"-e",
"--export",
default=False,
action="store_true",
default=False,
help="copy without changing the database path",
)
move_cmd.parser.add_album_option()
Expand Down
Loading
Loading