diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index b1dc981b2..2b32a2076 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -35,8 +35,9 @@ from typing_extensions import ( from unidecode import unidecode import beets +from beets.util.functemplate import get_template -from ..util import cached_classproperty, functemplate +from ..util import cached_classproperty from . import types from .query import MatchQuery, TrueQuery from .sort import NullSort @@ -688,20 +689,13 @@ class Model(ABC, Generic[D]): """ return self._formatter(self, included_keys, for_path) - def evaluate_template( - self, template: str | functemplate.Template, for_path: bool = False - ) -> str: - """Evaluate a template (a string or a `Template` object) using - the object's fields. If `for_path` is true, then no new path - separators will be added to the template. + def evaluate_template(self, fmt: str, for_path: bool = False) -> str: + """Evaluate a format string using the object's fields. + + If `for_path` is true, then no new path separators are added to the template. """ # Perform substitution. - if isinstance(template, str): - t = functemplate.template(template) - else: - # Help out mypy - t = template - return t.substitute( + return get_template(fmt).substitute( self.formatted(for_path=for_path), self._template_funcs() ) diff --git a/beets/library/models.py b/beets/library/models.py index 7d8172014..95b623833 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -26,7 +26,6 @@ from beets.util import ( syspath, ) from beets.util.deprecation import maybe_replace_legacy_field -from beets.util.functemplate import Template, template from beets.util.pathformats import PF_KEY_DEFAULT from .exceptions import FileOperationError, ReadError, WriteError @@ -102,10 +101,9 @@ class LibModel(dbcore.Model["Library"]): super().add(lib) def __format__(self, spec: str) -> str: - if not spec: - spec = beets.config[self._format_config_key].as_str() - assert isinstance(spec, str) - return self.evaluate_template(spec) + return self.evaluate_template( + spec or beets.config[self._format_config_key].as_str() + ) def __str__(self) -> str: return format(self) @@ -546,8 +544,8 @@ class Album(LibModel): image = bytestring_path(image) item_dir = item_dir or self.item_dir() - filename_tmpl = template(beets.config["art_filename"].as_str()) - subpath = self.evaluate_template(filename_tmpl, True) + filename_tmpl = beets.config["art_filename"].as_str() + subpath = self.evaluate_template(filename_tmpl, for_path=True) if beets.config["asciify_paths"]: subpath = util.asciify_path(subpath) subpath = util.sanitize_path(subpath, replacements=self.db.replacements) @@ -1250,13 +1248,8 @@ class Item(LibModel): break else: assert False, "no default path format" - if isinstance(path_format, Template): - subpath_tmpl = path_format - else: - subpath_tmpl = template(path_format) - # Evaluate the selected template. - subpath = self.evaluate_template(subpath_tmpl, True) + subpath = self.evaluate_template(path_format, for_path=True) if beets.config["asciify_paths"]: subpath = util.asciify_path(subpath) diff --git a/beets/test/helper.py b/beets/test/helper.py index ffd623eb8..9f72bad00 100644 --- a/beets/test/helper.py +++ b/beets/test/helper.py @@ -39,7 +39,6 @@ from beets.library import Item, Library from beets.test import _common from beets.ui.commands.import_.session import TerminalImportSession from beets.util import MoveOperation, clean_module_tempdir, syspath -from beets.util.functemplate import template if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Sequence @@ -614,12 +613,9 @@ class ImportHelper(TestHelper, ImporterMixin): super().setup_beets() self.import_media = [] self.lib.path_formats = [ - ("default", template(os.path.join("$artist", "$album", "$title"))), - ("singleton:true", template(os.path.join("singletons", "$title"))), - ( - "comp:true", - template(os.path.join("compilations", "$album", "$title")), - ), + ("default", os.path.join("$artist", "$album", "$title")), + ("singleton:true", os.path.join("singletons", "$title")), + ("comp:true", os.path.join("compilations", "$album", "$title")), ] diff --git a/beets/ui/commands/modify.py b/beets/ui/commands/modify.py index 18ea099f5..e03b2de2f 100644 --- a/beets/ui/commands/modify.py +++ b/beets/ui/commands/modify.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, NamedTuple from beets import library, ui from beets.dbcore import types from beets.exceptions import UserError -from beets.util import functemplate from beets.util.deprecation import maybe_replace_legacy_field from .utils import do_query @@ -69,16 +68,15 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm, inherit): # objects. ui.print_(f"Modifying {len(objs)} {'album' if album else 'item'}s.") changed = [] - templates = {} - for key, mod in mods.items(): - templates[key] = functemplate.template(mod.value) for obj in objs: - obj_mods = {} - for key, mod in mods.items(): - parsed_value = model_cls._parse( - key, obj.evaluate_template(templates[key]) + obj_mods = { + key: mod.apply( + obj, + key, + model_cls._parse(key, obj.evaluate_template(mod.value)), ) - obj_mods[key] = mod.apply(obj, key, parsed_value) + for key, mod in mods.items() + } if print_and_modify(obj, obj_mods, dels) and obj not in changed: changed.append(obj) diff --git a/beets/util/functemplate.py b/beets/util/functemplate.py index 5547bcb0e..e7451fb3b 100644 --- a/beets/util/functemplate.py +++ b/beets/util/functemplate.py @@ -16,9 +16,9 @@ from __future__ import annotations import ast import dis -import functools import re import types +from functools import lru_cache SYMBOL_DELIM = "$" FUNC_DELIM = "%" @@ -496,8 +496,8 @@ def _parse(template): return Expression(parts) -@functools.lru_cache(maxsize=128) -def template(fmt) -> Template: +@lru_cache(maxsize=128) +def get_template(fmt: str) -> Template: return Template(fmt) diff --git a/beets/util/pathformats.py b/beets/util/pathformats.py index 275f0f2cb..7d1a6faf5 100644 --- a/beets/util/pathformats.py +++ b/beets/util/pathformats.py @@ -2,14 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING -from .functemplate import template - if TYPE_CHECKING: import confuse - from .functemplate import Template - - PathFormat = tuple[str, Template] + PathFormat = tuple[str, str] # Special path format key. @@ -25,7 +21,4 @@ def get_path_formats(subview: confuse.Subview) -> list[PathFormat]: part of ``paths``. This keeps inherited defaults such as ``default``, ``comp``, and ``singleton`` available unless they are explicitly replaced. """ - return [ - (PF_KEY_QUERIES.get(q, q), template(v.as_str())) - for q, v in subview.items() - ] + return [(PF_KEY_QUERIES.get(q, q), v.as_str()) for q, v in subview.items()] diff --git a/beetsplug/bench.py b/beetsplug/bench.py index d1f71c7ad..3beca7928 100644 --- a/beetsplug/bench.py +++ b/beetsplug/bench.py @@ -6,7 +6,6 @@ import timeit from beets import importer, plugins, ui from beets.autotag import tag_album from beets.plugins import BeetsPlugin -from beets.util.functemplate import Template from beets.util.pathformats import PF_KEY_DEFAULT from beetsplug._utils import vfs @@ -17,10 +16,7 @@ def aunique_benchmark(lib, prof): # Measure path generation performance with %aunique{} included. lib.path_formats = [ - ( - PF_KEY_DEFAULT, - Template("$albumartist/$album%aunique{}/$track $title"), - ) + (PF_KEY_DEFAULT, "$albumartist/$album%aunique{}/$track $title") ] if prof: cProfile.runctx( @@ -35,7 +31,7 @@ def aunique_benchmark(lib, prof): # And with %aunique replaced with a "cheap" no-op function. lib.path_formats = [ - (PF_KEY_DEFAULT, Template("$albumartist/$album%lower{}/$track $title")) + (PF_KEY_DEFAULT, "$albumartist/$album%lower{}/$track $title") ] if prof: cProfile.runctx( diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 65d98f5d0..6ea172631 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -215,9 +215,20 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]): name: str, va_likely: bool, ) -> tuple[str, dict[str, str]]: - query = f'album:"{name}"' if query_type == "album" else name - if query_type == "track" or not va_likely: - query += f' artist:"{artist}"' + if query_type == "album": + query = f'album:"{name}"' + if not va_likely: + query += f' artist:"{artist}"' + else: + # Deezer drops unquoted free text as soon as the query carries any + # field:"value" filter, so `