diff --git a/picard/script.py b/picard/script.py index a99f0f02e..c2e32d51e 100644 --- a/picard/script.py +++ b/picard/script.py @@ -28,6 +28,7 @@ from inspect import getfullargspec from picard.metadata import Metadata from picard.metadata import MULTI_VALUED_JOINER from picard.plugin import ExtensionPoint +from picard.util import uniqify class ScriptError(Exception): @@ -324,6 +325,31 @@ def _compute_logic(operation, *args): return operation(args) +def _get_multi_values(parser, multi, separator): + if isinstance(separator, ScriptExpression): + separator = separator.eval(parser) + + if separator == MULTI_VALUED_JOINER: + # Convert ScriptExpression containing only a single variable into variable + if (isinstance(multi, ScriptExpression) and + len(multi) == 1 and + isinstance(multi[0], ScriptVariable)): + multi = multi[0] + + # If a variable, return multi-values + if isinstance(multi, ScriptVariable): + if multi.name.startswith("_"): + name = "~" + multi.name[1:] + else: + name = multi.name + return parser.context.getall(name) + + # Fall-back to converting to a string and splitting if haystack is an expression + # or user has overridden the separator character. + multi = multi.eval(parser) + return multi.split(separator) if separator else [multi] + + def func_if(parser, _if, _then, _else=None): """If ``if`` is not empty, it returns ``then``, otherwise it returns ``else``.""" if _if.eval(parser): @@ -395,9 +421,14 @@ def func_in(parser, text, needle): return "" -def func_inmulti(parser, text, value, separator=MULTI_VALUED_JOINER): - """Splits ``text`` by ``separator``, and returns true if the resulting list contains ``value``.""" - return func_in(parser, text.split(separator) if separator else [text], value) +def func_inmulti(parser, haystack, needle, separator=MULTI_VALUED_JOINER): + """Searches for ``needle`` in ``haystack``, supporting a list variable for + ``haystack``. If a string is used instead, then a ``separator`` can be + used to split it. In both cases, it returns true if the resulting list + contains exactly ``needle`` as a member.""" + + needle = needle.eval(parser) + return func_in(parser, _get_multi_values(parser, haystack, separator), needle) def func_rreplace(parser, text, old, new): @@ -486,7 +517,7 @@ def func_copymerge(parser, new, old): old = "~" + old[1:] newvals = parser.context.getall(new) oldvals = parser.context.getall(old) - parser.context[new] = newvals + list(set(oldvals) - set(newvals)) + parser.context[new] = uniqify(newvals + oldvals) return "" @@ -644,6 +675,10 @@ def func_len(parser, text=""): return string_(len(text)) +def func_lenmulti(parser, multi, separator=MULTI_VALUED_JOINER): + return func_len(parser, _get_multi_values(parser, multi, separator)) + + def func_performer(parser, pattern="", join=", "): values = [] for name, value in parser.context.items(): @@ -827,10 +862,11 @@ register_script_function(func_lte, "lte") register_script_function(func_gt, "gt") register_script_function(func_gte, "gte") register_script_function(func_in, "in") -register_script_function(func_inmulti, "inmulti") +register_script_function(func_inmulti, "inmulti", eval_args=False) register_script_function(func_copy, "copy") register_script_function(func_copymerge, "copymerge") register_script_function(func_len, "len") +register_script_function(func_lenmulti, "lenmulti", eval_args=False) register_script_function(func_performer, "performer") register_script_function(func_matchedtracks, "matchedtracks") register_script_function(func_is_complete, "is_complete") diff --git a/test/test_script.py b/test/test_script.py index 6727e6952..24c9c2b4d 100644 --- a/test/test_script.py +++ b/test/test_script.py @@ -11,9 +11,12 @@ class ScriptParserTest(unittest.TestCase): config.setting = { 'enabled_plugins': '', } + self.parser = ScriptParser() + def func_noargstest(parser): return "" + register_script_function(func_noargstest, "noargstest") def assertScriptResultEquals(self, script, expected, context=None): @@ -172,16 +175,10 @@ class ScriptParserTest(unittest.TestCase): self.assertScriptResultEquals("$upper(AbeCeDA)", "ABECEDA") def test_cmd_rreplace(self): - self.assertEqual( - self.parser.eval(r'''$rreplace(test \(disc 1\),\\s\\\(disc \\d+\\\),)'''), - "test" - ) + self.assertScriptResultEquals(r'''$rreplace(test \(disc 1\),\\s\\\(disc \\d+\\\),)''', "test") def test_cmd_rsearch(self): - self.assertEqual( - self.parser.eval(r"$rsearch(test \(disc 1\),\\\(disc \(\\d+\)\\\))"), - "1" - ) + self.assertScriptResultEquals(r"$rsearch(test \(disc 1\),\\\(disc \(\\d+\)\\\))", "1") def test_arguments(self): self.assertTrue( @@ -271,7 +268,7 @@ class ScriptParserTest(unittest.TestCase): def _eval_and_check_copymerge(self, context, expected): self.parser.eval("$copymerge(target,source)", context) - self.assertEqual(sorted(self.parser.context.getall("target")), sorted(expected)) + self.assertEqual(self.parser.context.getall("target"), expected) def test_cmd_copymerge_notarget(self): context = Metadata() @@ -287,8 +284,8 @@ class ScriptParserTest(unittest.TestCase): def test_cmd_copymerge_removedupes(self): context = Metadata() - context["target"] = ["tag1", "tag2"] - context["source"] = ["tag2", "tag3"] + context["target"] = ["tag1", "tag2", "tag1"] + context["source"] = ["tag2", "tag3", "tag2"] self._eval_and_check_copymerge(context, ["tag1", "tag2", "tag3"]) def test_cmd_copymerge_nonlist(self): @@ -385,6 +382,75 @@ class ScriptParserTest(unittest.TestCase): self.assertNotIn('performer:bar', context) self.assertNotIn('performer:foo', context) + def test_cmd_inmulti(self): + context = Metadata() + + # Test with single-value string + context["foo"] = "First:A; Second:B; Third:C" + # Tests with $in for comparison purposes + self.assertScriptResultEquals("$in(%foo%,Second:B)", "1", context) + self.assertScriptResultEquals("$in(%foo%,irst:A; Second:B; Thi)", "1", context) + self.assertScriptResultEquals("$in(%foo%,First:A; Second:B; Third:C)", "1", context) + # Base $inmulti tests + self.assertScriptResultEquals("$inmulti(%foo%,Second:B)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First:A; Second:B; Third:C)", "1", context) + # Test separator override but with existing separator - results should be same as base + self.assertScriptResultEquals("$inmulti(%foo%,Second:B,; )", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi,; )", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First:A; Second:B; Third:C,; )", "1", context) + # Test separator override + self.assertScriptResultEquals("$inmulti(%foo%,First:A,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,Second:B,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,Third:C,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,A; Second,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,B; Third,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,C,:)", "1", context) + + # Test with multi-values + context["foo"] = ["First:A", "Second:B", "Third:C"] + # Tests with $in for comparison purposes + self.assertScriptResultEquals("$in(%foo%,Second:B)", "1", context) + self.assertScriptResultEquals("$in(%foo%,irst:A; Second:B; Thi)", "1", context) + self.assertScriptResultEquals("$in(%foo%,First:A; Second:B; Third:C)", "1", context) + # Base $inmulti tests + self.assertScriptResultEquals("$inmulti(%foo%,Second:B)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First:A; Second:B; Third:C)", "", context) + # Test separator override but with existing separator - results should be same as base + self.assertScriptResultEquals("$inmulti(%foo%,Second:B,; )", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,irst:A; Second:B; Thi,; )", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First:A; Second:B; Third:C,; )", "", context) + # Test separator override + self.assertScriptResultEquals("$inmulti(%foo%,First:A,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,Second:B,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,Third:C,:)", "", context) + self.assertScriptResultEquals("$inmulti(%foo%,First,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,A; Second,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,B; Third,:)", "1", context) + self.assertScriptResultEquals("$inmulti(%foo%,C,:)", "1", context) + + def test_cmd_lenmulti(self): + context = Metadata() + context["foo"] = "First:A; Second:B; Third:C" + context["bar"] = ["First:A", "Second:B", "Third:C"] + # Tests with $len for comparison purposes + self.assertScriptResultEquals("$len(%foo%)", "26", context) + self.assertScriptResultEquals("$len(%bar%)", "26", context) + # Base $lenmulti tests + self.assertScriptResultEquals("$lenmulti(%foo%)", "1", context) + self.assertScriptResultEquals("$lenmulti(%bar%)", "3", context) + self.assertScriptResultEquals("$lenmulti(%foo%.)", "3", context) + # Test separator override but with existing separator - results should be same as base + self.assertScriptResultEquals("$lenmulti(%foo%,; )", "1", context) + self.assertScriptResultEquals("$lenmulti(%bar%,; )", "3", context) + self.assertScriptResultEquals("$lenmulti(%foo%.,; )", "3", context) + # Test separator override + self.assertScriptResultEquals("$lenmulti(%foo%,:)", "4", context) + self.assertScriptResultEquals("$lenmulti(%bar%,:)", "4", context) + self.assertScriptResultEquals("$lenmulti(%foo%.,:)", "4", context) + def test_required_kwonly_parameters(self): def func(a, *, required_kwarg): pass