mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
## Description Fixes [#6115](https://github.com/beetbox/beets/issues/6115). When an inline field definition shadows a built-in database field (e.g., redefining `track_no` in `item_fields`), the inline plugin evaluates the field template by constructing a dictionary of all item values. Previously, this triggered unbounded recursion because `_dict_for(obj)` re-entered `__getitem__` for the same key while evaluating the computed field. This PR adds a per-object, per-key evaluation guard to prevent re-entry when the same inline field is accessed during expression evaluation. This resolves the recursion error while preserving normal computed-field behavior. A regression test (`TestInlineRecursion.test_no_recursion_when_inline_shadows_fixed_field`) verifies that `$track_no` evaluates correctly (`'01'`) when shadowed. ## To Do - [x] ~Documentation.~ - [x] ~Changelog.~ - [x] Tests.
This commit is contained in:
commit
b4f0dbf53b
3 changed files with 75 additions and 4 deletions
|
|
@ -61,18 +61,18 @@ class InlinePlugin(BeetsPlugin):
|
||||||
config["item_fields"].items(), config["pathfields"].items()
|
config["item_fields"].items(), config["pathfields"].items()
|
||||||
):
|
):
|
||||||
self._log.debug("adding item field {}", key)
|
self._log.debug("adding item field {}", key)
|
||||||
func = self.compile_inline(view.as_str(), False)
|
func = self.compile_inline(view.as_str(), False, key)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
self.template_fields[key] = func
|
self.template_fields[key] = func
|
||||||
|
|
||||||
# Album fields.
|
# Album fields.
|
||||||
for key, view in config["album_fields"].items():
|
for key, view in config["album_fields"].items():
|
||||||
self._log.debug("adding album field {}", key)
|
self._log.debug("adding album field {}", key)
|
||||||
func = self.compile_inline(view.as_str(), True)
|
func = self.compile_inline(view.as_str(), True, key)
|
||||||
if func is not None:
|
if func is not None:
|
||||||
self.album_template_fields[key] = func
|
self.album_template_fields[key] = func
|
||||||
|
|
||||||
def compile_inline(self, python_code, album):
|
def compile_inline(self, python_code, album, field_name):
|
||||||
"""Given a Python expression or function body, compile it as a path
|
"""Given a Python expression or function body, compile it as a path
|
||||||
field function. The returned function takes a single argument, an
|
field function. The returned function takes a single argument, an
|
||||||
Item, and returns a Unicode string. If the expression cannot be
|
Item, and returns a Unicode string. If the expression cannot be
|
||||||
|
|
@ -97,7 +97,12 @@ class InlinePlugin(BeetsPlugin):
|
||||||
is_expr = True
|
is_expr = True
|
||||||
|
|
||||||
def _dict_for(obj):
|
def _dict_for(obj):
|
||||||
out = dict(obj)
|
out = {}
|
||||||
|
for key in obj.keys(computed=False):
|
||||||
|
if key == field_name:
|
||||||
|
continue
|
||||||
|
out[key] = obj._get(key)
|
||||||
|
|
||||||
if album:
|
if album:
|
||||||
out["items"] = list(obj.items())
|
out["items"] = list(obj.items())
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ New features:
|
||||||
|
|
||||||
Bug fixes:
|
Bug fixes:
|
||||||
|
|
||||||
|
- :doc:`plugins/inline`: Fix recursion error when an inline field definition
|
||||||
|
shadows a built-in item field (e.g., redefining ``track_no``). Inline
|
||||||
|
expressions now skip self-references during evaluation to avoid infinite
|
||||||
|
recursion. :bug:`6115`
|
||||||
- When hardlinking from a symlink (e.g. importing a symlink with hardlinking
|
- When hardlinking from a symlink (e.g. importing a symlink with hardlinking
|
||||||
enabled), dereference the symlink then hardlink, rather than creating a new
|
enabled), dereference the symlink then hardlink, rather than creating a new
|
||||||
(potentially broken) symlink :bug:`5676`
|
(potentially broken) symlink :bug:`5676`
|
||||||
|
|
|
||||||
62
test/plugins/test_inline.py
Normal file
62
test/plugins/test_inline.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# This file is part of beets.
|
||||||
|
# Copyright 2025, Gabe Push.
|
||||||
|
#
|
||||||
|
# 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 beets import config, plugins
|
||||||
|
from beets.test.helper import PluginTestCase
|
||||||
|
from beetsplug.inline import InlinePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class TestInlineRecursion(PluginTestCase):
|
||||||
|
def test_no_recursion_when_inline_shadows_fixed_field(self):
|
||||||
|
config["plugins"] = ["inline"]
|
||||||
|
|
||||||
|
config["item_fields"] = {
|
||||||
|
"track_no": (
|
||||||
|
"f'{disc:02d}-{track:02d}' if disctotal > 1 else f'{track:02d}'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins._instances.clear()
|
||||||
|
plugins.load_plugins()
|
||||||
|
|
||||||
|
item = self.add_item_fixture(
|
||||||
|
artist="Artist",
|
||||||
|
album="Album",
|
||||||
|
title="Title",
|
||||||
|
track=1,
|
||||||
|
disc=1,
|
||||||
|
disctotal=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
out = item.evaluate_template("$track_no")
|
||||||
|
|
||||||
|
assert out == "01"
|
||||||
|
|
||||||
|
def test_inline_function_body_item_field(self):
|
||||||
|
plugin = InlinePlugin()
|
||||||
|
func = plugin.compile_inline(
|
||||||
|
"return track + 1", album=False, field_name="next_track"
|
||||||
|
)
|
||||||
|
|
||||||
|
item = self.add_item_fixture(track=3)
|
||||||
|
assert func(item) == 4
|
||||||
|
|
||||||
|
def test_inline_album_expression_uses_items(self):
|
||||||
|
plugin = InlinePlugin()
|
||||||
|
func = plugin.compile_inline(
|
||||||
|
"len(items)", album=True, field_name="item_count"
|
||||||
|
)
|
||||||
|
|
||||||
|
album = self.add_album_fixture()
|
||||||
|
assert func(album) == len(list(album.items()))
|
||||||
Loading…
Reference in a new issue