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=", ")` """`$performer(pattern="",join=", ")`
Returns the performers where the performance type (e.g. "vocal") matches `pattern`, joined by `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 You can specify a regular expression in the format `/pattern/flags`. `flags` are optional. Currently
`/^guitars?$/` matches the performance type "guitar" or "guitars", but not e.g. "bass guitar". 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_""" _Since Picard 0.10_"""
)) ))
def func_performer(parser, pattern="", join=", "): def func_performer(parser, pattern="", join=", "):
values = [] values = []
try: try:
regex = pattern_as_regex(pattern, allow_wildcards=False, flags=re.IGNORECASE) regex = pattern_as_regex(pattern, allow_wildcards=False)
except re.error: except re.error:
return '' return ''
for name, value in parser.context.items(): 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): def pattern_as_regex(pattern, allow_wildcards=False, flags=0):
"""Parses a string and interprets it as a matching pattern. """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) - 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 - 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 Raises: `re.error` if the regular expression could not be parsed
""" """
if len(pattern) > 2 and pattern[0] == '/' and pattern[-1] == '/': plain_pattern = pattern.rstrip('im')
pattern = pattern[1:-1] if len(plain_pattern) > 2 and plain_pattern[0] == '/' and plain_pattern[-1] == '/':
return re.compile(pattern, flags) 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: elif allow_wildcards:
# FIXME?: only support '*' (not '?' or '[abc]') # FIXME?: only support '*' (not '?' or '[abc]')
# replace multiple '*' by one # replace multiple '*' by one

View File

@@ -959,6 +959,14 @@ class ScriptParserTest(PicardTestCase):
self.assertScriptResultEquals(r"$performer(/drums \(/)", "", context) self.assertScriptResultEquals(r"$performer(/drums \(/)", "", context)
self.assertScriptResultEquals(r"$performer(drums \()", "Drummer", 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): def test_cmd_performer_custom_join(self):
context = Metadata() context = Metadata()
context['performer:guitar'] = 'Foo1' context['performer:guitar'] = 'Foo1'

View File

@@ -531,6 +531,13 @@ class PatternAsRegexTest(PicardTestCase):
self.assertTrue(regex.flags & re.IGNORECASE) self.assertTrue(regex.flags & re.IGNORECASE)
self.assertTrue(regex.flags & re.MULTILINE) 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): def test_regex_raises(self):
with self.assertRaises(re.error): with self.assertRaises(re.error):
pattern_as_regex(r'/^foo(.*/') pattern_as_regex(r'/^foo(.*/')