From 12eeaeeae1fbb4cbaf020de7cd5e78faa8e99d13 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 29 Apr 2023 11:11:51 +0200 Subject: [PATCH] Add black and isort to toolchain Signed-off-by: Thomas Scholtes --- .flake8 | 3 + .github/workflows/main.yaml | 2 + DEVELOPING.md | 2 + beetsplug/__init__.py | 1 + beetsplug/alternatives.py | 198 ++++++++-------- poetry.lock | 141 +++++++++++- pyproject.toml | 5 + test/cli_test.py | 436 ++++++++++++++++++------------------ test/helper.py | 225 +++++++++---------- 9 files changed, 579 insertions(+), 434 deletions(-) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..8dd399a --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 35923c1..6831505 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -14,6 +14,8 @@ jobs: python-version: 3.8 cache: poetry - run: poetry install + - run: poetry run black --check . + - run: poetry run isort --check . - run: poetry run flake8 - run: poetry run pytest - uses: coverallsapp/github-action@v2 diff --git a/DEVELOPING.md b/DEVELOPING.md index be0eb7d..ae65619 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -4,6 +4,8 @@ This project uses [Poetry][] for packaging and dependency management. We’re using the following tools to ensure consistency and quality +- [black](https://github.com/psf/black) +- [isort](https://github.com/PyCQA/isort) - [flake8](https://github.com/PyCQA/flake8) - [pytest](https://docs.pytest.org/) diff --git a/beetsplug/__init__.py b/beetsplug/__init__.py index 3ad9513..b36383a 100644 --- a/beetsplug/__init__.py +++ b/beetsplug/__init__.py @@ -1,2 +1,3 @@ from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/beetsplug/alternatives.py b/beetsplug/alternatives.py index 0ce0af9..913ac1f 100644 --- a/beetsplug/alternatives.py +++ b/beetsplug/alternatives.py @@ -12,21 +12,25 @@ # all copies or substantial portions of the Software. +import argparse import os.path import threading -import argparse -from concurrent import futures -import six import traceback +from concurrent import futures import beets -from beets import util, art +import six +from beets import art, util +from beets.library import Item, parse_query_string from beets.plugins import BeetsPlugin -from beets.ui import Subcommand, get_path_formats, input_yn, UserError, \ - print_, decargs -from beets.library import parse_query_string, Item -from beets.util import syspath, displayable_path, cpu_count, bytestring_path, \ - FilesystemError +from beets.ui import Subcommand, UserError, decargs, get_path_formats, input_yn, print_ +from beets.util import ( + FilesystemError, + bytestring_path, + cpu_count, + displayable_path, + syspath, +) from beetsplug import convert @@ -43,11 +47,10 @@ def _remove(path, soft=True): try: os.remove(path) except (OSError, IOError) as exc: - raise FilesystemError(exc, 'delete', (path,), traceback.format_exc()) + raise FilesystemError(exc, "delete", (path,), traceback.format_exc()) class AlternativesPlugin(BeetsPlugin): - def __init__(self): super(AlternativesPlugin, self).__init__() @@ -58,13 +61,12 @@ def update(self, lib, options): try: alt = self.alternative(options.name, lib) except KeyError as e: - raise UserError(u"Alternative collection '{0}' not found." - .format(e.args[0])) + raise UserError("Alternative collection '{0}' not found.".format(e.args[0])) alt.update(create=options.create) def list_tracks(self, lib, options): if options.format is not None: - fmt, = decargs([options.format]) + (fmt,) = decargs([options.format]) beets.config[beets.library.Item._format_config_key].set(fmt) alt = self.alternative(options.name, lib) @@ -80,9 +82,9 @@ def alternative(self, name, lib): if not conf.exists(): raise KeyError(name) - if conf['formats'].exists(): - fmt = conf['formats'].as_str() - if fmt == u'link': + if conf["formats"].exists(): + fmt = conf["formats"].as_str() + if fmt == "link": return SymlinkView(self._log, name, lib, conf) else: return ExternalConvert(self._log, name, fmt.split(), lib, conf) @@ -91,40 +93,39 @@ def alternative(self, name, lib): class AlternativesCommand(Subcommand): - - name = 'alt' - help = 'manage alternative files' + name = "alt" + help = "manage alternative files" def __init__(self, plugin): parser = ArgumentParser() - subparsers = parser.add_subparsers(prog=parser.prog + ' alt') + subparsers = parser.add_subparsers(prog=parser.prog + " alt") subparsers.required = True - update = subparsers.add_parser('update') + update = subparsers.add_parser("update") update.set_defaults(func=plugin.update) - update.add_argument('name', metavar='NAME') - update.add_argument('--create', action='store_const', - dest='create', const=True) - update.add_argument('--no-create', action='store_const', - dest='create', const=False) + update.add_argument("name", metavar="NAME") + update.add_argument("--create", action="store_const", dest="create", const=True) + update.add_argument( + "--no-create", action="store_const", dest="create", const=False + ) list_tracks = subparsers.add_parser( - 'list-tracks', + "list-tracks", description=""" List all tracks that are currently part of an alternative collection""", ) list_tracks.set_defaults(func=plugin.list_tracks) list_tracks.add_argument( - 'name', - metavar='NAME', - help='Name of the alternative', + "name", + metavar="NAME", + help="Name of the alternative", ) list_tracks.add_argument( - '-f', - '--format', - metavar='FORMAT', - dest='format', + "-f", + "--format", + metavar="FORMAT", + dest="format", help="""Format string to print for each track. See beets’ Path Formats for more information.""", ) @@ -150,7 +151,6 @@ def _get_all_options(self): class External(object): - ADD = 1 REMOVE = 2 WRITE = 3 @@ -161,22 +161,22 @@ def __init__(self, log, name, lib, config): self._log = log self.name = name self.lib = lib - self.path_key = u'alt.{0}'.format(name) + self.path_key = "alt.{0}".format(name) self.parse_config(config) def parse_config(self, config): - if 'paths' in config: - path_config = config['paths'] + if "paths" in config: + path_config = config["paths"] else: - path_config = beets.config['paths'] + path_config = beets.config["paths"] self.path_formats = get_path_formats(path_config) - query = config['query'].as_str() + query = config["query"].as_str() self.query, _ = parse_query_string(query, Item) - self.removable = config.get(dict).get('removable', True) + self.removable = config.get(dict).get("removable", True) - if 'directory' in config: - dir = config['directory'].as_str() + if "directory" in config: + dir = config["directory"].as_str() else: dir = self.name dir = bytestring_path(dir) @@ -185,7 +185,7 @@ def parse_config(self, config): self.directory = dir def item_change_actions(self, item, path, dest): - """ Returns the necessary actions for items that were previously in the + """Returns the necessary actions for items that were previously in the external collection, but might require metadata updates. """ actions = [] @@ -194,15 +194,16 @@ def item_change_actions(self, item, path, dest): actions.append(self.MOVE) item_mtime_alt = os.path.getmtime(syspath(path)) - if (item_mtime_alt < os.path.getmtime(syspath(item.path))): + if item_mtime_alt < os.path.getmtime(syspath(item.path)): actions.append(self.WRITE) album = item.get_album() if album: - if (album.artpath and - os.path.isfile(syspath(album.artpath)) and - (item_mtime_alt - < os.path.getmtime(syspath(album.artpath)))): + if ( + album.artpath + and os.path.isfile(syspath(album.artpath)) + and (item_mtime_alt < os.path.getmtime(syspath(album.artpath))) + ): actions.append(self.SYNC_ART) return actions @@ -240,27 +241,31 @@ def ask_create(self, create=None): if create is not None: return create - msg = u"Collection at '{0}' does not exists. " \ - "Maybe you forgot to mount it.\n" \ - "Do you want to create the collection? (y/n)" \ - .format(displayable_path(self.directory)) + msg = ( + "Collection at '{0}' does not exists. " + "Maybe you forgot to mount it.\n" + "Do you want to create the collection? (y/n)".format( + displayable_path(self.directory) + ) + ) return input_yn(msg, require=True) def update(self, create=None): - if (not os.path.isdir(syspath(self.directory)) - and not self.ask_create(create)): - print_(u'Skipping creation of {0}' - .format(displayable_path(self.directory))) + if not os.path.isdir(syspath(self.directory)) and not self.ask_create(create): + print_("Skipping creation of {0}".format(displayable_path(self.directory))) return converter = self.converter() - for (item, actions) in self.items_actions(): + for item, actions in self.items_actions(): dest = self.destination(item) path = self.get_path(item) for action in actions: if action == self.MOVE: - print_(u'>{0} -> {1}'.format(displayable_path(path), - displayable_path(dest))) + print_( + ">{0} -> {1}".format( + displayable_path(path), displayable_path(dest) + ) + ) util.mkdirall(dest) util.move(path, dest) util.prune_dirs(os.path.dirname(path), root=self.directory) @@ -268,16 +273,16 @@ def update(self, create=None): item.store() path = dest elif action == self.WRITE: - print_(u'*{0}'.format(displayable_path(path))) + print_("*{0}".format(displayable_path(path))) item.write(path=path) elif action == self.SYNC_ART: - print_(u'~{0}'.format(displayable_path(path))) + print_("~{0}".format(displayable_path(path))) self.sync_art(item, path) elif action == self.ADD: - print_(u'+{0}'.format(displayable_path(dest))) + print_("+{0}".format(displayable_path(dest))) converter.submit(item) elif action == self.REMOVE: - print_(u'-{0}'.format(displayable_path(path))) + print_("-{0}".format(displayable_path(path))) self.remove_item(item) item.store() @@ -287,16 +292,15 @@ def update(self, create=None): converter.shutdown() def destination(self, item): - return item.destination(basedir=self.directory, - path_formats=self.path_formats) + return item.destination(basedir=self.directory, path_formats=self.path_formats) def set_path(self, item, path): - item[self.path_key] = six.text_type(path, 'utf8') + item[self.path_key] = six.text_type(path, "utf8") @staticmethod def _get_path(item, path_key): try: - return item[path_key].encode('utf8') + return item[path_key].encode("utf8") except KeyError: return None @@ -315,28 +319,28 @@ def _convert(item): util.mkdirall(dest) util.copy(item.path, dest, replace=True) return item, dest + return Worker(_convert) def sync_art(self, item, path): - """ Embed artwork in the destination file. - """ + """Embed artwork in the destination file.""" album = item.get_album() if album: if album.artpath and os.path.isfile(syspath(album.artpath)): - self._log.debug("Embedding art from {} into {}".format( - displayable_path(album.artpath), - displayable_path(path))) - art.embed_item(self._log, item, album.artpath, - itempath=path) + self._log.debug( + "Embedding art from {} into {}".format( + displayable_path(album.artpath), displayable_path(path) + ) + ) + art.embed_item(self._log, item, album.artpath, itempath=path) class ExternalConvert(External): - def __init__(self, log, name, formats, lib, config): super(ExternalConvert, self).__init__(log, name, lib, config) convert_plugin = convert.ConvertPlugin() self._encode = convert_plugin.encode - self._embed = convert_plugin.config['embed'].get(bool) + self._embed = convert_plugin.config["embed"].get(bool) formats = [f.lower() for f in formats] self.formats = [convert.ALIASES.get(f, f) for f in formats] self.convert_cmd, self.ext = convert.get_format(self.formats[0]) @@ -354,17 +358,18 @@ def _convert(item): # Don't rely on the converter to write correct/complete tags. item.write(path=dest) else: - self._log.debug(u'copying {0}'.format(displayable_path(dest))) + self._log.debug("copying {0}".format(displayable_path(dest))) util.copy(item.path, dest, replace=True) if self._embed: self.sync_art(item, dest) return item, dest + return Worker(_convert) def destination(self, item): dest = super(ExternalConvert, self).destination(item) if self.should_transcode(item): - return os.path.splitext(dest)[0] + b'.' + self.ext + return os.path.splitext(dest)[0] + b"." + self.ext else: return dest @@ -377,19 +382,20 @@ class SymlinkView(External): LINK_RELATIVE = 1 def parse_config(self, config): - if 'query' not in config: - config['query'] = u'' # This is a TrueQuery() - if 'link_type' not in config: + if "query" not in config: + config["query"] = "" # This is a TrueQuery() + if "link_type" not in config: # Default as absolute so it doesn't break previous implementation - config['link_type'] = 'absolute' + config["link_type"] = "absolute" - self.relativelinks = config['link_type'].as_choice( - {"relative": self.LINK_RELATIVE, "absolute": self.LINK_ABSOLUTE}) + self.relativelinks = config["link_type"].as_choice( + {"relative": self.LINK_RELATIVE, "absolute": self.LINK_ABSOLUTE} + ) super(SymlinkView, self).parse_config(config) def item_change_actions(self, item, path, dest): - """ Returns the necessary actions for items that were previously in the + """Returns the necessary actions for items that were previously in the external collection, but might require metadata updates. """ actions = [] @@ -404,22 +410,25 @@ def item_change_actions(self, item, path, dest): return actions def update(self, create=None): - for (item, actions) in self.items_actions(): + for item, actions in self.items_actions(): dest = self.destination(item) path = self.get_path(item) for action in actions: if action == self.MOVE: - print_(u'>{0} -> {1}'.format(displayable_path(path), - displayable_path(dest))) + print_( + ">{0} -> {1}".format( + displayable_path(path), displayable_path(dest) + ) + ) self.remove_item(item) self.create_symlink(item) self.set_path(item, dest) elif action == self.ADD: - print_(u'+{0}'.format(displayable_path(dest))) + print_("+{0}".format(displayable_path(dest))) self.create_symlink(item) self.set_path(item, dest) elif action == self.REMOVE: - print_(u'-{0}'.format(displayable_path(path))) + print_("-{0}".format(displayable_path(path))) self.remove_item(item) else: continue @@ -430,7 +439,9 @@ def create_symlink(self, item): util.mkdirall(dest) link = ( os.path.relpath(item.path, os.path.dirname(dest)) - if self.relativelinks == self.LINK_RELATIVE else item.path) + if self.relativelinks == self.LINK_RELATIVE + else item.path + ) util.link(link, dest) def sync_art(self, item, path): @@ -439,7 +450,6 @@ def sync_art(self, item, path): class Worker(futures.ThreadPoolExecutor): - def __init__(self, fn, max_workers=None): super(Worker, self).__init__(max_workers or cpu_count()) self._tasks = set() diff --git a/poetry.lock b/poetry.lock index de2a640..b99cf70 100644 --- a/poetry.lock +++ b/poetry.lock @@ -46,6 +46,71 @@ test = ["beautifulsoup4", "coverage", "flask", "mock", "py7zr", "pylast", "pytes thumbnails = ["Pillow", "pyxdg"] web = ["flask", "flask-cors"] +[[package]] +name = "black" +version = "23.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, + {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, + {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, + {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, + {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, + {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, + {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, + {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, + {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, + {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, + {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, + {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, + {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, + {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, + {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, + {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, + {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, + {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, +] + +[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 = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -77,7 +142,7 @@ pyyaml = "*" name = "coverage" version = "7.2.3" description = "Code coverage measurement for Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -184,6 +249,24 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + [[package]] name = "jellyfish" version = "0.11.2" @@ -335,6 +418,18 @@ files = [ {file = "mutagen-1.46.0.tar.gz", hash = "sha256:6e5f8ba84836b99fe60be5fb27f84be4ad919bbb6b49caa6ae81e70584b55e58"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" version = "23.1" @@ -347,6 +442,34 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "platformdirs" +version = "3.5.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.5.0-py3-none-any.whl", hash = "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4"}, + {file = "platformdirs-3.5.0.tar.gz", hash = "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"}, +] + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -495,7 +618,7 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -503,6 +626,18 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.5.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, +] + [[package]] name = "unidecode" version = "1.3.6" @@ -518,4 +653,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "771499e44883a1a4f60309ed9cb0188b3095cda8945438a93243800ee830f1c5" +content-hash = "5eac7211e31a72ce20ad7e5ad2227630caafd5d6f6e9749a6170e4aabc836c68" diff --git a/pyproject.toml b/pyproject.toml index 0fe60b1..62c4ed3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,15 @@ pytest = "^7.3.1" pytest-cov = "^4.0.0" mock = "^5.0.2" flake8 = "^6.0.0" +isort = "^5.12.0" +black = "^23.3.0" [tool.pytest.ini_options] addopts = "--cov --cov-report=term --cov-report=html" +[tool.isort] +profile = "black" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/test/cli_test.py b/test/cli_test.py index 100a08f..0728c31 100644 --- a/test/cli_test.py +++ b/test/cli_test.py @@ -2,13 +2,11 @@ import os.path import shutil -from helper import TestHelper, control_stdin - -from beets.mediafile import MediaFile from beets import util +from beets.mediafile import MediaFile from beets.util import bytestring_path, syspath - from beets.util.confit import ConfigValueError +from helper import TestHelper, control_stdin class DocTest(TestHelper): @@ -17,90 +15,95 @@ class DocTest(TestHelper): """ def test_external(self): - external_dir = os.path.join(self.mkdtemp(), 'myplayer') - self.config['convert']['formats'] = { - 'aac': { - 'command': 'bash -c "cp \'$source\' \'$dest\';' + - 'printf ISAAC >> \'$dest\'"', - 'extension': 'm4a' + external_dir = os.path.join(self.mkdtemp(), "myplayer") + self.config["convert"]["formats"] = { + "aac": { + "command": "bash -c \"cp '$source' '$dest';" + + "printf ISAAC >> '$dest'\"", + "extension": "m4a", }, } - self.config['alternatives'] = { - 'myplayer': { - 'directory': external_dir, - 'paths': {'default': u'$artist/$title'}, - 'formats': u'aac mp3', - 'query': u'onplayer:true', - 'removable': True, + self.config["alternatives"] = { + "myplayer": { + "directory": external_dir, + "paths": {"default": "$artist/$title"}, + "formats": "aac mp3", + "query": "onplayer:true", + "removable": True, } } - self.add_album(artist='Bach', title='was mp3', format='mp3') - self.add_album(artist='Bach', title='was m4a', format='m4a') - self.add_album(artist='Bach', title='was ogg', format='ogg') - self.add_album(artist='Beethoven', title='was ogg', format='ogg') + self.add_album(artist="Bach", title="was mp3", format="mp3") + self.add_album(artist="Bach", title="was m4a", format="m4a") + self.add_album(artist="Bach", title="was ogg", format="ogg") + self.add_album(artist="Beethoven", title="was ogg", format="ogg") external_from_mp3 = bytestring_path( - os.path.join(external_dir, 'Bach', 'was mp3.mp3')) + os.path.join(external_dir, "Bach", "was mp3.mp3") + ) external_from_m4a = bytestring_path( - os.path.join(external_dir, 'Bach', 'was m4a.m4a')) + os.path.join(external_dir, "Bach", "was m4a.m4a") + ) external_from_ogg = bytestring_path( - os.path.join(external_dir, 'Bach', 'was ogg.m4a')) + os.path.join(external_dir, "Bach", "was ogg.m4a") + ) external_beet = bytestring_path( - os.path.join(external_dir, 'Beethoven', 'was ogg.m4a')) + os.path.join(external_dir, "Beethoven", "was ogg.m4a") + ) - self.runcli('modify', '--yes', 'onplayer=true', 'artist:Bach') - with control_stdin('y'): - out = self.runcli('alt', 'update', 'myplayer') - self.assertIn('Do you want to create the collection?', out) + self.runcli("modify", "--yes", "onplayer=true", "artist:Bach") + with control_stdin("y"): + out = self.runcli("alt", "update", "myplayer") + self.assertIn("Do you want to create the collection?", out) - self.assertNotFileTag(external_from_mp3, b'ISAAC') - self.assertNotFileTag(external_from_m4a, b'ISAAC') - self.assertFileTag(external_from_ogg, b'ISAAC') + self.assertNotFileTag(external_from_mp3, b"ISAAC") + self.assertNotFileTag(external_from_m4a, b"ISAAC") + self.assertFileTag(external_from_ogg, b"ISAAC") self.assertFalse(os.path.isfile(external_beet)) - self.runcli('modify', '--yes', 'composer=JSB', 'artist:Bach') + self.runcli("modify", "--yes", "composer=JSB", "artist:Bach") list_output = self.runcli( - 'alt', 'list-tracks', 'myplayer', '--format', '$artist $title') - self.assertEqual(list_output, '\n'.join( - ['Bach was mp3', 'Bach was m4a', 'Bach was ogg', ''])) + "alt", "list-tracks", "myplayer", "--format", "$artist $title" + ) + self.assertEqual( + list_output, "\n".join(["Bach was mp3", "Bach was m4a", "Bach was ogg", ""]) + ) - self.runcli('alt', 'update', 'myplayer') + self.runcli("alt", "update", "myplayer") mediafile = MediaFile(syspath(external_from_ogg)) - self.assertEqual(mediafile.composer, 'JSB') + self.assertEqual(mediafile.composer, "JSB") - self.runcli('modify', '--yes', 'onplayer!', 'artist:Bach') - self.runcli('modify', '--album', '--yes', - 'onplayer=true', 'albumartist:Beethoven') - self.runcli('alt', 'update', 'myplayer') + self.runcli("modify", "--yes", "onplayer!", "artist:Bach") + self.runcli( + "modify", "--album", "--yes", "onplayer=true", "albumartist:Beethoven" + ) + self.runcli("alt", "update", "myplayer") list_output = self.runcli( - 'alt', 'list-tracks', 'myplayer', '--format', '$artist') - self.assertEqual(list_output, 'Beethoven\n') + "alt", "list-tracks", "myplayer", "--format", "$artist" + ) + self.assertEqual(list_output, "Beethoven\n") self.assertFalse(os.path.isfile(external_from_mp3)) self.assertFalse(os.path.isfile(external_from_m4a)) self.assertFalse(os.path.isfile(external_from_ogg)) - self.assertFileTag(external_beet, b'ISAAC') + self.assertFileTag(external_beet, b"ISAAC") class SymlinkViewTest(TestHelper): - """Test alternatives with the ``link`` format producing symbolic links. - """ + """Test alternatives with the ``link`` format producing symbolic links.""" def setUp(self): super(SymlinkViewTest, self).setUp() - self.set_paths_config({ - 'default': '$artist/$album/$title' - }) - self.config['alternatives'] = { - 'by-year': { - 'paths': {'default': '$year/$album/$title'}, - 'formats': 'link', + self.set_paths_config({"default": "$artist/$album/$title"}) + self.config["alternatives"] = { + "by-year": { + "paths": {"default": "$year/$album/$title"}, + "formats": "link", } } - self.alt_config = self.config['alternatives']['by-year'] + self.alt_config = self.config["alternatives"]["by-year"] def test_add_move_remove_album(self, absolute=True): """Test the symlinks are created and deleted @@ -110,29 +113,33 @@ def test_add_move_remove_album(self, absolute=True): album does not match it anymore. * The links are absolute """ - self.add_album(artist='Michael Jackson', album='Thriller', - year='1990', original_year='1982') + self.add_album( + artist="Michael Jackson", + album="Thriller", + year="1990", + original_year="1982", + ) - self.runcli('alt', 'update', 'by-year') + self.runcli("alt", "update", "by-year") - by_year_path = self.lib_path(b'by-year/1990/Thriller/track 1.mp3') - target_path = self.lib_path(b'Michael Jackson/Thriller/track 1.mp3') + by_year_path = self.lib_path(b"by-year/1990/Thriller/track 1.mp3") + target_path = self.lib_path(b"Michael Jackson/Thriller/track 1.mp3") self.assertSymlink(by_year_path, target_path, absolute) - self.alt_config['paths']['default'] = '$original_year/$album/$title' - self.runcli('alt', 'update', 'by-year') + self.alt_config["paths"]["default"] = "$original_year/$album/$title" + self.runcli("alt", "update", "by-year") - by_orig_year_path = self.lib_path(b'by-year/1982/Thriller/track 1.mp3') + by_orig_year_path = self.lib_path(b"by-year/1982/Thriller/track 1.mp3") self.assertIsNotFile(by_year_path) self.assertSymlink(by_orig_year_path, target_path, absolute) - self.alt_config['query'] = u'some_field::foobar' - self.runcli('alt', 'update', 'by-year') + self.alt_config["query"] = "some_field::foobar" + self.runcli("alt", "update", "by-year") self.assertIsNotFile(by_orig_year_path) def test_add_move_remove_album_absolute(self): - """ Test the absolute symlinks are created and deleted + """Test the absolute symlinks are created and deleted * Config link type is absolute * An album is added * The path of the alternative collection is changed @@ -140,11 +147,11 @@ def test_add_move_remove_album_absolute(self): album does not match it anymore. * The links are absolute """ - self.alt_config['link_type'] = 'absolute' + self.alt_config["link_type"] = "absolute" self.test_add_move_remove_album(absolute=True) def test_add_move_remove_album_relative(self): - """ Test the relative symlinks are created and deleted + """Test the relative symlinks are created and deleted * Config link type is relative * An album is added * The path of the alternative collection is changed @@ -152,11 +159,11 @@ def test_add_move_remove_album_relative(self): album does not match it anymore. * The links are relative """ - self.alt_config['link_type'] = 'relative' + self.alt_config["link_type"] = "relative" self.test_add_move_remove_album(absolute=False) def test_add_update_move_album(self): - """ Test that symlinks are properly updated and no broken links left + """Test that symlinks are properly updated and no broken links left when an item's path in the library changes. Since moving the items causes the links in the symlink view to be broken, this situation used to be incorrectly detected as @@ -167,44 +174,44 @@ def test_add_update_move_album(self): * The album name is changed, which also causes the tracks to be moved. * The symlink view is updated. """ - self.add_album(artist='Michael Jackson', album='Thriller', year='1990') + self.add_album(artist="Michael Jackson", album="Thriller", year="1990") - self.runcli('alt', 'update', 'by-year') + self.runcli("alt", "update", "by-year") - by_year_path = self.lib_path(b'by-year/1990/Thriller/track 1.mp3') + by_year_path = self.lib_path(b"by-year/1990/Thriller/track 1.mp3") self.assertSymlink( link=by_year_path, - target=self.lib_path(b'Michael Jackson/Thriller/track 1.mp3'), + target=self.lib_path(b"Michael Jackson/Thriller/track 1.mp3"), absolute=True, ) # `-y` skips the prompt, `-a` updates album-level fields, `-m` forces # actually moving the files - self.runcli('mod', '-y', '-a', '-m', - 'Thriller', 'album=Thriller (Remastered)') - self.runcli('alt', 'update', 'by-year') + self.runcli("mod", "-y", "-a", "-m", "Thriller", "album=Thriller (Remastered)") + self.runcli("alt", "update", "by-year") self.assertIsNotFile(by_year_path) self.assertSymlink( - link=self.lib_path( - b'by-year/1990/Thriller (Remastered)/track 1.mp3'), - target=self.lib_path( - b'Michael Jackson/Thriller (Remastered)/track 1.mp3'), + link=self.lib_path(b"by-year/1990/Thriller (Remastered)/track 1.mp3"), + target=self.lib_path(b"Michael Jackson/Thriller (Remastered)/track 1.mp3"), absolute=True, ) def test_valid_options(self): - """ Test that an error is raised when option is invalid + """Test that an error is raised when option is invalid * Config link type is invalid * An album is added * A confuse.ConfigValueError is raised """ - self.alt_config['link_type'] = 'Hylian' - self.add_album(artist='Michael Jackson', album='Thriller', - year='1990', original_year='1982') + self.alt_config["link_type"] = "Hylian" + self.add_album( + artist="Michael Jackson", + album="Thriller", + year="1990", + original_year="1982", + ) - self.assertRaises(ConfigValueError, - self.runcli, 'alt', 'update', 'by-year') + self.assertRaises(ConfigValueError, self.runcli, "alt", "update", "by-year") class ExternalCopyTest(TestHelper): @@ -215,74 +222,74 @@ class ExternalCopyTest(TestHelper): def setUp(self): super(ExternalCopyTest, self).setUp() external_dir = self.mkdtemp() - self.config['alternatives'] = { - 'myexternal': { - 'directory': external_dir, - 'query': u'myexternal:true', + self.config["alternatives"] = { + "myexternal": { + "directory": external_dir, + "query": "myexternal:true", } } - self.external_config = self.config['alternatives']['myexternal'] + self.external_config = self.config["alternatives"]["myexternal"] def test_add_singleton(self): - item = self.add_track(title=u'\u00e9', myexternal='true') - self.runcli('alt', 'update', 'myexternal') + item = self.add_track(title="\u00e9", myexternal="true") + self.runcli("alt", "update", "myexternal") item.load() self.assertIsFile(self.get_path(item)) def test_add_album(self): album = self.add_album() - album['myexternal'] = 'true' + album["myexternal"] = "true" album.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") for item in album.items(): self.assertIsFile(self.get_path(item)) def test_add_nonexistent(self): - item = self.add_external_track('myexternal') + item = self.add_external_track("myexternal") path = self.get_path(item) util.remove(path) - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") self.assertIsFile(self.get_path(item)) def test_add_replace(self): - item = self.add_external_track('myexternal') - del item['alt.myexternal'] + item = self.add_external_track("myexternal") + del item["alt.myexternal"] item.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() - self.assertIn('alt.myexternal', item) + self.assertIn("alt.myexternal", item) def test_update_older(self): - item = self.add_external_track('myexternal') - item['composer'] = 'JSB' + item = self.add_external_track("myexternal") + item["composer"] = "JSB" item.store() item.write() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() mediafile = MediaFile(syspath(self.get_path(item))) - self.assertEqual(mediafile.composer, 'JSB') + self.assertEqual(mediafile.composer, "JSB") def test_no_udpdate_newer(self): - item = self.add_external_track('myexternal') - item['composer'] = 'JSB' + item = self.add_external_track("myexternal") + item["composer"] = "JSB" item.store() # We omit write to keep old mtime - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() mediafile = MediaFile(syspath(self.get_path(item))) - self.assertNotEqual(mediafile.composer, 'JSB') + self.assertNotEqual(mediafile.composer, "JSB") def test_move_after_path_format_update(self): - item = self.add_external_track('myexternal') + item = self.add_external_track("myexternal") old_path = self.get_path(item) self.assertIsFile(old_path) - self.external_config['paths'] = {'default': '$album/$title'} - self.runcli('alt', 'update', 'myexternal') + self.external_config["paths"] = {"default": "$album/$title"} + self.runcli("alt", "update", "myexternal") item.load() new_path = self.get_path(item) @@ -290,66 +297,66 @@ def test_move_after_path_format_update(self): self.assertIsFile(new_path) def test_move_and_write_after_tags_changed(self): - item = self.add_external_track('myexternal') + item = self.add_external_track("myexternal") old_path = self.get_path(item) self.assertIsFile(old_path) - item['title'] = 'a new title' + item["title"] = "a new title" item.store() item.try_write() # Required to update mtime. - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() new_path = self.get_path(item) self.assertIsNotFile(old_path) self.assertIsFile(new_path) mediafile = MediaFile(syspath(new_path)) - self.assertEqual(mediafile.title, 'a new title') + self.assertEqual(mediafile.title, "a new title") def test_prune_after_move(self): - item = self.add_external_track('myexternal') + item = self.add_external_track("myexternal") artist_dir = os.path.dirname(self.get_path(item)) self.assertTrue(os.path.isdir(artist_dir)) - item['artist'] = 'a new artist' + item["artist"] = "a new artist" item.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") self.assertFalse(os.path.exists(syspath(artist_dir))) def test_remove_item(self): - item = self.add_external_track('myexternal') + item = self.add_external_track("myexternal") old_path = self.get_path(item) self.assertIsFile(old_path) - del item['myexternal'] + del item["myexternal"] item.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() - self.assertNotIn('alt.myexternal', item) + self.assertNotIn("alt.myexternal", item) self.assertIsNotFile(old_path) def test_remove_album(self): - album = self.add_external_album('myexternal') + album = self.add_external_album("myexternal") item = album.items().get() old_path = self.get_path(item) self.assertIsFile(old_path) - del album['myexternal'] + del album["myexternal"] album.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() - self.assertNotIn('alt.myexternal', item) + self.assertNotIn("alt.myexternal", item) self.assertIsNotFile(old_path) def test_unkown_collection(self): - out = self.runcli('alt', 'update', 'unkown') + out = self.runcli("alt", "update", "unkown") self.assertIn("Alternative collection 'unkown' not found.", out) def test_embed_art(self): - """ Test that artwork is embedded and updated to match the source file. + """Test that artwork is embedded and updated to match the source file. There used to be a bug that meant that albumart was only embedded once on initial addition to the alternative collection, but not if @@ -358,8 +365,9 @@ def test_embed_art(self): This test comprehensively checks that embedded artwork is up-to-date with the artwork file, even if no changes to the database happen. """ + def touch_art(item, image_path): - """ `touch` the image file, but don't set mtime to the current + """`touch` the image file, but don't set mtime to the current time since the tests run rather fast and item and art mtimes might end up identical if the filesystem has low mtime granularity or mtimes are cashed as laid out in @@ -369,14 +377,12 @@ def touch_art(item, image_path): bugs. """ item_mtime_alt = os.path.getmtime(syspath(item.path)) - os.utime(syspath(image_path), - (item_mtime_alt + 2, item_mtime_alt + 2) - ) + os.utime(syspath(image_path), (item_mtime_alt + 2, item_mtime_alt + 2)) # Initially add album without artwork. - album = self.add_album(myexternal='true') + album = self.add_album(myexternal="true") album.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item = album.items().get() self.assertHasNoEmbeddedArtwork(self.get_path(item)) @@ -384,14 +390,14 @@ def touch_art(item, image_path): # Make a copy of the artwork, so that changing mtime/content won't # affect the repository. image_dir = bytestring_path(self.mkdtemp()) - image_path = os.path.join(image_dir, b'image') + image_path = os.path.join(image_dir, b"image") shutil.copy(self.IMAGE_FIXTURE1, syspath(image_path)) touch_art(item, image_path) # Add a cover image, assert that it is being embedded. album.artpath = image_path album.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item = album.items().get() self.assertHasEmbeddedArtwork(self.get_path(item), self.IMAGE_FIXTURE1) @@ -401,7 +407,7 @@ def touch_art(item, image_path): # Assert that artwork is re-embedded. shutil.copy(self.IMAGE_FIXTURE2, image_path) touch_art(item, image_path) - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item = album.items().get() self.assertHasEmbeddedArtwork(self.get_path(item), self.IMAGE_FIXTURE2) @@ -415,85 +421,83 @@ class ExternalConvertTest(TestHelper): def setUp(self): super(ExternalConvertTest, self).setUp() external_dir = self.mkdtemp() - self.config['convert']['formats'] = { - 'ogg': 'bash -c "cp \'$source\' \'$dest\';' + - 'printf ISOGG >> \'$dest\'"' + self.config["convert"]["formats"] = { + "ogg": "bash -c \"cp '$source' '$dest';" + "printf ISOGG >> '$dest'\"" } - self.config['alternatives'] = { - 'myexternal': { - 'directory': external_dir, - 'query': u'myexternal:true', - 'formats': 'ogg mp3' + self.config["alternatives"] = { + "myexternal": { + "directory": external_dir, + "query": "myexternal:true", + "formats": "ogg mp3", } } - self.external_config = self.config['alternatives']['myexternal'] + self.external_config = self.config["alternatives"]["myexternal"] def test_convert(self): - item = self.add_track(myexternal='true', format='m4a') - self.runcli('alt', 'update', 'myexternal') + item = self.add_track(myexternal="true", format="m4a") + self.runcli("alt", "update", "myexternal") item.load() converted_path = self.get_path(item) - self.assertFileTag(converted_path, b'ISOGG') + self.assertFileTag(converted_path, b"ISOGG") def test_convert_and_embed(self): - self.config['convert']['embed'] = True + self.config["convert"]["embed"] = True - album = self.add_album(myexternal='true', format='m4a') + album = self.add_album(myexternal="true", format="m4a") album.artpath = self.IMAGE_FIXTURE1 album.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item = album.items().get() self.assertHasEmbeddedArtwork(self.get_path(item)) def test_convert_write_tags(self): - item = self.add_track(myexternal='true', format='m4a', title=u'TITLE') + item = self.add_track(myexternal="true", format="m4a", title="TITLE") # We "convert" by copying the file. Setting the title simulates # a badly behaved converter mediafile_converted = MediaFile(syspath(item.path)) - mediafile_converted.title = u'WRONG' + mediafile_converted.title = "WRONG" mediafile_converted.save() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() alt_mediafile = MediaFile(syspath(self.get_path(item))) - self.assertEqual(alt_mediafile.title, u'TITLE') + self.assertEqual(alt_mediafile.title, "TITLE") def test_skip_convert_for_same_format(self): - item = self.add_track(myexternal='true') - item['format'] = 'OGG' + item = self.add_track(myexternal="true") + item["format"] = "OGG" item.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() converted_path = self.get_path(item) - self.assertNotFileTag(converted_path, b'ISOGG') + self.assertNotFileTag(converted_path, b"ISOGG") def test_skip_convert_for_alternative_format(self): - item = self.add_track(myexternal='true') - item['format'] = 'MP3' + item = self.add_track(myexternal="true") + item["format"] = "MP3" item.store() - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() converted_path = self.get_path(item) - self.assertNotFileTag(converted_path, b'ISOGG') + self.assertNotFileTag(converted_path, b"ISOGG") def test_no_move_on_extension_change(self): - item = self.add_track(myexternal='true', format='m4a') - self.runcli('alt', 'update', 'myexternal') + item = self.add_track(myexternal="true", format="m4a") + self.runcli("alt", "update", "myexternal") - self.config['convert']['formats'] = { - 'mp3': 'bash -c "cp \'$source\' \'$dest\';' + - 'printf ISMP3 >> \'$dest\'"' + self.config["convert"]["formats"] = { + "mp3": "bash -c \"cp '$source' '$dest';" + "printf ISMP3 >> '$dest'\"" } - self.config['alternatives']['myexternal']['formats'] = 'mp3' + self.config["alternatives"]["myexternal"]["formats"] = "mp3" # Assert that this re-encodes instead of copying the ogg file - self.runcli('alt', 'update', 'myexternal') + self.runcli("alt", "update", "myexternal") item.load() converted_path = self.get_path(item) - self.assertFileTag(converted_path, b'ISMP3') + self.assertFileTag(converted_path, b"ISMP3") class ExternalConvertWorkerTest(TestHelper): @@ -505,32 +509,34 @@ class ExternalConvertWorkerTest(TestHelper): def setUp(self): super(ExternalConvertWorkerTest, self).setUp(mock_worker=False) external_dir = self.mkdtemp() - self.config['convert']['formats'] = { - 'ogg': 'bash -c "cp \'{source}\' \'$dest\'"'.format( + self.config["convert"]["formats"] = { + "ogg": "bash -c \"cp '{source}' '$dest'\"".format( # The convert plugin will encode this again using arg_encoding - source=self.item_fixture_path('ogg').decode( - util.arg_encoding())) + source=self.item_fixture_path("ogg").decode(util.arg_encoding()) + ) } - self.config['alternatives'] = { - 'myexternal': { - 'directory': external_dir, - 'query': u'myexternal:true', - 'formats': 'ogg' + self.config["alternatives"] = { + "myexternal": { + "directory": external_dir, + "query": "myexternal:true", + "formats": "ogg", } } def test_convert_multiple(self): - items = [self.add_track(title="track {}".format(i), - myexternal='true', - format='m4a', - ) for i in range(24) - ] - self.runcli('alt', 'update', 'myexternal') + items = [ + self.add_track( + title="track {}".format(i), + myexternal="true", + format="m4a", + ) + for i in range(24) + ] + self.runcli("alt", "update", "myexternal") for item in items: item.load() converted_path = self.get_path(item) - self.assertMediaFileFields(converted_path, - type='ogg', title=item.title) + self.assertMediaFileFields(converted_path, type="ogg", title=item.title) class ExternalRemovableTest(TestHelper): @@ -540,51 +546,51 @@ class ExternalRemovableTest(TestHelper): def setUp(self): super(ExternalRemovableTest, self).setUp() - external_dir = os.path.join(self.mkdtemp(), u'\u00e9xt') - self.config['alternatives'] = { - 'myexternal': { - 'directory': external_dir, - 'query': u'', + external_dir = os.path.join(self.mkdtemp(), "\u00e9xt") + self.config["alternatives"] = { + "myexternal": { + "directory": external_dir, + "query": "", } } - self.external_config = self.config['alternatives']['myexternal'] + self.external_config = self.config["alternatives"]["myexternal"] def test_ask_create_yes(self): item = self.add_track() - with control_stdin('y'): - out = self.runcli('alt', 'update', 'myexternal') - self.assertIn('Do you want to create the collection?', out) + with control_stdin("y"): + out = self.runcli("alt", "update", "myexternal") + self.assertIn("Do you want to create the collection?", out) item.load() - self.assertIn('alt.myexternal', item) + self.assertIn("alt.myexternal", item) def test_ask_create_no(self): item = self.add_track() - with control_stdin('n'): - out = self.runcli('alt', 'update', 'myexternal') - self.assertIn('Skipping creation of', out) + with control_stdin("n"): + out = self.runcli("alt", "update", "myexternal") + self.assertIn("Skipping creation of", out) item.load() - self.assertNotIn('alt.myexternal', item) + self.assertNotIn("alt.myexternal", item) def test_create_option(self): item = self.add_track() - self.runcli('alt', 'update', '--create', 'myexternal') + self.runcli("alt", "update", "--create", "myexternal") item.load() - self.assertIn('alt.myexternal', item) + self.assertIn("alt.myexternal", item) def test_no_create_option(self): item = self.add_track() - self.runcli('alt', 'update', '--no-create', 'myexternal') + self.runcli("alt", "update", "--no-create", "myexternal") item.load() - self.assertNotIn('alt.myexternal', item) + self.assertNotIn("alt.myexternal", item) def test_not_removable(self): item = self.add_track() - self.external_config['removable'] = False - with control_stdin('y'): - out = self.runcli('alt', 'update', 'myexternal') - self.assertNotIn('Do you want to create the collection?', out) + self.external_config["removable"] = False + with control_stdin("y"): + out = self.runcli("alt", "update", "myexternal") + self.assertNotIn("Do you want to create the collection?", out) item.load() - self.assertIn('alt.myexternal', item) + self.assertIn("alt.myexternal", item) class CompletionTest(TestHelper): @@ -594,4 +600,4 @@ class CompletionTest(TestHelper): """ def test_completion(self): - self.runcli('completion') + self.runcli("completion") diff --git a/test/helper.py b/test/helper.py index 3197aec..71105f0 100644 --- a/test/helper.py +++ b/test/helper.py @@ -1,39 +1,27 @@ -import sys import os -import tempfile -import six import shutil -from contextlib import contextmanager -from six import StringIO +import sys +import tempfile from concurrent import futures -from zlib import crc32 +from contextlib import contextmanager from unittest import TestCase - -from mock import patch +from zlib import crc32 import beets -from beets import logging -from beets import plugins -from beets import ui -from beets import util +import six +from beets import logging, plugins, ui, util from beets.library import Item from beets.mediafile import MediaFile -from beets.util import ( - MoveOperation, - syspath, - bytestring_path, - displayable_path, -) - -from beetsplug import alternatives -from beetsplug import convert +from beets.util import MoveOperation, bytestring_path, displayable_path, syspath +from mock import patch +from six import StringIO +from beetsplug import alternatives, convert -logging.getLogger('beets').propagate = True +logging.getLogger("beets").propagate = True class LogCapture(logging.Handler): - def __init__(self): super(LogCapture, self).__init__() self.messages = [] @@ -43,7 +31,7 @@ def emit(self, record): @contextmanager -def capture_log(logger='beets'): +def capture_log(logger="beets"): capture = LogCapture() log = logging.getLogger(logger) log.addHandler(capture) @@ -66,7 +54,7 @@ def capture_stdout(): org = sys.stdout sys.stdout = capture = StringIO() if six.PY2: # StringIO encoding attr isn't writable in python >= 3 - sys.stdout.encoding = 'utf-8' + sys.stdout.encoding = "utf-8" try: yield sys.stdout finally: @@ -85,7 +73,7 @@ def control_stdin(input=None): org = sys.stdin sys.stdin = StringIO(input) if six.PY2: # StringIO encoding attr isn't writable in python >= 3 - sys.stdin.encoding = 'utf-8' + sys.stdin.encoding = "utf-8" try: yield sys.stdin finally: @@ -94,7 +82,7 @@ def control_stdin(input=None): def _convert_args(args): """Convert args to bytestrings for Python 2 and convert them to strings - on Python 3. + on Python 3. """ for i, elem in enumerate(args): if six.PY2: @@ -108,105 +96,104 @@ def _convert_args(args): class Assertions(object): - def assertFileTag(self, path, tag): self.assertIsFile(path) - with open(syspath(path), 'rb') as f: + with open(syspath(path), "rb") as f: f.seek(-5, os.SEEK_END) self.assertEqual(f.read(), tag) def assertNotFileTag(self, path, tag): self.assertIsFile(path) - with open(syspath(path), 'rb') as f: + with open(syspath(path), "rb") as f: f.seek(-5, os.SEEK_END) self.assertNotEqual(f.read(), tag) def assertIsFile(self, path): - self.assertTrue(os.path.isfile(syspath(path)), - msg=u'Path is not a file: {0}'.format( - displayable_path(path) - )) + self.assertTrue( + os.path.isfile(syspath(path)), + msg="Path is not a file: {0}".format(displayable_path(path)), + ) def assertIsNotFile(self, path): """Asserts that `path` is neither a regular file (``os.path.isfile``, follows symlinks and returns False for a broken symlink) nor a symlink (``os.path.islink``, returns True for both valid and broken symlinks). """ - self.assertFalse(os.path.isfile(syspath(path)), - msg=u'Path is a file: {0}'.format( - displayable_path(path) - )) - self.assertFalse(os.path.islink(syspath(path)), - msg=u'Path is a symlink: {0}'.format( - displayable_path(path) - )) + self.assertFalse( + os.path.isfile(syspath(path)), + msg="Path is a file: {0}".format(displayable_path(path)), + ) + self.assertFalse( + os.path.islink(syspath(path)), + msg="Path is a symlink: {0}".format(displayable_path(path)), + ) def assertSymlink(self, link, target, absolute=True): - self.assertTrue(os.path.islink(syspath(link)), - msg=u'Path is not a symbolic link: {0}'.format( - displayable_path(link) - )) - self.assertTrue(os.path.isfile(syspath(target)), - msg=u'Path is not a file: {0}'.format( - displayable_path(link) - )) + self.assertTrue( + os.path.islink(syspath(link)), + msg="Path is not a symbolic link: {0}".format(displayable_path(link)), + ) + self.assertTrue( + os.path.isfile(syspath(target)), + msg="Path is not a file: {0}".format(displayable_path(link)), + ) pre_link_target = bytestring_path(os.readlink(syspath(link))) link_target = os.path.join(os.path.dirname(link), pre_link_target) - self.assertTrue(util.samefile(target, link_target), - msg=u'Symlink points to {} instead of {}'.format( - displayable_path(link_target), - displayable_path(target) - )) + self.assertTrue( + util.samefile(target, link_target), + msg="Symlink points to {} instead of {}".format( + displayable_path(link_target), displayable_path(target) + ), + ) if absolute: - self.assertTrue(os.path.isabs(pre_link_target), - msg=u'Symlink {} is not absolute'.format( - displayable_path(pre_link_target) - )) + self.assertTrue( + os.path.isabs(pre_link_target), + msg="Symlink {} is not absolute".format( + displayable_path(pre_link_target) + ), + ) else: - self.assertFalse(os.path.isabs(pre_link_target), - msg=u'Symlink {} is not relative'.format( - displayable_path(pre_link_target) - )) + self.assertFalse( + os.path.isabs(pre_link_target), + msg="Symlink {} is not relative".format( + displayable_path(pre_link_target) + ), + ) class MediaFileAssertions(object): - def assertHasEmbeddedArtwork(self, path, compare_file=None): mediafile = MediaFile(syspath(path)) - self.assertIsNotNone(mediafile.art, - msg=u'MediaFile has no embedded artwork') + self.assertIsNotNone(mediafile.art, msg="MediaFile has no embedded artwork") if compare_file: - with open(syspath(compare_file), 'rb') as compare_fh: + with open(syspath(compare_file), "rb") as compare_fh: crc_is = crc32(mediafile.art) crc_expected = crc32(compare_fh.read()) self.assertEqual( - crc_is, crc_expected, - msg=u"MediaFile has embedded artwork, but " - u"content (CRC32: {}) doesn't match " - u"expectations (CRC32: {}).".format( - crc_is, crc_expected - ) - ) + crc_is, + crc_expected, + msg="MediaFile has embedded artwork, but " + "content (CRC32: {}) doesn't match " + "expectations (CRC32: {}).".format(crc_is, crc_expected), + ) def assertHasNoEmbeddedArtwork(self, path): mediafile = MediaFile(syspath(path)) - self.assertIsNone(mediafile.art, - msg=u'MediaFile has embedded artwork') + self.assertIsNone(mediafile.art, msg="MediaFile has embedded artwork") def assertMediaFileFields(self, path, **kwargs): mediafile = MediaFile(syspath(path)) for k, v in kwargs.items(): actual = getattr(mediafile, k) - self.assertTrue(actual == v, - msg=u"MediaFile has tag {k}='{actual}' " - u"instead of '{expected}'".format( - k=k, actual=actual, expected=v) - ) + self.assertTrue( + actual == v, + msg="MediaFile has tag {k}='{actual}' " + "instead of '{expected}'".format(k=k, actual=actual, expected=v), + ) class TestHelper(TestCase, Assertions, MediaFileAssertions): - def setUp(self, mock_worker=True): """Setup required for running test. Must be called before running any tests. @@ -218,13 +205,12 @@ def setUp(self, mock_worker=True): files. Thus, the 'converted' files need not be valid audio files. """ if mock_worker: - patcher = patch('beetsplug.alternatives.Worker', new=MockedWorker) + patcher = patch("beetsplug.alternatives.Worker", new=MockedWorker) patcher.start() self.addCleanup(patcher.stop) self._tempdirs = [] - plugins._classes = set([alternatives.AlternativesPlugin, - convert.ConvertPlugin]) + plugins._classes = set([alternatives.AlternativesPlugin, convert.ConvertPlugin]) self.setup_beets() def tearDown(self): @@ -241,36 +227,34 @@ def mkdtemp(self): def setup_beets(self): self.addCleanup(self.teardown_beets) - os.environ['BEETSDIR'] = self.mkdtemp() + os.environ["BEETSDIR"] = self.mkdtemp() self.config = beets.config self.config.clear() self.config.read() - self.config['plugins'] = [] - self.config['verbose'] = True - self.config['ui']['color'] = False - self.config['threaded'] = False - self.config['import']['copy'] = False + self.config["plugins"] = [] + self.config["verbose"] = True + self.config["ui"]["color"] = False + self.config["threaded"] = False + self.config["import"]["copy"] = False libdir = self.mkdtemp() - self.config['directory'] = libdir + self.config["directory"] = libdir self.libdir = bytestring_path(libdir) - self.lib = beets.library.Library(':memory:', self.libdir) + self.lib = beets.library.Library(":memory:", self.libdir) self.fixture_dir = os.path.join( - bytestring_path(os.path.dirname(__file__)), - b'fixtures') + bytestring_path(os.path.dirname(__file__)), b"fixtures" + ) - self.IMAGE_FIXTURE1 = os.path.join(self.fixture_dir, - b'image.png') - self.IMAGE_FIXTURE2 = os.path.join(self.fixture_dir, - b'image_black.png') + self.IMAGE_FIXTURE1 = os.path.join(self.fixture_dir, b"image.png") + self.IMAGE_FIXTURE2 = os.path.join(self.fixture_dir, b"image_black.png") def teardown_beets(self): del self.lib._connections - if 'BEETSDIR' in os.environ: - del os.environ['BEETSDIR'] + if "BEETSDIR" in os.environ: + del os.environ["BEETSDIR"] self.config.clear() beets.config.read(user=False, defaults=True) @@ -294,23 +278,21 @@ def runcli(self, *args): return out.getvalue() def lib_path(self, path): - return os.path.join(self.libdir, - path.replace(b'/', bytestring_path(os.sep))) + return os.path.join(self.libdir, path.replace(b"/", bytestring_path(os.sep))) def item_fixture_path(self, fmt): - assert fmt in 'mp3 m4a ogg'.split() - return os.path.join(self.fixture_dir, - bytestring_path('min.' + fmt.lower())) + assert fmt in "mp3 m4a ogg".split() + return os.path.join(self.fixture_dir, bytestring_path("min." + fmt.lower())) def add_album(self, **kwargs): values = { - 'title': 'track 1', - 'artist': 'artist 1', - 'album': 'album 1', - 'format': 'mp3', + "title": "track 1", + "artist": "artist 1", + "album": "album 1", + "format": "mp3", } values.update(kwargs) - item = Item.from_path(self.item_fixture_path(values.pop('format'))) + item = Item.from_path(self.item_fixture_path(values.pop("format"))) item.add(self.lib) item.update(values) item.move(MoveOperation.COPY) @@ -322,14 +304,14 @@ def add_album(self, **kwargs): def add_track(self, **kwargs): values = { - 'title': 'track 1', - 'artist': 'artist 1', - 'album': 'album 1', - 'format': 'mp3', + "title": "track 1", + "artist": "artist 1", + "album": "album 1", + "format": "mp3", } values.update(kwargs) - item = Item.from_path(self.item_fixture_path(values.pop('format'))) + item = Item.from_path(self.item_fixture_path(values.pop("format"))) item.add(self.lib) item.update(values) item.move(MoveOperation.COPY) @@ -337,26 +319,25 @@ def add_track(self, **kwargs): return item def add_external_track(self, ext_name, **kwargs): - kwargs[ext_name] = 'true' + kwargs[ext_name] = "true" item = self.add_track(**kwargs) - self.runcli('alt', 'update', ext_name) + self.runcli("alt", "update", ext_name) item.load() return item def add_external_album(self, ext_name, **kwargs): album = self.add_album(**kwargs) - album[ext_name] = 'true' + album[ext_name] = "true" album.store() - self.runcli('alt', 'update', ext_name) + self.runcli("alt", "update", ext_name) album.load() return album - def get_path(self, item, path_key='alt.myexternal'): + def get_path(self, item, path_key="alt.myexternal"): return alternatives.External._get_path(item, path_key) class MockedWorker(alternatives.Worker): - def __init__(self, fn, max_workers=None): self._tasks = set() self._fn = fn