mirror of
https://github.com/beetbox/beets.git
synced 2026-07-16 19:00:49 -04:00
Refactor template evaluation to handle format strings ONLY
- Handle formats as strings ONLY and evaluate them through a shared cached template helper. - This removes the need for conditional logic that acts on Template objects or strings.
This commit is contained in:
@@ -34,8 +34,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
|
||||
@@ -676,20 +677,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()
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,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
|
||||
@@ -39,6 +38,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from beets.dbcore.query import FieldQuery, FieldQueryType
|
||||
from beets.dbcore.sort import FieldSort
|
||||
from beets.util.pathformats import PathFormat
|
||||
|
||||
from .library import Library # noqa: F401
|
||||
|
||||
@@ -101,10 +101,9 @@ class LibModel(dbcore.Model["Library"]):
|
||||
super().add(lib)
|
||||
|
||||
def __format__(self, spec):
|
||||
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):
|
||||
return format(self)
|
||||
@@ -526,8 +525,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, beets.config["path_sep_replace"].as_str()
|
||||
@@ -1182,7 +1181,10 @@ class Item(LibModel):
|
||||
# Templating.
|
||||
|
||||
def destination(
|
||||
self, relative_to_libdir=False, basedir=None, path_formats=None
|
||||
self,
|
||||
relative_to_libdir=False,
|
||||
basedir=None,
|
||||
path_formats: list[PathFormat] | None = None,
|
||||
) -> bytes:
|
||||
"""Return the path in the library directory designated for the item
|
||||
(i.e., where the file ought to be).
|
||||
@@ -1212,13 +1214,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)
|
||||
|
||||
# Prepare path for output: normalize Unicode characters.
|
||||
if sys.platform == "darwin":
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from beets import library, ui
|
||||
from beets.exceptions import UserError
|
||||
from beets.util import functemplate
|
||||
from beets.util.deprecation import maybe_replace_legacy_field
|
||||
|
||||
from .utils import do_query
|
||||
@@ -26,13 +25,10 @@ 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 = {
|
||||
key: functemplate.template(value) for key, value in mods.items()
|
||||
}
|
||||
for obj in objs:
|
||||
obj_mods = {
|
||||
key: model_cls._parse(key, obj.evaluate_template(templates[key]))
|
||||
for key in mods.keys()
|
||||
key: model_cls._parse(key, obj.evaluate_template(fmt))
|
||||
for key, fmt in mods.items()
|
||||
}
|
||||
if print_and_modify(obj, obj_mods, dels) and obj not in changed:
|
||||
changed.append(obj)
|
||||
|
||||
@@ -12,11 +12,13 @@ This is sort of like a tiny, horrible degeneration of a real templating
|
||||
engine like Jinja2 or Mustache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import dis
|
||||
import functools
|
||||
import re
|
||||
import types
|
||||
from functools import lru_cache
|
||||
|
||||
SYMBOL_DELIM = "$"
|
||||
FUNC_DELIM = "%"
|
||||
@@ -494,8 +496,8 @@ def _parse(template):
|
||||
return Expression(parts)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=128)
|
||||
def template(fmt):
|
||||
@lru_cache(maxsize=128)
|
||||
def get_template(fmt: str) -> Template:
|
||||
return Template(fmt)
|
||||
|
||||
|
||||
|
||||
@@ -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()]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -389,7 +389,7 @@ class SmartPlaylistPlugin(plugins.BeetsPlugin):
|
||||
# the items and generate the correct m3u file names.
|
||||
matched_items: list[Item] = []
|
||||
for item in items:
|
||||
m3u_name = item.evaluate_template(name, True)
|
||||
m3u_name = item.evaluate_template(name, for_path=True)
|
||||
m3u_name = sanitize_path(m3u_name, lib.replacements)
|
||||
item_uri = self.get_item_uri(item)
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class SmartPlaylistTest(BeetsTestCase):
|
||||
spl = SmartPlaylistPlugin()
|
||||
|
||||
i = Mock(path=b"/tagada.mp3")
|
||||
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
|
||||
i.evaluate_template.side_effect = lambda pl, **__: os.fsdecode(
|
||||
pl
|
||||
).replace("$title", "ta:ga:da")
|
||||
|
||||
@@ -201,7 +201,7 @@ class SmartPlaylistTest(BeetsTestCase):
|
||||
type(i).title = PropertyMock(return_value="fake title")
|
||||
type(i).length = PropertyMock(return_value=300.123)
|
||||
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
|
||||
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
|
||||
i.evaluate_template.side_effect = lambda pl, **__: os.fsdecode(
|
||||
pl
|
||||
).replace("$title", "ta:ga:da")
|
||||
|
||||
@@ -250,7 +250,7 @@ class SmartPlaylistTest(BeetsTestCase):
|
||||
type(i).path = PropertyMock(return_value=b"/tagada.mp3")
|
||||
a = {"id": 456, "genres": ["Rock", "Pop"]}
|
||||
i.__getitem__.side_effect = a.__getitem__
|
||||
i.evaluate_template.side_effect = lambda pl, *_: os.fsdecode(
|
||||
i.evaluate_template.side_effect = lambda pl, **__: os.fsdecode(
|
||||
pl
|
||||
).replace("$title", "ta:ga:da")
|
||||
|
||||
|
||||
@@ -148,9 +148,7 @@ class ConfigTest(IOMixin, TestPluginTestCase):
|
||||
config.write("paths: {x: y}")
|
||||
|
||||
self.run_command("test")
|
||||
key, template = self.test_cmd.lib.path_formats[0]
|
||||
assert key == "x"
|
||||
assert template.original == "y"
|
||||
assert self.test_cmd.lib.path_formats[0] == ("x", "y")
|
||||
|
||||
def test_nonexistant_db(self):
|
||||
with self.write_config_file() as config:
|
||||
|
||||
@@ -6,9 +6,7 @@ def test_get_path_formats(config):
|
||||
# override the default 'singleton' path and add a new one
|
||||
config["paths"].set({"singleton": "bar", "new": "hello"})
|
||||
|
||||
path_formats = get_path_formats(config["paths"])
|
||||
actual_path_formats = [(key, tmpl.original) for key, tmpl in path_formats]
|
||||
assert actual_path_formats == [
|
||||
assert get_path_formats(config["paths"]) == [
|
||||
("singleton:true", "bar"),
|
||||
("new", "hello"),
|
||||
# defaults
|
||||
|
||||
Reference in New Issue
Block a user