From 0209b5ed2805e16596ad552cbaaf75a8dd77bc9c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 2 Jun 2021 23:18:45 +0200 Subject: [PATCH] PICARD-2218: Support setting regex flags in $performer pattern --- picard/script/functions.py | 7 ++++--- picard/util/__init__.py | 15 +++++++++++---- test/test_script.py | 8 ++++++++ test/test_utils.py | 7 +++++++ 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/picard/script/functions.py b/picard/script/functions.py index 8e740f7b9..f7f0dcca8 100644 --- a/picard/script/functions.py +++ b/picard/script/functions.py @@ -764,15 +764,16 @@ def func_lenmulti(parser, multi, separator=MULTI_VALUED_JOINER): """`$performer(pattern="",join=", ")` Returns the performers where the performance type (e.g. "vocal") matches `pattern`, joined by `join`. -You can specify a regular expression by surrounding the pattern with `/.../`. For example -`/^guitars?$/` matches the performance type "guitar" or "guitars", but not e.g. "bass guitar". +You can specify a regular expression in the format `/pattern/flags`. `flags` are optional. Currently +the only supported flag is "i" (ignore case). For example `$performer(/^guitars?$/i)` matches the +performance type "guitar" or "Guitars", but not e.g. "bass guitar". _Since Picard 0.10_""" )) def func_performer(parser, pattern="", join=", "): values = [] try: - regex = pattern_as_regex(pattern, allow_wildcards=False, flags=re.IGNORECASE) + regex = pattern_as_regex(pattern, allow_wildcards=False) except re.error: return '' for name, value in parser.context.items(): diff --git a/picard/util/__init__.py b/picard/util/__init__.py index fbf50397d..fab58e447 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -734,7 +734,9 @@ def extract_year_from_date(dt): def pattern_as_regex(pattern, allow_wildcards=False, flags=0): """Parses a string and interprets it as a matching pattern. - - If pattern starts and ends with / it is interpreted as a regular expression (e.g. `/foo.*/`) + - If pattern is of the form /pattern/flags it is interpreted as a regular expression (e.g. `/foo.*/`). + The flags are optional and in addition to the flags passed in the `flags` function parameter. Supported + flags in the expression are "i" (ignore case) and "m" (multiline) - Otherwise if `allow_wildcards` is True, it is interpreted as a pattern that allows wildcard matching (see below) - If `allow_wildcards` is False a regex matching the literal string is returned @@ -750,9 +752,14 @@ def pattern_as_regex(pattern, allow_wildcards=False, flags=0): Raises: `re.error` if the regular expression could not be parsed """ - if len(pattern) > 2 and pattern[0] == '/' and pattern[-1] == '/': - pattern = pattern[1:-1] - return re.compile(pattern, flags) + plain_pattern = pattern.rstrip('im') + if len(plain_pattern) > 2 and plain_pattern[0] == '/' and plain_pattern[-1] == '/': + extra_flags = pattern[len(plain_pattern):] + if 'i' in extra_flags: + flags |= re.IGNORECASE + if 'm' in extra_flags: + flags |= re.MULTILINE + return re.compile(plain_pattern[1:-1], flags) elif allow_wildcards: # FIXME?: only support '*' (not '?' or '[abc]') # replace multiple '*' by one diff --git a/test/test_script.py b/test/test_script.py index 61b13fdde..f65c470a3 100644 --- a/test/test_script.py +++ b/test/test_script.py @@ -959,6 +959,14 @@ class ScriptParserTest(PicardTestCase): self.assertScriptResultEquals(r"$performer(/drums \(/)", "", context) self.assertScriptResultEquals(r"$performer(drums \()", "Drummer", context) + def test_cmd_performer_regex_ignore_case(self): + context = Metadata() + context['performer:guitar'] = 'Foo1' + context['performer:GUITARS'] = 'Foo2' + context['performer:rhythm-guitar'] = 'Foo3' + result = self.parser.eval(r"$performer(/^guitars?/i)", context=context) + self.assertEqual({'Foo1', 'Foo2'}, set(result.split(', '))) + def test_cmd_performer_custom_join(self): context = Metadata() context['performer:guitar'] = 'Foo1' diff --git a/test/test_utils.py b/test/test_utils.py index 0593b94e6..992b97c83 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -531,6 +531,13 @@ class PatternAsRegexTest(PicardTestCase): self.assertTrue(regex.flags & re.IGNORECASE) self.assertTrue(regex.flags & re.MULTILINE) + def test_regex_extra_flags(self): + regex = pattern_as_regex(r'/^foo.*/im', flags=re.VERBOSE) + self.assertEqual(r'^foo.*', regex.pattern) + self.assertTrue(regex.flags & re.VERBOSE) + self.assertTrue(regex.flags & re.IGNORECASE) + self.assertTrue(regex.flags & re.MULTILINE) + def test_regex_raises(self): with self.assertRaises(re.error): pattern_as_regex(r'/^foo(.*/')