Skip to content

Commit d9d1e16

Browse files
author
Theofilos Manitaras
committed
Address PR comments and provide documentation
1 parent ef23a08 commit d9d1e16

File tree

2 files changed

+101
-66
lines changed

2 files changed

+101
-66
lines changed

reframe/utility/sanity.py

Lines changed: 85 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,68 @@ def findall(patt, filename, encoding='utf-8'):
521521
return list(evaluate(x) for x in finditer(patt, filename, encoding))
522522

523523

524+
def _callable_name(fn):
525+
fn_name = '<unknown>'
526+
try:
527+
# Assume fn is standard function
528+
fn_name = fn.__name__
529+
except AttributeError:
530+
try:
531+
# Assume fn is callable object
532+
fn_name = fn.__class__.__name__
533+
except AttributeError:
534+
pass
535+
536+
return fn_name
537+
538+
539+
def _extractiter_tag(patt, filename, tag, conv, encoding):
540+
if isinstance(conv, collections.Iterable):
541+
conv = conv[0]
542+
543+
for m in finditer(patt, filename, encoding):
544+
try:
545+
val = m.group(tag)
546+
except (IndexError, KeyError):
547+
raise SanityError(f'no such group in pattern {patt!r}: {tag}')
548+
549+
try:
550+
yield conv(val) if callable(conv) else val
551+
except ValueError:
552+
fn_name = _callable_name(conv)
553+
raise SanityError(
554+
f'could not convert value {val!r} using {fn_name}()')
555+
556+
557+
def _extractiter_multitag(patt, filename, tags, conv, encoding):
558+
for m in finditer(patt, filename, encoding):
559+
val = []
560+
for t in tags:
561+
try:
562+
val.append(m.group(t))
563+
except (IndexError, KeyError):
564+
raise SanityError(f'no such group in pattern {patt!r}: {t}')
565+
566+
converted_vals = []
567+
if not isinstance(conv, collections.Iterable):
568+
conv = [conv] * builtins.len(val)
569+
elif builtins.len(conv) > builtins.len(val):
570+
conv = conv[:builtins.len(val)]
571+
572+
# Here we use the last conversion function for the remaining
573+
# tags which don't have a corresponding one, if length of the
574+
# conversion function iterable is less that the one of tags
575+
for v, c in itertools.zip_longest(val, conv, fillvalue=conv[-1]):
576+
try:
577+
converted_vals.append(c(v) if callable(c) else v)
578+
except ValueError:
579+
fn_name = _callable_name(conv)
580+
raise SanityError(
581+
f'could not convert value {v!r} using {fn_name}()')
582+
583+
yield tuple(converted_vals)
584+
585+
524586
@deferrable
525587
def extractiter(patt, filename, tag=0, conv=None, encoding='utf-8'):
526588
'''Get an iterator over the values extracted from the capturing group
@@ -530,66 +592,10 @@ def extractiter(patt, filename, tag=0, conv=None, encoding='utf-8'):
530592
a generator object, instead of a list, which you can use to iterate over
531593
the extracted values.
532594
'''
533-
for m in finditer(patt, filename, encoding):
534-
if isinstance(tag, collections.Iterable) and not isinstance(tag, str):
535-
val = []
536-
for t in tag:
537-
try:
538-
val.append(m.group(t))
539-
except (IndexError, KeyError):
540-
raise SanityError(
541-
"no such group in pattern `%s': %s" % (patt, t))
542-
else:
543-
try:
544-
val = m.group(tag)
545-
except (IndexError, KeyError):
546-
raise SanityError(
547-
"no such group in pattern `%s': %s" % (patt, tag))
548-
549-
if isinstance(val, list):
550-
converted_vals = []
551-
if not isinstance(conv, collections.Iterable):
552-
conv = [conv] * len(val)
553-
554-
# Here we use the last conversion function for the remaining
555-
# tags which don't have a corresponding one
556-
for v, c in itertools.zip_longest(val, conv, fillvalue=conv[-1]):
557-
try:
558-
converted_vals.append(c(v) if callable(c) else v)
559-
except ValueError:
560-
fn_name = '<unknown>'
561-
try:
562-
# Assume conv is standard function
563-
fn_name = c.__name__
564-
except AttributeError:
565-
try:
566-
# Assume conv is callable object
567-
fn_name = c.__class__.__name__
568-
except AttributeError:
569-
pass
570-
571-
raise SanityError(
572-
"could not convert value `%s' using `%s()'" %
573-
(v, fn_name))
574-
575-
yield(tuple(converted_vals))
576-
else:
577-
try:
578-
yield conv(val) if callable(conv) else val
579-
except ValueError:
580-
fn_name = '<unknown>'
581-
try:
582-
# Assume conv is standard function
583-
fn_name = conv.__name__
584-
except AttributeError:
585-
try:
586-
# Assume conv is callable object
587-
fn_name = conv.__class__.__name__
588-
except AttributeError:
589-
pass
590-
591-
raise SanityError("could not convert value `%s' using `%s()'" %
592-
(val, fn_name))
595+
if isinstance(tag, collections.Iterable) and not isinstance(tag, str):
596+
yield from _extractiter_multitag(patt, filename, tag, conv, encoding)
597+
else:
598+
yield from _extractiter_tag(patt, filename, tag, conv, encoding)
593599

594600

595601
@deferrable
@@ -610,11 +616,26 @@ def extractall(patt, filename, tag=0, conv=None, encoding='utf-8'):
610616
Group ``0`` refers always to the whole match.
611617
Since the file is processed line by line, this means that group ``0``
612618
returns the whole line that was matched.
613-
:arg conv: A callable that takes a single argument and returns a new value.
614-
If provided, it will be used to convert the extracted values before
615-
returning them.
616-
:returns: A list of the extracted values from the matched regex.
619+
:arg conv: A callable or iterable of callables taking a single argument
620+
and returning a new value.
621+
If provided, and is not an iterable it will be used to convert
622+
the extracted values for all the capturing groups of ``tag``
623+
returning the converted values.
624+
If an iterable of callables is provided, each one will be used to
625+
convert the corresponding extracted capturing group of `tag`.
626+
If more callables functions than the corresponding capturing groups of
627+
``tag`` are provided, the last conversion function is used for the
628+
remaining capturing groups.
629+
:returns: A list of the extracted values from the matched regex if ``tag``
630+
converted using the ``conv`` callable if ``tag`` is a single value.
631+
In case of multiple capturing groups, a list of tuples where each one
632+
contains the extracted capturing converted using the corresponding
633+
callable of ``conv``.
617634
:raises reframe.core.exceptions.SanityError: In case of errors.
635+
636+
.. versionchanged:: 3.1
637+
Multiple regex capturing groups are now supporetd via ``tag`` and
638+
multiple callables can be used in ``conv``.
618639
'''
619640
return list(evaluate(x)
620641
for x in extractiter(patt, filename, tag, conv, encoding))

unittests/test_sanity_functions.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,15 +659,15 @@ def test_extractall_multiple_tags(self):
659659
r'Number: (\d+) (\d+)', self.tempfile, (1, 2)))
660660
for expected, v in enumerate(res, start=1):
661661
assert str(expected) == v[0]
662-
assert str(2 * expected) == v[1]
662+
assert str(2*expected) == v[1]
663663

664664
# Check multiple named groups
665665
res = sn.evaluate(sn.extractall(
666666
r'Number: (?P<no1>\d+) (?P<no2>\d+)', self.tempfile,
667667
('no1', 'no2')))
668668
for expected, v in enumerate(res, start=1):
669669
assert str(expected) == v[0]
670-
assert str(2 * expected) == v[1]
670+
assert str(2*expected) == v[1]
671671

672672
# Check single convert function
673673
res = sn.evaluate(sn.extractall(r'Number: (?P<no1>\d+) (?P<no2>\d+)',
@@ -685,6 +685,20 @@ def test_extractall_multiple_tags(self):
685685
assert 2 * expected == v[1]
686686
assert isinstance(v[1], float)
687687

688+
# Check more convert functions than tags
689+
res = sn.evaluate(sn.extractall(r'Number: (?P<no1>\d+) (?P<no2>\d+)',
690+
self.tempfile, ('no1', 'no2'),
691+
[int, float, float, float]))
692+
for expected, v in enumerate(res, start=1):
693+
assert expected == v[0]
694+
assert 2 * expected == v[1]
695+
696+
# Check multiple convert functions and single tag
697+
res = sn.evaluate(sn.extractall(
698+
r'Number: (?P<no>\d+) \d+', self.tempfile, 'no', [int, float]))
699+
for expected, v in enumerate(res, start=1):
700+
assert expected == v
701+
688702
# Check fewer convert functions than tags
689703
res = sn.evaluate(sn.extractall(r'Number: (?P<no1>\d+) (?P<no2>\d+)',
690704
self.tempfile, ('no1', 'no2'),

0 commit comments

Comments
 (0)