Migrate from isort/Black/Flake8 to Ruff for Code Formatting and Linting (#5424)

#### Context
I noticed one of recently merged PRs used `unittest` assertions that we
are migrating from. Thus, I thought it would be a good idea to configure
our linting to catch such issues in the future. Since I was here, I
replaced our multiple linting and formatting tools with a single tool,
Ruff, to simplify and speed up our development workflow.

#### Summary
This PR migrates the codebase from using isort, Black and Flake8 to Ruff
for code formatting and linting. The changes include updates to the
GitHub Actions workflow, pre-commit configuration, and documentation to
reflect the new tooling.

#### Changes
1. **GitHub Actions Workflow**
- Updated `.github/workflows/lint.yml` to use `poetry install
--only=lint` and `poe lint --output-format=github`.

2. **Pre-commit Configuration**
   - Replaced Black and Isort with Ruff in `.pre-commit-config.yaml`.

3. **Documentation**
- Updated `CONTRIBUTING.rst` to reflect the use of Ruff for formatting
and linting.
- Modified instructions for running tests and handling external API
requests.

4. **Poetry Configuration**
- Removed Black, Isort, Flake8, and related dependencies from
`poetry.lock` and `pyproject.toml`.
   - Added Ruff as the new linter and formatter in `pyproject.toml`.

5. **Setup Configuration**
   - Removed Flake8 configuration from `setup.cfg`.

6. **Git blame**
- Introduced `.git-blame-ignore-revs` file to keep git blame clean from
formatting
changes. Configure your local `beets` repository to use this file by
running:
   ```fish
   $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs
   ```

#### Benefits
- **Performance**: Ruff is known for its speed and efficiency, which
should improve the developer experience.
- **Consolidation**: Using a single tool for both formatting and linting
simplifies the development workflow.

#### How to Test
1. **Linting and Formatting**
   - Run `poe check-format` to check for formatting issues.
   - Run `poe format` to format the codebase.
   - Run `poe lint` to check for linting issues.

2. **Pre-commit Hooks**
- Ensure pre-commit hooks are working correctly by running `pre-commit
run --all-files`.

3. **CI Pipeline**
   - Verify that the GitHub Actions workflow completes successfully.

#### Notes
- Contributions migrating existing tests from `unittest` to `pytest` are
welcome.
- External API requests should be mocked using `requests_mock` and
tested weekly in the integration test suite.

#### References
- [Ruff Documentation](https://docs.astral.sh/ruff/)
- [Pre-commit Hooks](https://pre-commit.com/hooks.html)

---

This PR aims to streamline our development process by adopting Ruff, a
modern and efficient tool for Python code formatting and linting.
This commit is contained in:
Šarūnas Nejus 2024-09-21 13:35:14 +01:00 committed by GitHub
commit 88d3f040e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
121 changed files with 554 additions and 794 deletions

45
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,45 @@
# 2014
# flake8-cleanliness in missing
e21c04e9125a28ae0452374acf03d93315eb4381
# 2016
# Removed unicode_literals from library, logging and mediafile
43572f50b0eb3522239d94149d91223e67d9a009
# Removed unicode_literals from plugins
53d2c8d9db87be4d4750ad879bf46176537be73f
# reformat flake8 errors
1db46dfeb6607c164afb247d8da82443677795c1
# 2021
# pyupgrade root
e26276658052947e9464d9726b703335304c7c13
# pyupgrade beets dir
6d1316f463cb7c9390f85bf35b220e250a35004a
# pyupgrade autotag dir
f8b8938fd8bbe91898d0982552bc75d35703d3ef
# pyupgrade dbcore dir
d288f872903c79a7ee7c5a7c9cc690809441196e
# pyupgrade ui directory
432fa557258d9ff01e23ed750f9a86a96239599e
# pyupgrade util dir
af102c3e2f1c7a49e99839e2825906fe01780eec
# fix unused import and flake8
910354a6c617ed5aa643cff666205b43e1557373
# pyupgrade beetsplug and tests
1ec87a3bdd737abe46c6e614051bf9e314db4619
# 2022
# Reformat flake8 config comments
abc3dfbf429b179fac25bd1dff72d577cd4d04c7
# 2023
# Apply formatting tools to all files
a6e5201ff3fad4c69bf24d17bace2ef744b9f51b
# 2024
# Reformat the codebase
85a17ee5039628a6f3cdcb7a03d7d1bd530fbe89
# Fix lint issues
f36bc497c8c8f89004f3f6879908d3f0b25123e1
# Remove some lint exclusions and fix the issues
5f78d1b82b2292d5ce0c99623ba0ec444b80d24c

View file

@ -60,7 +60,7 @@ jobs:
cache: poetry
- name: Install dependencies
run: poetry install --only=format
run: poetry install --only=lint
- name: Check code formatting
# the job output will contain colored diffs with what needs adjusting
@ -84,10 +84,7 @@ jobs:
run: poetry install --only=lint
- name: Lint code
uses: liskin/gh-problem-matcher-wrap@v3
with:
linters: flake8
run: poe lint ${{ needs.changed-files.outputs.changed_python_files }}
run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }}
mypy:
if: needs.changed-files.outputs.any_python_changed == 'true'

View file

@ -2,13 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black
rev: 24.2.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.6
hooks:
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)
- id: ruff-format

View file

@ -274,14 +274,13 @@ There are a few coding conventions we use in beets:
Style
-----
We follow `black`_ formatting and `google's docstring format`_.
We use `ruff`_ to format and lint the codebase.
Use ``poe check-format`` and ``poe lint`` to check your code for style and
Run ``poe check-format`` and ``poe lint`` to check your code for style and
linting errors. Running ``poe format`` will automatically format your code
according to the specifications required by the project.
.. _black: https://black.readthedocs.io/en/stable/
.. _google's docstring format: https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings
.. _ruff: https://docs.astral.sh/ruff/
Handling Paths
--------------
@ -345,10 +344,10 @@ environment variable ``SKIP_SLOW_TESTS``, for example::
Coverage
^^^^^^^^
Coverage is measured automatically when running the tests. If you find it takes
a while to calculate, disable it::
The ``test`` command does not include coverage as it slows down testing. In
order to measure it, use the ``test-with-coverage`` task
$ poe test --no-cov
$ poe test-with-coverage [pytest options]
You are welcome to explore coverage by opening the HTML report in
``.reports/html/index.html``.
@ -379,28 +378,24 @@ Writing Tests
Writing tests is done by adding or modifying files in folder `test`_.
Take a look at
`https://github.com/beetbox/beets/blob/master/test/test_template.py#L224`_
to get a basic view on how tests are written. We currently allow writing
tests with either `unittest`_ or `pytest`_.
to get a basic view on how tests are written. Since we are currently migrating
the tests from `unittest`_ to `pytest`_, new tests should be written using
`pytest`_. Contributions migrating existing tests are welcome!
Any tests that involve sending out network traffic e.g. an external API
call, should be skipped normally and run under our weekly `integration
test`_ suite. These tests can be useful in detecting external changes
that would affect ``beets``. In order to do this, simply add the
following snippet before the applicable test case:
External API requests under test should be mocked with `requests_mock`_,
However, we still want to know whether external APIs are up and that they
return expected responses, therefore we test them weekly with our `integration
test`_ suite.
In order to add such a test, mark your test with the ``integration_test`` marker
.. code-block:: python
@unittest.skipUnless(
os.environ.get('INTEGRATION_TEST', '0') == '1',
'integration testing not enabled')
@pytest.mark.integration_test
def test_external_api_call():
...
If you do this, it is also advised to create a similar test that 'mocks'
the network call and can be run under normal circumstances by our CI and
others. See `unittest.mock`_ for more info.
- **AVOID** using the ``start()`` and ``stop()`` methods of
``mock.patch``, as they require manual cleanup. Use the annotation or
context manager forms instead.
This way, the test will be run only in the integration test suite.
.. _Codecov: https://codecov.io/github/beetbox/beets
.. _pytest-random: https://github.com/klrmn/pytest-random
@ -410,6 +405,6 @@ others. See `unittest.mock`_ for more info.
.. _`https://github.com/beetbox/beets/blob/master/test/test_template.py#L224`: https://github.com/beetbox/beets/blob/master/test/test_template.py#L224
.. _unittest: https://docs.python.org/3/library/unittest.html
.. _integration test: https://github.com/beetbox/beets/actions?query=workflow%3A%22integration+tests%22
.. _unittest.mock: https://docs.python.org/3/library/unittest.mock.html
.. _requests-mock: https://requests-mock.readthedocs.io/en/latest/response.html
.. _documentation: https://beets.readthedocs.io/en/stable/
.. _vim: https://www.vim.org/

View file

@ -16,7 +16,6 @@
`python -m beets`.
"""
import sys
from .ui import main

View file

@ -16,7 +16,6 @@
music and items' embedded album art.
"""
import os
from tempfile import NamedTemporaryFile

View file

@ -12,23 +12,38 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Facilities for automatically determining files' correct metadata.
"""
"""Facilities for automatically determining files' correct metadata."""
from typing import Mapping, Sequence, Union
from beets import config, logging
from beets.library import Album, Item
# Parts of external interface.
from .hooks import ( # noqa
AlbumInfo,
AlbumMatch,
Distance,
TrackInfo,
TrackMatch,
from .hooks import AlbumInfo, AlbumMatch, Distance, TrackInfo, TrackMatch
from .match import (
Proposal,
Recommendation,
current_metadata,
tag_album,
tag_item,
)
from .match import Recommendation # noqa
from .match import Proposal, current_metadata, tag_album, tag_item # noqa
__all__ = [
"AlbumInfo",
"AlbumMatch",
"Distance",
"TrackInfo",
"TrackMatch",
"Proposal",
"Recommendation",
"apply_album_metadata",
"apply_item_metadata",
"apply_metadata",
"current_metadata",
"tag_album",
"tag_item",
]
# Global logger.
log = logging.getLogger("beets")

View file

@ -371,7 +371,7 @@ class Distance:
self.tracks: Dict[TrackInfo, Distance] = {}
@cached_classproperty
def _weights(cls) -> Dict[str, float]: # noqa: N805
def _weights(cls) -> Dict[str, float]:
"""A dictionary from keys to floating-point weights."""
weights_view = config["match"]["distance_weights"]
weights = {}

View file

@ -16,11 +16,11 @@
releases and tracks.
"""
from __future__ import annotations
import datetime
import re
from enum import IntEnum
from typing import (
Any,
Dict,
@ -48,7 +48,6 @@ from beets.autotag import (
)
from beets.library import Item
from beets.util import plurality
from beets.util.enumeration import OrderedEnum
# Artist signals that indicate "various artists". These are used at the
# album level to determine whether a given release is likely a VA
@ -63,7 +62,7 @@ log = logging.getLogger("beets")
# Recommendation enumeration.
class Recommendation(OrderedEnum):
class Recommendation(IntEnum):
"""Indicates a qualitative suggestion to the user about what should
be done with a given match.
"""

View file

@ -12,8 +12,8 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Searches for albums in the MusicBrainz database.
"""
"""Searches for albums in the MusicBrainz database."""
from __future__ import annotations
import re
@ -54,7 +54,7 @@ FIELDS_TO_MB_KEYS = {
musicbrainzngs.set_useragent("beets", beets.__version__, "https://beets.io/")
class MusicBrainzAPIError(util.HumanReadableException):
class MusicBrainzAPIError(util.HumanReadableError):
"""An error while talking to MusicBrainz. The `query` field is the
parameter to the action and may have any type.
"""

View file

@ -32,4 +32,18 @@ from .queryparse import (
)
from .types import Type
# flake8: noqa
__all__ = [
"AndQuery",
"Database",
"FieldQuery",
"InvalidQueryError",
"MatchQuery",
"Model",
"OrQuery",
"Query",
"Results",
"Type",
"parse_sorted_query",
"query_from_strings",
"sort_from_strings",
]

View file

@ -1224,9 +1224,7 @@ class Database:
UNIQUE(entity_id, key) ON CONFLICT REPLACE);
CREATE INDEX IF NOT EXISTS {0}_by_entity
ON {0} (entity_id);
""".format(
flex_table
)
""".format(flex_table)
)
# Querying.

View file

@ -12,8 +12,8 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Representation of type information for DBCore model fields.
"""
"""Representation of type information for DBCore model fields."""
import typing
from abc import ABC
from typing import Any, Generic, List, TypeVar, Union, cast

View file

@ -74,7 +74,7 @@ REIMPORT_FRESH_FIELDS_ITEM = list(REIMPORT_FRESH_FIELDS_ALBUM)
log = logging.getLogger("beets")
class ImportAbort(Exception):
class ImportAbortError(Exception):
"""Raised when the user aborts the tagging operation."""
pass
@ -360,7 +360,7 @@ class ImportSession:
pl.run_parallel(QUEUE_SIZE)
else:
pl.run_sequential()
except ImportAbort:
except ImportAbortError:
# User aborted operation. Silently stop.
pass
@ -627,8 +627,7 @@ class ImportTask(BaseImportTask):
self.save_progress()
if session.config["incremental"] and not (
# Should we skip recording to incremental list?
self.skip
and session.config["incremental_skip_later"]
self.skip and session.config["incremental_skip_later"]
):
self.save_history()
@ -947,7 +946,7 @@ class ImportTask(BaseImportTask):
dup_item.remove()
log.debug(
"{0} of {1} items replaced",
sum(bool(l) for l in self.replaced_items.values()),
sum(bool(v) for v in self.replaced_items.values()),
len(self.imported_items()),
)

View file

@ -20,10 +20,34 @@ use {}-style formatting and can interpolate keywords arguments to the logging
calls (`debug`, `info`, etc).
"""
import logging
import threading
from copy import copy
from logging import (
DEBUG,
INFO,
NOTSET,
WARNING,
FileHandler,
Filter,
Handler,
Logger,
NullHandler,
StreamHandler,
)
__all__ = [
"DEBUG",
"INFO",
"NOTSET",
"WARNING",
"FileHandler",
"Filter",
"Handler",
"Logger",
"NullHandler",
"StreamHandler",
"getLogger",
]
def logsafe(val):
@ -46,7 +70,7 @@ def logsafe(val):
return val
class StrFormatLogger(logging.Logger):
class StrFormatLogger(Logger):
"""A version of `Logger` that uses `str.format`-style formatting
instead of %-style formatting and supports keyword arguments.
@ -96,12 +120,12 @@ class StrFormatLogger(logging.Logger):
)
class ThreadLocalLevelLogger(logging.Logger):
class ThreadLocalLevelLogger(Logger):
"""A version of `Logger` whose level is thread-local instead of shared."""
def __init__(self, name, level=logging.NOTSET):
def __init__(self, name, level=NOTSET):
self._thread_level = threading.local()
self.default_level = logging.NOTSET
self.default_level = NOTSET
super().__init__(name, level)
@property
@ -128,17 +152,13 @@ class BeetsLogger(ThreadLocalLevelLogger, StrFormatLogger):
pass
my_manager = copy(logging.Logger.manager)
my_manager = copy(Logger.manager)
my_manager.loggerClass = BeetsLogger
# Act like the stdlib logging module by re-exporting its namespace.
from logging import * # noqa
# Override the `getLogger` to use our machinery.
def getLogger(name=None): # noqa
if name:
return my_manager.getLogger(name)
else:
return logging.Logger.root
return Logger.root

View file

@ -14,7 +14,6 @@
"""Support for beets plugins."""
import abc
import inspect
import re
@ -36,7 +35,7 @@ LASTFM_KEY = "2dc3914abf35f0d9c92d97d8f8e42b43"
log = logging.getLogger("beets")
class PluginConflictException(Exception):
class PluginConflictError(Exception):
"""Indicates that the services provided by one plugin conflict with
those of another.
@ -343,7 +342,7 @@ def types(model_cls):
plugin_types = getattr(plugin, attr_name, {})
for field in plugin_types:
if field in types and plugin_types[field] != types[field]:
raise PluginConflictException(
raise PluginConflictError(
"Plugin {} defines flexible field {} "
"which has already been defined with "
"another type.".format(plugin.name, field)
@ -447,13 +446,13 @@ def import_stages():
def _check_conflicts_and_merge(plugin, plugin_funcs, funcs):
"""Check the provided template functions for conflicts and merge into funcs.
Raises a `PluginConflictException` if a plugin defines template functions
Raises a `PluginConflictError` if a plugin defines template functions
for fields that another plugin has already defined template functions for.
"""
if plugin_funcs:
if not plugin_funcs.keys().isdisjoint(funcs.keys()):
conflicted_fields = ", ".join(plugin_funcs.keys() & funcs.keys())
raise PluginConflictException(
raise PluginConflictError(
f"Plugin {plugin.name} defines template functions for "
f"{conflicted_fields} that conflict with another plugin."
)

View file

@ -143,19 +143,19 @@ def import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):
class Assertions:
"""A mixin with additional unit test assertions."""
def assertExists(self, path): # noqa
def assertExists(self, path):
assert os.path.exists(syspath(path)), f"file does not exist: {path!r}"
def assertNotExists(self, path): # noqa
def assertNotExists(self, path):
assert not os.path.exists(syspath(path)), f"file exists: {path!r}"
def assertIsFile(self, path): # noqa
def assertIsFile(self, path):
self.assertExists(path)
assert os.path.isfile(
syspath(path)
), "path exists, but is not a regular file: {!r}".format(path)
def assertIsDir(self, path): # noqa
def assertIsDir(self, path):
self.assertExists(path)
assert os.path.isdir(
syspath(path)
@ -171,7 +171,7 @@ class Assertions:
# Mock I/O.
class InputException(Exception):
class InputError(Exception):
def __init__(self, output=None):
self.output = output
@ -218,9 +218,9 @@ class DummyIn:
def readline(self):
if not self.buf:
if self.out:
raise InputException(self.out.get())
raise InputError(self.out.get())
else:
raise InputException()
raise InputError()
self.reads += 1
return self.buf.pop(0)

View file

@ -17,7 +17,6 @@ interface. To invoke the CLI, just call beets.ui.main(). The actual
CLI commands are implemented in the ui.commands module.
"""
import errno
import optparse
import os.path
@ -318,7 +317,7 @@ def input_options(
# Wrap the query text.
# Start prompt with U+279C: Heavy Round-Tipped Rightwards Arrow
prompt = colorize("action", "\u279C ")
prompt = colorize("action", "\u279c ")
line_length = 0
for i, (part, length) in enumerate(
zip(prompt_parts, prompt_part_lengths)
@ -387,7 +386,7 @@ def input_yn(prompt, require=False):
"yes" unless `require` is `True`, in which case there is no default.
"""
# Start prompt with U+279C: Heavy Round-Tipped Rightwards Arrow
yesno = colorize("action", "\u279C ") + colorize(
yesno = colorize("action", "\u279c ") + colorize(
"action_description", "Enter Y or N:"
)
sel = input_options(("y", "n"), require, prompt, yesno)
@ -1497,9 +1496,7 @@ class SubcommandsOptionParser(CommonOptionsParser):
"""
# A more helpful default usage.
if "usage" not in kwargs:
kwargs[
"usage"
] = """
kwargs["usage"] = """
%prog COMMAND [ARGS...]
%prog help COMMAND"""
kwargs["add_help_option"] = False
@ -1867,7 +1864,7 @@ def main(args=None):
message = exc.args[0] if exc.args else None
log.error("error: {0}", message)
sys.exit(1)
except util.HumanReadableException as exc:
except util.HumanReadableError as exc:
exc.log(log)
sys.exit(1)
except library.FileOperationError as exc:

View file

@ -16,7 +16,6 @@
interface.
"""
import os
import re
from collections import Counter
@ -1027,7 +1026,7 @@ def manual_id(session, task):
def abort_action(session, task):
"""A prompt choice callback that aborts the importer."""
raise importer.ImportAbort()
raise importer.ImportAbortError()
class TerminalImportSession(importer.ImportSession):
@ -1057,7 +1056,7 @@ class TerminalImportSession(importer.ImportSession):
if len(actions) == 1:
return actions[0]
elif len(actions) > 1:
raise plugins.PluginConflictException(
raise plugins.PluginConflictError(
"Only one handler for `import_task_before_choice` may return "
"an action."
)
@ -1317,8 +1316,7 @@ def import_files(lib, paths, query):
loghandler = logging.FileHandler(logpath, encoding="utf-8")
except OSError:
raise ui.UserError(
"could not open log file for writing: "
"{}".format(displayable_path(logpath))
f"Could not open log file for writing: {displayable_path(logpath)}"
)
else:
loghandler = None

View file

@ -67,7 +67,7 @@ PathLike = Union[BytesOrStr, Path]
Replacements: TypeAlias = "Sequence[tuple[Pattern[str], str]]"
class HumanReadableException(Exception):
class HumanReadableError(Exception):
"""An Exception that can include a human-readable error message to
be logged without a traceback. Can preserve a traceback for
debugging purposes as well.
@ -123,7 +123,7 @@ class HumanReadableException(Exception):
logger.error("{0}: {1}", self.error_kind, self.args[0])
class FilesystemError(HumanReadableException):
class FilesystemError(HumanReadableError):
"""An error that occurred while performing a filesystem manipulation
via a function in this module. The `paths` field is a sequence of
pathnames involved in the operation.
@ -1069,7 +1069,7 @@ def par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:
pool.join()
class cached_classproperty: # noqa: N801
class cached_classproperty:
"""A decorator implementing a read-only property that is *lazy* in
the sense that the getter is only invoked once. Subsequent accesses
through *any* instance use the cached result.

View file

@ -203,7 +203,7 @@ def _event_select(events):
return ready_events
class ThreadException(Exception):
class ThreadError(Exception):
def __init__(self, coro, exc_info):
self.coro = coro
self.exc_info = exc_info
@ -266,7 +266,7 @@ def run(root_coro):
"""After an event is fired, run a given coroutine associated with
it in the threads dict until it yields again. If the coroutine
exits, then the thread is removed from the pool. If the coroutine
raises an exception, it is reraised in a ThreadException. If
raises an exception, it is reraised in a ThreadError. If
is_exc is True, then the value must be an exc_info tuple and the
exception is thrown into the coroutine.
"""
@ -281,7 +281,7 @@ def run(root_coro):
except BaseException:
# Thread raised some other exception.
del threads[coro]
raise ThreadException(coro, sys.exc_info())
raise ThreadError(coro, sys.exc_info())
else:
if isinstance(next_event, types.GeneratorType):
# Automatically invoke sub-coroutines. (Shorthand for
@ -369,7 +369,7 @@ def run(root_coro):
else:
advance_thread(event2coro[event], value)
except ThreadException as te:
except ThreadError as te:
# Exception raised from inside a thread.
event = ExceptionEvent(te.exc_info)
if te.coro in delegators:

View file

@ -1,42 +0,0 @@
# This file is part of beets.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
from enum import Enum
class OrderedEnum(Enum):
"""
An Enum subclass that allows comparison of members.
"""
def __ge__(self, other):
if self.__class__ is other.__class__:
return self.value >= other.value
return NotImplemented
def __gt__(self, other):
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented
def __le__(self, other):
if self.__class__ is other.__class__:
return self.value <= other.value
return NotImplemented
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented

View file

@ -26,7 +26,6 @@ This is sort of like a tiny, horrible degeneration of a real templating
engine like Jinja2 or Mustache.
"""
import ast
import dis
import functools

View file

@ -31,7 +31,6 @@ To do so, pass an iterable of coroutines to the Pipeline constructor
in place of any single coroutine.
"""
import queue
import sys
from threading import Lock, Thread

View file

@ -14,7 +14,6 @@
"""A namespace package for beets plugins."""
# Make this a namespace package.
from pkgutil import extend_path

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Calculate acoustic information and submit to AcousticBrainz.
"""
"""Calculate acoustic information and submit to AcousticBrainz."""
import errno
import hashlib
@ -187,9 +185,9 @@ only files which would be processed",
with open(filename) as tmp_file:
analysis = json.load(tmp_file)
# Add the hash to the output.
analysis["metadata"]["version"][
"essentia_build_sha"
] = self.extractor_sha
analysis["metadata"]["version"]["essentia_build_sha"] = (
self.extractor_sha
)
return analysis
finally:
try:

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Fetch various AcousticBrainz metadata using MBID.
"""
"""Fetch various AcousticBrainz metadata using MBID."""
from collections import defaultdict

View file

@ -14,7 +14,6 @@
"""Adds an album template field for formatted album types."""
from beets.autotag.mb import VARIOUS_ARTISTS_ID
from beets.library import Album
from beets.plugins import BeetsPlugin

View file

@ -14,7 +14,6 @@
"""An AURA server using Flask."""
import os
import re
import sys

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Use command-line tools to check for audio file corruption.
"""
"""Use command-line tools to check for audio file corruption."""
import errno
import os
@ -30,7 +28,7 @@ from beets.ui import Subcommand
from beets.util import displayable_path, par_map
class CheckerCommandException(Exception):
class CheckerCommandError(Exception):
"""Raised when running a checker failed.
Attributes:
@ -70,7 +68,7 @@ class BadFiles(BeetsPlugin):
errors = 1
status = e.returncode
except OSError as e:
raise CheckerCommandException(cmd, e)
raise CheckerCommandError(cmd, e)
output = output.decode(sys.getdefaultencoding(), "replace")
return status, errors, [line for line in output.split("\n") if line]
@ -128,7 +126,7 @@ class BadFiles(BeetsPlugin):
path = item.path.decode(sys.getfilesystemencoding())
try:
status, errors, output = checker(path)
except CheckerCommandException as e:
except CheckerCommandError as e:
if e.errno == errno.ENOENT:
self._log.error(
"command not found: {} when validating file: {}",
@ -200,7 +198,7 @@ class BadFiles(BeetsPlugin):
elif sel == "c":
return None
elif sel == "b":
raise importer.ImportAbort()
raise importer.ImportAbortError()
else:
raise Exception(f"Unexpected selection: {sel}")

View file

@ -18,7 +18,6 @@
"""Provides a bare-ASCII matching query."""
from unidecode import unidecode
from beets import ui

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Adds Beatport release and track search support to the autotagger
"""
"""Adds Beatport release and track search support to the autotagger"""
import json
import re

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Some simple performance benchmarks for beets.
"""
"""Some simple performance benchmarks for beets."""
import cProfile
import timeit

View file

@ -17,7 +17,6 @@ Beets library. Attempts to implement a compatible protocol to allow
use of the wide range of MPD clients.
"""
import inspect
import math
import random
@ -168,13 +167,13 @@ def cast_arg(t, val):
raise ArgumentTypeError()
class BPDClose(Exception):
class BPDCloseError(Exception):
"""Raised by a command invocation to indicate that the connection
should be closed.
"""
class BPDIdle(Exception):
class BPDIdleError(Exception):
"""Raised by a command to indicate the client wants to enter the idle state
and should be notified when a relevant event happens.
"""
@ -349,7 +348,7 @@ class BaseServer:
for system in subsystems:
if system not in SUBSYSTEMS:
raise BPDError(ERROR_ARG, f"Unrecognised idle event: {system}")
raise BPDIdle(subsystems) # put the connection into idle mode
raise BPDIdleError(subsystems) # put the connection into idle mode
def cmd_kill(self, conn):
"""Exits the server process."""
@ -357,7 +356,7 @@ class BaseServer:
def cmd_close(self, conn):
"""Closes the connection."""
raise BPDClose()
raise BPDCloseError()
def cmd_password(self, conn, password):
"""Attempts password authentication."""
@ -739,13 +738,13 @@ class BaseServer:
# Additions to the MPD protocol.
def cmd_crash_TypeError(self, conn): # noqa: N802
def cmd_crash(self, conn):
"""Deliberately trigger a TypeError for testing purposes.
We want to test that the server properly responds with ERROR_SYSTEM
without crashing, and that this is not treated as ERROR_ARG (since it
is caused by a programming error, not a protocol error).
"""
"a" + 2
raise TypeError
class Connection:
@ -773,8 +772,8 @@ class Connection:
if isinstance(lines, str):
lines = [lines]
out = NEWLINE.join(lines) + NEWLINE
for l in out.split(NEWLINE)[:-1]:
self.debug(l, kind=">")
for line in out.split(NEWLINE)[:-1]:
self.debug(line, kind=">")
if isinstance(out, str):
out = out.encode("utf-8")
return self.sock.sendall(out)
@ -853,8 +852,8 @@ class MPDConnection(Connection):
self.disconnect() # Client sent a blank line.
break
line = line.decode("utf8") # MPD protocol uses UTF-8.
for l in line.split(NEWLINE):
self.debug(l, kind="<")
for line in line.split(NEWLINE):
self.debug(line, kind="<")
if self.idle_subscriptions:
# The connection is in idle mode.
@ -888,12 +887,12 @@ class MPDConnection(Connection):
# Ordinary command.
try:
yield bluelet.call(self.do_command(Command(line)))
except BPDClose:
except BPDCloseError:
# Command indicates that the conn should close.
self.sock.close()
self.disconnect() # Client explicitly closed.
return
except BPDIdle as e:
except BPDIdleError as e:
self.idle_subscriptions = e.subsystems
self.debug(
"awaiting: {}".format(" ".join(e.subsystems)), kind="z"
@ -922,8 +921,8 @@ class ControlConnection(Connection):
if not line:
break # Client sent a blank line.
line = line.decode("utf8") # Protocol uses UTF-8.
for l in line.split(NEWLINE):
self.debug(l, kind="<")
for line in line.split(NEWLINE):
self.debug(line, kind="<")
command = Command(line)
try:
func = command.delegate("ctrl_", self)
@ -1046,12 +1045,12 @@ class Command:
e.cmd_name = self.name
raise e
except BPDClose:
except BPDCloseError:
# An indication that the connection should close. Send
# it on the Connection.
raise
except BPDIdle:
except BPDIdleError:
raise
except Exception:

View file

@ -16,7 +16,6 @@
music player.
"""
import _thread
import copy
import os

View file

@ -14,7 +14,6 @@
"""Determine BPM by pressing a key to the rhythm."""
import time
from beets import ui

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Update library's tags using Beatport.
"""
"""Update library's tags using Beatport."""
from beets import autotag, library, ui, util
from beets.plugins import BeetsPlugin, apply_item_changes

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Provides the %bucket{} function for path formatting.
"""
"""Provides the %bucket{} function for path formatting."""
import re
import string

View file

@ -12,8 +12,8 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Converts tracks or albums to external directory
"""
"""Converts tracks or albums to external directory"""
import logging
import os
import shlex
@ -95,8 +95,9 @@ def should_transcode(item, fmt):
query, _ = parse_query_string(query_string, Item)
if query.match(item):
return False
if config["convert"]["never_convert_lossy_files"] and not (
item.format.lower() in LOSSLESS_FORMATS
if (
config["convert"]["never_convert_lossy_files"]
and item.format.lower() not in LOSSLESS_FORMATS
):
return False
maxbr = config["convert"]["max_bitrate"].get(Optional(int))

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Adds Deezer release and track search support to the autotagger
"""
"""Adds Deezer release and track search support to the autotagger"""
import collections
import time
@ -112,8 +111,8 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin):
day = None
else:
raise ui.UserError(
"Invalid `release_date` returned "
"by {} API: '{}'".format(self.data_source, release_date)
f"Invalid `release_date` returned by {self.data_source} API: "
f"{release_date!r}"
)
tracks_obj = self.fetch_data(self.album_url + deezer_id + "/tracks")
if tracks_obj is None:

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""List duplicate tracks or albums.
"""
"""List duplicate tracks or albums."""
import os
import shlex
@ -304,7 +303,9 @@ class DuplicatesPlugin(BeetsPlugin):
kind = "items" if all(isinstance(o, Item) for o in objs) else "albums"
if tiebreak and kind in tiebreak.keys():
key = lambda x: tuple(getattr(x, k) for k in tiebreak[kind])
def key(x):
return tuple(getattr(x, k) for k in tiebreak[kind])
else:
if kind == "items":
@ -317,9 +318,13 @@ class DuplicatesPlugin(BeetsPlugin):
)
fields = Item.all_keys()
key = lambda x: sum(1 for f in fields if truthy(getattr(x, f)))
def key(x):
return sum(1 for f in fields if truthy(getattr(x, f)))
else:
key = lambda x: len(x.items())
def key(x):
return len(x.items())
return sorted(objs, key=key, reverse=True)

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Open metadata information in a text editor to let the user edit it.
"""
"""Open metadata information in a text editor to let the user edit it."""
import codecs
import os

View file

@ -1,11 +1,11 @@
"""Updates the Emby Library whenever the beets library is changed.
emby:
host: localhost
port: 8096
username: user
apikey: apikey
password: password
emby:
host: localhost
port: 8096
username: user
apikey: apikey
password: password
"""
import hashlib

View file

@ -11,9 +11,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Exports data from beets
"""
"""Exports data from beets"""
import codecs
import csv

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Fetches album art.
"""
"""Fetches album art."""
import os
import re

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Filter imported files using a regular expression.
"""
"""Filter imported files using a regular expression."""
import re

View file

@ -22,7 +22,6 @@ by default but can be added via the `-e` / `--extravalues` flag. For example:
`beet fish -e genre -e albumartist`
"""
import os
from operator import attrgetter

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Creates freedesktop.org-compliant .directory files on an album level.
"""
"""Creates freedesktop.org-compliant .directory files on an album level."""
from beets import ui
from beets.plugins import BeetsPlugin

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Moves "featured" artists to the title from the artist field.
"""
"""Moves "featured" artists to the title from the artist field."""
import re

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Provides a fuzzy matching query.
"""
"""Provides a fuzzy matching query."""
import difflib

View file

@ -17,6 +17,7 @@
music player. Also allow printing the new file locations to stdout in case
one wants to manually add music to a player by its path.
"""
import datetime
import os
import re

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Shows file metadata.
"""
"""Shows file metadata."""
import os

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Allows inline path template customization code in the config file.
"""
"""Allows inline path template customization code in the config file."""
import itertools
import traceback

View file

@ -11,9 +11,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Adds support for ipfs. Requires go-ipfs and a running ipfs daemon
"""
"""Adds support for ipfs. Requires go-ipfs and a running ipfs daemon"""
import os
import shutil

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Uses the `KeyFinder` program to add the `initial_key` field.
"""
"""Uses the `KeyFinder` program to add the `initial_key` field."""
import os.path
import subprocess

View file

@ -21,6 +21,7 @@ and has been edited to remove some questionable entries.
The scraper script used is available here:
https://gist.github.com/1241307
"""
import codecs
import os
import traceback

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Load SQLite extensions.
"""
"""Load SQLite extensions."""
import sqlite3

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Fetches, embeds, and displays lyrics.
"""
"""Fetches, embeds, and displays lyrics."""
import difflib
import errno
@ -1063,7 +1061,7 @@ class LyricsPlugin(plugins.BeetsPlugin):
if any(lyrics):
break
lyrics = "\n\n---\n\n".join([l for l in lyrics if l])
lyrics = "\n\n---\n\n".join(filter(None, lyrics))
if lyrics:
self._log.info("fetched lyrics: {0}", item)

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Update library's tags using MusicBrainz.
"""
"""Update library's tags using MusicBrainz."""
import re
from collections import defaultdict

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Synchronize information from music player libraries
"""
"""Synchronize information from music player libraries"""
from abc import ABCMeta, abstractmethod
from importlib import import_module
@ -126,8 +124,7 @@ class MetaSyncPlugin(BeetsPlugin):
meta_source_instances[player] = cls(self.config, self._log)
except (ImportError, ConfigValueError) as e:
self._log.error(
"Failed to instantiate metadata source "
"'{}': {}".format(player, e)
f"Failed to instantiate metadata source {player!r}: {e}"
)
# Avoid needlessly iterating over items

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Synchronize information from amarok's library via dbus
"""
"""Synchronize information from amarok's library via dbus"""
from datetime import datetime
from os.path import basename

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Synchronize information from iTunes's library
"""
"""Synchronize information from iTunes's library"""
import os
import plistlib

View file

@ -13,8 +13,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""List missing tracks.
"""
"""List missing tracks."""
from collections import defaultdict

View file

@ -16,7 +16,6 @@
and work composition date
"""
import musicbrainzngs
from beets import ui

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Send the results of a query to the configured music player as a playlist.
"""
"""Send the results of a query to the configured music player as a playlist."""
import shlex
import subprocess
@ -197,7 +196,7 @@ class PlayPlugin(BeetsPlugin):
filename = get_temp_filename(__name__, suffix=".m3u")
with open(filename, "wb") as m3u:
if utf8_bom:
m3u.write(b"\xEF\xBB\xBF")
m3u.write(b"\xef\xbb\xbf")
for item in paths_list:
m3u.write(item + b"\n")

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Get a random song or album from the library.
"""
"""Get a random song or album from the library."""
from beets.plugins import BeetsPlugin
from beets.random import random_objs

View file

@ -334,9 +334,7 @@ class FfmpegBackend(Backend):
task.target_level,
task.peak_method,
count_blocks=False,
)[
0
] # take only the gain, discarding number of gating blocks
)[0] # take only the gain, discarding number of gating blocks
for item in task.items
]

View file

@ -16,7 +16,6 @@
automatically whenever tags are written.
"""
import mediafile
import mutagen

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Generates smart playlists based on beets queries.
"""
"""Generates smart playlists based on beets queries."""
import json
import os

View file

@ -14,7 +14,6 @@
"""Moves patterns in path formats (suitable for moving articles)."""
import re
from typing import List

View file

@ -18,7 +18,6 @@ This plugin is POSIX-only.
Spec: standards.freedesktop.org/thumbnail-spec/latest/index.html
"""
import ctypes
import ctypes.util
import os
@ -280,8 +279,7 @@ class GioURI(URIGetter):
if not uri_ptr:
self.libgio.g_free(uri_ptr)
raise RuntimeError(
"No URI received from the gfile pointer for "
"{}".format(displayable_path(path))
f"No URI received from the gfile pointer for {displayable_path(path)}"
)
try:

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
""" Clears tag fields in media files."""
"""Clears tag fields in media files."""
import re

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""A utility script for automating the beets release process.
"""
"""A utility script for automating the beets release process."""
from __future__ import annotations
import re

166
poetry.lock generated
View file

@ -110,52 +110,6 @@ charset-normalizer = ["charset-normalizer"]
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "black"
version = "24.8.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
{file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"},
{file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"},
{file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"},
{file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"},
{file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"},
{file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"},
{file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"},
{file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"},
{file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"},
{file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"},
{file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"},
{file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"},
{file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"},
{file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"},
{file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"},
{file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"},
{file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"},
{file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"},
{file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"},
{file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "blinker"
version = "1.8.2"
@ -678,22 +632,6 @@ files = [
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "flask"
version = "3.0.3"
@ -940,23 +878,6 @@ files = [
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.dependencies]
colorama = {version = ">=0.4.6", optional = true, markers = "extra == \"colors\""}
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "itsdangerous"
version = "2.2.0"
@ -1398,17 +1319,6 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mediafile"
version = "0.12.0"
@ -1714,31 +1624,6 @@ files = [
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "pep8-naming"
version = "0.14.1"
description = "Check PEP-8 naming conventions, plugin for flake8"
optional = false
python-versions = ">=3.8"
files = [
{file = "pep8-naming-0.14.1.tar.gz", hash = "sha256:1ef228ae80875557eb6c1549deafed4dabbf3261cfcafa12f773fe0db9be8a36"},
{file = "pep8_naming-0.14.1-py3-none-any.whl", hash = "sha256:63f514fc777d715f935faf185dedd679ab99526a7f2f503abb61587877f7b1c5"},
]
[package.dependencies]
flake8 = ">=5.0.0"
[[package]]
name = "pillow"
version = "10.4.0"
@ -2039,17 +1924,6 @@ files = [
{file = "pycairo-1.26.1.tar.gz", hash = "sha256:a11b999ce55b798dbf13516ab038e0ce8b6ec299b208d7c4e767a6f7e68e8430"},
]
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]]
name = "pycparser"
version = "2.22"
@ -2129,17 +2003,6 @@ dev = ["nox", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml"]
doc = ["ablog (>=0.11.0rc2)", "colorama", "ipykernel", "ipyleaflet", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (>=3.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube (<1.4)", "sphinxext-rediraffe", "xarray"]
test = ["pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]]
name = "pygments"
version = "2.18.0"
@ -2664,6 +2527,33 @@ urllib3 = ">=1.25.10,<3.0"
[package.extras]
tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"]
[[package]]
name = "ruff"
version = "0.6.6"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
]
[[package]]
name = "scikit-learn"
version = "1.3.2"
@ -3242,4 +3132,4 @@ web = ["flask", "flask-cors"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8,<4"
content-hash = "09b64b25f3ff363a3c31d44fd99e4c07309b27dda65523f9ec3d1279ca3a3143"
content-hash = "b3eb66c6852cb14afcbe817619ea9ecea699e838885d8e9719f2809eb708bcc3"

View file

@ -84,13 +84,8 @@ rarfile = "*"
requests_oauthlib = "*"
responses = ">=0.3.0"
[tool.poetry.group.format.dependencies]
isort = { version = "<5.14", extras = ["colors"] }
black = ">=24.3,<25"
[tool.poetry.group.lint.dependencies]
flake8 = "*"
pep8-naming = "*"
ruff = ">=0.6.4"
[tool.poetry.group.typing.dependencies]
mypy = "*"
@ -154,24 +149,6 @@ build-backend = "poetry.core.masonry.api"
poethepoet = ">=0.26"
poetry = ">=1.8"
# We use a default path '.' to make black and isort behave like flake8 and
# mypy do: they act on the entire codebase (flake8 does it by default, and
# mypy follows our configuration) by default. Positional command-line arguments
# override this. Therefore, locally you can run `poe check-format <some-path>`
# to quickly check a specific path.
#
# Note: both tools respect .gitignore, therefore if we see them format
# something unwanted locally, we should add these paths to .gitignore.
[tool.poe.tasks._black]
help = "Run black"
cmd = "black $OPTS $path"
args = { path = { help = "Path to blacken", positional = true, multiple = true, default = "." } }
[tool.poe.tasks._isort]
help = "Run isort"
cmd = "isort $OPTS $path"
args = { path = { help = "Path to isort", positional = true, multiple = true, default = "." } }
[tool.poe.tasks.bump]
help = "Bump project version and update relevant files"
cmd = "python ./extra/release.py bump $version"
@ -187,8 +164,7 @@ cmd = "make -C docs linkcheck"
[tool.poe.tasks.check-format]
help = "Check the code for style issues"
ref = "format"
env.OPTS = "--check --diff --color"
cmd = "ruff format --check --diff"
[tool.poe.tasks.check-types]
help = "Check the code for typing issues. Accepts mypy options."
@ -200,13 +176,11 @@ cmd = "make -C docs html"
[tool.poe.tasks.format]
help = "Format the codebase"
ignore_fail = "return_non_zero"
sequence = ["_black $path", "_isort $path"]
args = { path = { help = "Path to format", positional = true, multiple = true, default = "." } }
cmd = "ruff format"
[tool.poe.tasks.lint]
help = "Check the code for linting issues. Accepts flake8 options."
cmd = "flake8"
help = "Check the code for linting issues. Accepts ruff options."
cmd = "ruff check"
[tool.poe.tasks.update-dependencies]
help = "Update dependencies to their latest versions."
@ -249,13 +223,41 @@ done
"""
interpreter = "zsh"
[tool.black]
[tool.ruff]
target-version = "py38"
line-length = 80
target-version = ["py38", "py39", "py310", "py311"]
[tool.isort]
profile = "black"
py_version = 38
multi_line_output = 3
line_length = 80
indent = 4
[tool.ruff.lint]
select = [
# "ARG", # flake8-unused-arguments
# "C4", # flake8-comprehensions
"E", # pycodestyle
"F", # pyflakes
# "B", # flake8-bugbear
"I", # isort
"N", # pep8-naming
"PT", # flake8-pytest-style
# "RUF", # ruff
# "UP", # pyupgrade
"W", # pycodestyle
]
[tool.ruff.lint.per-file-ignores]
"beets/**" = ["PT"]
[tool.ruff.lint.isort]
split-on-trailing-comma = false
[tool.ruff.lint.pycodestyle]
max-line-length = 88
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
parametrize-names-type = "csv"
[tool.ruff.lint.flake8-unused-arguments]
ignore-variadic-names = true
[tool.ruff.lint.pep8-naming]
classmethod-decorators = ["cached_classproperty"]
extend-ignore-names = ["assert*", "cached_classproperty"]

View file

@ -7,6 +7,8 @@ addopts =
# show all skipped/failed/xfailed tests in the summary except passed
-ra
--strict-config
markers =
integration_test: mark a test as an integration test
[coverage:run]
data_file = .reports/coverage/data
@ -27,41 +29,6 @@ exclude_lines =
[coverage:html]
show_contexts = true
[flake8]
min-version = 3.8
accept-encodings = utf-8
max-line-length = 88
classmethod-decorators =
classmethod
cached_classproperty
# errors we ignore; see https://www.flake8rules.com/ for more info
ignore =
# pycodestyle errors
# continuation line under-indented for hanging indent
E121,
# closing bracket does not match indentation of opening bracket's line
E123,
# continuation line over-indented for hanging indent
E126,
# multiple spaces after non-arithmetic operators (for vertical alignment)
E241,
# expected 2 blank lines after end of function or class
E305,
# do not assign a lambda expression, use a def
E731,
# do not use variables name 'I', 'O', or 'l'
E741,
# pycodestyle warnings: line breaks around binary operators
W503,
W504,
# mccabe errors: function is too complex
C901,
# Exception subclasses should be named with an Error suffix
N818,
# Exclude rules for black compatibility
E203,
E704,
[mypy]
files = beets,beetsplug,test,extra,docs
allow_any_generics = false

12
test/conftest.py Normal file
View file

@ -0,0 +1,12 @@
import os
import pytest
def pytest_runtest_setup(item: pytest.Item):
"""Skip integration tests if INTEGRATION_TEST environment variable is not set."""
if os.environ.get("INTEGRATION_TEST"):
return
if next(item.iter_markers(name="integration_test"), None):
pytest.skip(f"INTEGRATION_TEST=1 required: {item.nodeid}")

View file

@ -15,10 +15,11 @@
import os
import sys
from test.plugins import test_lyrics
import requests
from test.plugins import test_lyrics
def mkdir_p(path):
try:

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the 'acousticbrainz' plugin.
"""
"""Tests for the 'acousticbrainz' plugin."""
import json
import os.path

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Test the advancedrewrite plugin for various configurations.
"""
"""Test the advancedrewrite plugin for various configurations."""
import pytest

View file

@ -14,7 +14,6 @@
"""Tests for the 'albumtypes' plugin."""
from typing import Sequence, Tuple
from beets.autotag.mb import VARIOUS_ARTISTS_ID

View file

@ -14,7 +14,6 @@
"""Tests for the album art fetchers."""
import os
import shutil
from unittest.mock import patch
@ -866,7 +865,7 @@ class ArtForAlbumTest(UseThePlugin):
fetchart.FileSystem.get = self.old_fs_source_get
super().tearDown()
def _assertImageIsValidArt(self, image_file, should_exist): # noqa
def assertImageIsValidArt(self, image_file, should_exist):
self.assertExists(image_file)
self.image_file = image_file
@ -897,42 +896,42 @@ class ArtForAlbumTest(UseThePlugin):
def test_respect_minwidth(self):
self._require_backend()
self.plugin.minwidth = 300
self._assertImageIsValidArt(self.IMG_225x225, False)
self._assertImageIsValidArt(self.IMG_348x348, True)
self.assertImageIsValidArt(self.IMG_225x225, False)
self.assertImageIsValidArt(self.IMG_348x348, True)
def test_respect_enforce_ratio_yes(self):
self._require_backend()
self.plugin.enforce_ratio = True
self._assertImageIsValidArt(self.IMG_500x490, False)
self._assertImageIsValidArt(self.IMG_225x225, True)
self.assertImageIsValidArt(self.IMG_500x490, False)
self.assertImageIsValidArt(self.IMG_225x225, True)
def test_respect_enforce_ratio_no(self):
self.plugin.enforce_ratio = False
self._assertImageIsValidArt(self.IMG_500x490, True)
self.assertImageIsValidArt(self.IMG_500x490, True)
def test_respect_enforce_ratio_px_above(self):
self._require_backend()
self.plugin.enforce_ratio = True
self.plugin.margin_px = 5
self._assertImageIsValidArt(self.IMG_500x490, False)
self.assertImageIsValidArt(self.IMG_500x490, False)
def test_respect_enforce_ratio_px_below(self):
self._require_backend()
self.plugin.enforce_ratio = True
self.plugin.margin_px = 15
self._assertImageIsValidArt(self.IMG_500x490, True)
self.assertImageIsValidArt(self.IMG_500x490, True)
def test_respect_enforce_ratio_percent_above(self):
self._require_backend()
self.plugin.enforce_ratio = True
self.plugin.margin_percent = (500 - 490) / 500 * 0.5
self._assertImageIsValidArt(self.IMG_500x490, False)
self.assertImageIsValidArt(self.IMG_500x490, False)
def test_respect_enforce_ratio_percent_below(self):
self._require_backend()
self.plugin.enforce_ratio = True
self.plugin.margin_percent = (500 - 490) / 500 * 1.5
self._assertImageIsValidArt(self.IMG_500x490, True)
self.assertImageIsValidArt(self.IMG_500x490, True)
def test_resize_if_necessary(self):
self._require_backend()
@ -949,7 +948,7 @@ class ArtForAlbumTest(UseThePlugin):
self._require_backend()
self.plugin.max_filesize = self.IMG_225x225_SIZE
self._assert_image_operated(self.IMG_225x225, self.RESIZE_OP, False)
self._assertImageIsValidArt(self.IMG_225x225, True)
self.assertImageIsValidArt(self.IMG_225x225, True)
def test_fileresize_no_scale(self):
self._require_backend()
@ -995,7 +994,7 @@ class DeprecatedConfigTest(BeetsTestCase):
self.plugin = fetchart.FetchArtPlugin()
def test_moves_filesystem_to_end(self):
assert type(self.plugin.sources[-1]) == fetchart.FileSystem
assert isinstance(self.plugin.sources[-1], fetchart.FileSystem)
class EnforceRatioConfigTest(BeetsTestCase):

View file

@ -3,7 +3,6 @@
"""Tests for the 'bareasc' plugin."""
from beets import logging
from beets.test.helper import PluginTestCase, capture_stdout

View file

@ -14,7 +14,6 @@
"""Tests for the 'bucket' plugin."""
import pytest
from beets import config, ui

View file

@ -55,7 +55,7 @@ class ConvertMixin:
shell_quote(sys.executable), shell_quote(stub), tag
)
def assertFileTag(self, path, tag): # noqa
def assertFileTag(self, path, tag):
"""Assert that the path is a file and the files content ends
with `tag`.
"""
@ -68,7 +68,7 @@ class ConvertMixin:
f.read() == tag
), f"{displayable_path(path)} is not tagged with {display_tag}"
def assertNoFileTag(self, path, tag): # noqa
def assertNoFileTag(self, path, tag):
"""Assert that the path is a file and the files content does not
end with `tag`.
"""

View file

@ -77,7 +77,7 @@ class EditMixin(PluginMixin):
plugin = "edit"
def assertItemFieldsModified( # noqa
def assertItemFieldsModified(
self, library_items, items, fields=[], allowed=["path"]
):
"""Assert that items in the library (`lib_items`) have different values
@ -134,7 +134,7 @@ class EditCommandTest(EditMixin, BeetsTestCase):
{f: item[f] for f in item._fields} for item in self.album.items()
]
def assertCounts( # noqa
def assertCounts(
self,
mock_write,
album_count=ALBUM_COUNT,

View file

@ -17,7 +17,6 @@ import os.path
import shutil
import tempfile
import unittest
from test.test_art_resize import DummyIMBackend
from unittest.mock import MagicMock, patch
import pytest
@ -28,6 +27,7 @@ from beets.test import _common
from beets.test.helper import BeetsTestCase, FetchImageHelper, PluginMixin
from beets.util import bytestring_path, displayable_path, syspath
from beets.util.artresizer import ArtResizer
from test.test_art_resize import DummyIMBackend
def require_artresizer_compare(test):

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Test the beets.export utilities associated with the export plugin.
"""
"""Test the beets.export utilities associated with the export plugin."""
import json
import re # used to test csv format

View file

@ -12,8 +12,8 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests for the `filefilter` plugin.
"""
"""Tests for the `filefilter` plugin."""
from beets.test.helper import ImportTestCase, PluginMixin
from beets.util import bytestring_path

View file

@ -14,7 +14,6 @@
"""Tests for the 'ftintitle' plugin."""
import unittest
from beets.test.helper import PluginTestCase
@ -83,14 +82,14 @@ class FtInTitlePluginFunctional(PluginTestCase):
item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice")
self.run_command("ftintitle")
item.load()
self.assertEqual(item["artist"], "Alice ft Bob")
self.assertEqual(item["title"], "Song 1 feat. Bob")
assert item["artist"] == "Alice ft Bob"
assert item["title"] == "Song 1 feat. Bob"
item = self._ft_add_item("/", "Alice ft Bob", "Song 1", "Alice")
self.run_command("ftintitle", "-d")
item.load()
self.assertEqual(item["artist"], "Alice ft Bob")
self.assertEqual(item["title"], "Song 1")
assert item["artist"] == "Alice ft Bob"
assert item["title"] == "Song 1"
class FtInTitlePluginTest(unittest.TestCase):

View file

@ -74,11 +74,11 @@ class ImportAddedTest(PluginMixin, ImportTestCase):
"No MediaFile found for Item " + displayable_path(item.path)
)
def assertEqualTimes(self, first, second, msg=None): # noqa
def assertEqualTimes(self, first, second, msg=None):
"""For comparing file modification times at a sufficient precision"""
assert first == pytest.approx(second, rel=1e-4), msg
def assertAlbumImport(self): # noqa
def assertAlbumImport(self):
self.importer.run()
album = self.lib.albums().get()
assert album.added == self.min_mtime

View file

@ -13,7 +13,6 @@
"""Tests for the 'limit' plugin."""
from beets.test.helper import PluginTestCase

View file

@ -14,7 +14,6 @@
"""Tests for the 'lyrics' plugin."""
import itertools
import os
import re
@ -22,6 +21,7 @@ import unittest
from unittest.mock import MagicMock, patch
import confuse
import pytest
import requests
from beets import logging
@ -336,10 +336,7 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest, LyricsAssertions):
LyricsGoogleBaseTest.setUp(self)
self.plugin = lyrics.LyricsPlugin()
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_backend_sources_ok(self):
"""Test default backends with songs known to exist in respective
databases.
@ -352,10 +349,7 @@ class LyricsPluginSourcesTest(LyricsGoogleBaseTest, LyricsAssertions):
res = backend.fetch(s["artist"], s["title"])
self.assertLyricsContentOk(s["title"], res)
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_google_sources_ok(self):
"""Test if lyrics present on websites registered in beets google custom
search engine are correctly scraped.
@ -509,14 +503,14 @@ class GeniusFetchTest(GeniusBaseTest):
{
"result": {
"primary_artist": {
"name": "\u200Bblackbear",
"name": "\u200bblackbear",
},
"url": "blackbear_url",
}
},
{
"result": {
"primary_artist": {"name": "El\u002Dp"},
"primary_artist": {"name": "El\u002dp"},
"url": "El-p_url",
}
},
@ -650,19 +644,13 @@ class TekstowoIntegrationTest(TekstowoBaseTest, LyricsAssertions):
self.plugin = lyrics.LyricsPlugin()
tekstowo.config = self.plugin.config
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_normal(self):
"""Ensure we can fetch a song's lyrics in the ordinary case"""
lyrics = tekstowo.fetch("Boy in Space", "u n eye")
self.assertLyricsContentOk("u n eye", lyrics)
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_no_matching_results(self):
"""Ensure we fetch nothing if there are search results
returned but no matches"""
@ -737,28 +725,19 @@ class LRCLibIntegrationTest(LyricsAssertions):
self.plugin = lyrics.LyricsPlugin()
lrclib.config = self.plugin.config
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_track_with_lyrics(self):
lyrics = lrclib.fetch("Boy in Space", "u n eye", "Live EP", 160)
self.assertLyricsContentOk("u n eye", lyrics)
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_instrumental_track(self):
lyrics = lrclib.fetch(
"Kelly Bailey", "Black Mesa Inbound", "Half Life 2 Soundtrack", 134
)
assert lyrics is None
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
@pytest.mark.integration_test
def test_nonexistent_track(self):
lyrics = lrclib.fetch("blah", "blah", "blah", 999)
assert lyrics is None
@ -786,10 +765,10 @@ class SlugTests(unittest.TestCase):
assert lyrics.slug(text) == "cafe-au-lait-boisson"
text = "Multiple spaces -- and symbols! -- merged"
assert lyrics.slug(text) == "multiple-spaces-and-symbols-merged"
text = "\u200Bno-width-space"
text = "\u200bno-width-space"
assert lyrics.slug(text) == "no-width-space"
# variations of dashes should get standardized
dashes = ["\u200D", "\u2010"]
dashes = ["\u200d", "\u2010"]
for dash1, dash2 in itertools.combinations(dashes, 2):
assert lyrics.slug(dash1) == lyrics.slug(dash2)

View file

@ -14,11 +14,10 @@
"""Tests for the 'parentwork' plugin."""
import os
import unittest
from unittest.mock import patch
import pytest
from beets.library import Item
from beets.test.helper import PluginTestCase
from beetsplug import parentwork
@ -85,14 +84,11 @@ def mock_workid_response(mbid, includes):
return p_work
@pytest.mark.integration_test
class ParentWorkIntegrationTest(PluginTestCase):
plugin = "parentwork"
# test how it works with real musicbrainz data
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
def test_normal_case_real(self):
item = Item(
path="/file",
@ -107,10 +103,6 @@ class ParentWorkIntegrationTest(PluginTestCase):
item.load()
assert item["mb_parentworkid"] == "32c8943f-1b27-3a23-8660-4567f4847c94"
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
def test_force_real(self):
self.config["parentwork"]["force"] = True
item = Item(
@ -128,10 +120,6 @@ class ParentWorkIntegrationTest(PluginTestCase):
item.load()
assert item["mb_parentworkid"] == "32c8943f-1b27-3a23-8660-4567f4847c94"
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
def test_no_force_real(self):
self.config["parentwork"]["force"] = False
item = Item(
@ -153,10 +141,6 @@ class ParentWorkIntegrationTest(PluginTestCase):
# test different cases, still with Matthew Passion Ouverture or Mozart
# requiem
@unittest.skipUnless(
os.environ.get("INTEGRATION_TEST", "0") == "1",
"integration testing not enabled",
)
def test_direct_parent_work_real(self):
mb_workid = "2e4a3668-458d-3b2a-8be2-0b08e0d8243a"
assert (

View file

@ -1,5 +1,4 @@
"""Tests for the 'permissions' plugin.
"""
"""Tests for the 'permissions' plugin."""
import os
import platform
@ -63,7 +62,7 @@ class PermissionsPluginTest(AsIsImporterMixin, PluginMixin, ImportTestCase):
for path in dirs_in_library(self.lib.directory, item.path):
self.assertPerms(path, "dir", expect_success)
def assertPerms(self, path, typ, expect_success): # noqa
def assertPerms(self, path, typ, expect_success):
for x in [
(True, self.exp_perms[expect_success][typ], "!="),
(False, self.exp_perms[not expect_success][typ], "=="),

View file

@ -14,7 +14,6 @@
"""Tests for the play plugin"""
import os
import sys
import unittest

View file

@ -41,7 +41,7 @@ gstplayer = importlib.util.module_from_spec(
)
def _gstplayer_play(*_): # noqa: 42
def _gstplayer_play(*_):
bpd.gstplayer._GstPlayer.playing = True
return mock.DEFAULT
@ -242,7 +242,7 @@ class MPCClient:
return line
def implements(commands, expectedFailure=False): # noqa: N803
def implements(commands, fail=False):
def _test(self):
with self.run_bpd() as client:
response = client.send_command("commands")
@ -250,7 +250,7 @@ def implements(commands, expectedFailure=False): # noqa: N803
implemented = response.data["command"]
assert commands.intersection(implemented) == commands
return unittest.expectedFailure(_test) if expectedFailure else _test
return unittest.expectedFailure(_test) if fail else _test
bluelet_listener = bluelet.Listener
@ -437,7 +437,7 @@ class BPDTest(BPDTestHelper):
def test_system_error(self):
with self.run_bpd() as client:
response = client.send_command("crash_TypeError")
response = client.send_command("crash")
self._assert_failed(response, bpd.ERROR_SYSTEM)
def test_empty_request(self):
@ -763,7 +763,7 @@ class BPDControlTest(BPDTestHelper):
"seekid",
"seekcur",
},
expectedFailure=True,
fail=True,
)
def test_cmd_play(self):
@ -873,7 +873,7 @@ class BPDQueueTest(BPDTestHelper):
"addtagid",
"cleartagid",
},
expectedFailure=True,
fail=True,
)
METADATA = {"Pos", "Time", "Id", "file", "duration"}
@ -990,7 +990,7 @@ class BPDDatabaseTest(BPDTestHelper):
"update",
"rescan",
},
expectedFailure=True,
fail=True,
)
def test_cmd_search(self):
@ -1050,7 +1050,7 @@ class BPDMountsTest(BPDTestHelper):
"listmounts",
"listneighbors",
},
expectedFailure=True,
fail=True,
)
@ -1059,7 +1059,7 @@ class BPDStickerTest(BPDTestHelper):
{
"sticker",
},
expectedFailure=True,
fail=True,
)
@ -1142,7 +1142,7 @@ class BPDPartitionTest(BPDTestHelper):
"listpartitions",
"newpartition",
},
expectedFailure=True,
fail=True,
)
@ -1154,7 +1154,7 @@ class BPDDeviceTest(BPDTestHelper):
"toggleoutput",
"outputs",
},
expectedFailure=True,
fail=True,
)
@ -1166,7 +1166,7 @@ class BPDReflectionTest(BPDTestHelper):
"notcommands",
"urlhandlers",
},
expectedFailure=True,
fail=True,
)
def test_cmd_decoders(self):
@ -1187,5 +1187,5 @@ class BPDPeersTest(BPDTestHelper):
"readmessages",
"sendmessage",
},
expectedFailure=True,
fail=True,
)

View file

@ -12,8 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Tests the facility that lets plugins add custom field to MediaFile.
"""
"""Tests the facility that lets plugins add custom field to MediaFile."""
import os
import shutil

View file

@ -12,9 +12,7 @@
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
"""Test the beets.random utilities associated with the random plugin.
"""
"""Test the beets.random utilities associated with the random plugin."""
import math
import unittest

View file

@ -33,9 +33,9 @@ from beetsplug.thumbnails import (
class ThumbnailsTest(BeetsTestCase):
@patch("beetsplug.thumbnails.ArtResizer")
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok")
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
@patch("beetsplug.thumbnails.os.stat")
def test_add_tags(self, mock_stat, _, mock_artresizer):
def test_add_tags(self, mock_stat, mock_artresizer):
plugin = ThumbnailsPlugin()
plugin.get_uri = Mock(
side_effect={b"/path/to/cover": "COVER_URI"}.__getitem__
@ -98,13 +98,13 @@ class ThumbnailsTest(BeetsTestCase):
giouri_inst.available = False
assert ThumbnailsPlugin().get_uri.__self__.__class__ == PathlibURI
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok")
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
@patch("beetsplug.thumbnails.ArtResizer")
@patch("beetsplug.thumbnails.util")
@patch("beetsplug.thumbnails.os")
@patch("beetsplug.thumbnails.shutil")
def test_make_cover_thumbnail(
self, mock_shutils, mock_os, mock_util, mock_artresizer, _
self, mock_shutils, mock_os, mock_util, mock_artresizer
):
thumbnail_dir = os.path.normpath(b"/thumbnail/dir")
md5_file = os.path.join(thumbnail_dir, b"md5")
@ -166,8 +166,8 @@ class ThumbnailsTest(BeetsTestCase):
plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
mock_resize.assert_called_once_with(12345, path_to_art, md5_file)
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok")
def test_make_dolphin_cover_thumbnail(self, _):
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
def test_make_dolphin_cover_thumbnail(self):
plugin = ThumbnailsPlugin()
tmp = bytestring_path(mkdtemp())
album = Mock(path=tmp, artpath=os.path.join(tmp, b"cover.jpg"))
@ -189,9 +189,9 @@ class ThumbnailsTest(BeetsTestCase):
rmtree(syspath(tmp))
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok")
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
@patch("beetsplug.thumbnails.ArtResizer")
def test_process_album(self, mock_artresizer, _):
def test_process_album(self, mock_artresizer):
get_size = mock_artresizer.shared.get_size
plugin = ThumbnailsPlugin()
@ -234,9 +234,9 @@ class ThumbnailsTest(BeetsTestCase):
any_order=True,
)
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok")
@patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
@patch("beetsplug.thumbnails.decargs")
def test_invokations(self, mock_decargs, _):
def test_invokations(self, mock_decargs):
plugin = ThumbnailsPlugin()
plugin.process_album = Mock()
album = Mock()

Some files were not shown because too many files have changed in this diff Show more