PICARD-2218: Support setting regex flags in $performer pattern

This commit is contained in:
Philipp Wolfer
2021-06-02 23:18:45 +02:00
parent bdfd5b8156
commit 0209b5ed28
4 changed files with 30 additions and 7 deletions

View File

@@ -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():

View File

@@ -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

View File

@@ -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'

View File

@@ -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(.*/')