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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@
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
Expand Down Expand Up @@ -744,20 +745,13 @@ def formatted(
"""
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()
)

Expand Down
25 changes: 11 additions & 14 deletions beets/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
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
Expand All @@ -39,6 +38,7 @@

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

Expand Down Expand Up @@ -99,10 +99,9 @@ def add(self, lib=None):
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)
Expand Down Expand Up @@ -524,8 +523,8 @@ def art_destination(self, image, item_dir=None):
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()
Expand Down Expand Up @@ -1178,7 +1177,10 @@ def move(
# 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).
Expand Down Expand Up @@ -1208,13 +1210,8 @@ def destination(
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":
Expand Down
8 changes: 2 additions & 6 deletions beets/ui/commands/modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 5 additions & 3 deletions beets/util/functemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
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 = "%"
Expand Down Expand Up @@ -508,8 +510,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)


Expand Down
11 changes: 2 additions & 9 deletions beets/util/pathformats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@

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.
Expand All @@ -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()]
8 changes: 2 additions & 6 deletions beetsplug/bench.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
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

Expand All @@ -31,10 +30,7 @@ def _build_tree():

# 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(
Expand All @@ -49,7 +45,7 @@ def _build_tree():

# 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(
Expand Down
2 changes: 1 addition & 1 deletion beetsplug/smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def update_playlists(self, lib: Library) -> None:
# 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)

Expand Down
6 changes: 3 additions & 3 deletions test/plugins/test_smartplaylist.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def test_playlist_update(self):
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")

Expand Down Expand Up @@ -215,7 +215,7 @@ def test_playlist_update_output_extm3u(self):
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")

Expand Down Expand Up @@ -264,7 +264,7 @@ def test_playlist_update_output_extm3u_fields(self):
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")

Expand Down
4 changes: 1 addition & 3 deletions test/ui/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ def test_paths_section_respected(self):
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:
Expand Down
4 changes: 1 addition & 3 deletions test/util/test_pathformats.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading