diff --git a/beetsplug/inline.py b/beetsplug/inline.py index 860a205ee..55a9c0f5a 100644 --- a/beetsplug/inline.py +++ b/beetsplug/inline.py @@ -32,12 +32,12 @@ class InlineError(Exception): ) -def _compile_func(body): +def _compile_func(body, args=""): """Given Python code for a function body, return a compiled callable that invokes that code. """ body = body.replace("\n", "\n ") - body = f"def {FUNC_NAME}():\n {body}" + body = f"def {FUNC_NAME}({args}):\n {body}" code = compile(body, "inline", "exec") env = {} eval(code, env) @@ -84,7 +84,7 @@ class InlinePlugin(BeetsPlugin): except SyntaxError: # Fall back to a function body. try: - func = _compile_func(python_code) + func = _compile_func(python_code, args="db_obj") except SyntaxError: self._log.error( "syntax error in inline field definition:\n{}", @@ -111,6 +111,7 @@ class InlinePlugin(BeetsPlugin): # For expressions, just evaluate and return the result. def _expr_func(obj): values = _dict_for(obj) + values["db_obj"] = obj try: return eval(code, values) except Exception as exc: @@ -124,7 +125,7 @@ class InlinePlugin(BeetsPlugin): old_globals = dict(func.__globals__) func.__globals__.update(_dict_for(obj)) try: - return func() + return func(obj) except Exception as exc: raise InlineError(python_code, exc) finally: diff --git a/docs/changelog.rst b/docs/changelog.rst index 1bd532ab9..cc278337f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,6 +26,9 @@ New features after upgrading to trigger the migration. Only then you can safely move the library to a new location. +- :doc:`plugins/inline`: Add access to the ``album`` or ``item`` object as + ``db_obj`` in inline fields. + Bug fixes ~~~~~~~~~ diff --git a/docs/plugins/inline.rst b/docs/plugins/inline.rst index d653b6d52..a4e648183 100644 --- a/docs/plugins/inline.rst +++ b/docs/plugins/inline.rst @@ -11,8 +11,11 @@ To use the ``inline`` plugin, enable it in your configuration (see Under this key, every line defines a new template field; the key is the name of the field (you'll use the name to refer to the field in your templates) and the value is a Python expression or function body. The Python code has all of a -track's fields in scope, so you can refer to any normal attributes (such as -``artist`` or ``title``) as Python variables. +track's normal fields in scope, so you can refer to these attributes (such as +``artist`` or ``title``) as Python variables. The Python code also has direct +access to the item or album object as ``db_obj``. This allows use of computed +fields and plugin fields, for example, ``db_obj.albumtotal``, or +``db_obj.missing`` if the :doc:`/plugins/missing` plugin is enabled. Here are a couple of examples of expressions: diff --git a/test/plugins/test_inline.py b/test/plugins/test_inline.py index 79118bd06..8a62c7d41 100644 --- a/test/plugins/test_inline.py +++ b/test/plugins/test_inline.py @@ -60,3 +60,46 @@ class TestInlineRecursion(PluginTestCase): album = self.add_album_fixture() assert func(album) == len(list(album.items())) + + def test_inline_album_expression_uses_items_via_obj(self): + plugin = InlinePlugin() + func = plugin.compile_inline( + "len(db_obj.items())", album=True, field_name="item_count" + ) + + album = self.add_album_fixture() + assert func(album) == len(list(album.items())) + + def test_inline_function_body_item_field_via_obj(self): + plugin = InlinePlugin() + func = plugin.compile_inline( + "return db_obj.track + 1", album=False, field_name="next_track" + ) + + item = self.add_item_fixture(track=3) + assert func(item) == 4 + + def test_inline_obj_missing(self): + config["plugins"] = ["inline", "missing"] + + config["album_fields"] = {"has_missing": ("bool(db_obj.missing)")} + + plugins._instances.clear() + plugins.load_plugins() + + album = self.add_album_fixture(track_count=1) + album.tracktotal = 3 + for item in album.items(): + item.tracktotal = 3 + item.store() + album.store() + assert album._get("has_missing") + + def test_inline_function_body_obj(self): + plugin = InlinePlugin() + func = plugin.compile_inline( + "return db_obj.title", album=False, field_name="title_value" + ) + + item = self.add_item_fixture(track=3) + assert func(item)