Skip to content

Commit db640a0

Browse files
authored
Implement regular expression search for context (#272)
Support new operator `~` for matching patterns and `!~` for negative matching patterns in the context rules. Fix #226.
1 parent 95c9745 commit db640a0

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

docs/context.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ supported operators consult the following grammar outline::
4747
expression ::= 'true' | 'false'
4848
dimension ::= [[:alnum:]]+
4949
binary_operator ::= '==' | '!=' | '<' | '<=' | '>' | '>=' |
50-
'~=' | '~!=' | '~<' | '~<=' | '~>' | '~>='
50+
'~=' | '~!=' | '~<' | '~<=' | '~>' | '~>=' | '~' | '!~'
5151
unary_operator ::= 'is defined' | 'is not defined'
5252
values ::= value (',' value)*
5353
value ::= [[:alnum:]]+
@@ -65,6 +65,12 @@ Let's demonstrate the syntax on a couple of real-life examples::
6565
# check whether a dimension is defined
6666
collection is not defined
6767

68+
# search dimension value for a regular expression
69+
initiator ~ .*-ci
70+
71+
# make sure that the value does not match given regular expression
72+
arch !~ ppc64.*
73+
6874
# disable adjust rule (e.g. during debugging / experimenting)
6975
false and <original rule>
7076

docs/releases.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
======================
66

77

8+
fmf-1.6.0
9+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
10+
11+
In order to search :ref:`context` dimension values using regular
12+
expressions, it is now possible to use operator ``~`` for matching
13+
patterns and operator ``!~`` for non matching patterns.
14+
15+
816
fmf-1.5.0
917
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1018

fmf/context.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ class InvalidContext(Exception):
3434
class ContextValue:
3535
""" Value for dimension """
3636

37-
def __init__(self, origin):
37+
def __init__(self, raw):
3838
"""
3939
ContextValue("foo-1.2.3")
4040
ContextValue(["foo", "1", "2", "3"])
4141
"""
42-
if isinstance(origin, (tuple, list)):
43-
self._to_compare = tuple(origin)
42+
if isinstance(raw, (tuple, list)):
43+
self._to_compare = tuple(raw)
4444
else:
45-
self._to_compare = self._split_to_version(origin)
45+
self._to_compare = self._split_to_version(raw)
46+
47+
# Store the original string for regexp processing
48+
self.raw = raw
4649

4750
def __eq__(self, other):
4851
if isinstance(other, self.__class__):
@@ -238,6 +241,22 @@ def comparator(dimension_value, it_val):
238241

239242
return self._op_core(dimension_name, values, comparator)
240243

244+
def _op_match(self, dimension_name, values):
245+
""" '~' operator, regular expression matches """
246+
247+
def comparator(dimension_value, it_val):
248+
return re.search(it_val.raw, dimension_value.raw) is not None
249+
250+
return self._op_core(dimension_name, values, comparator)
251+
252+
def _op_not_match(self, dimension_name, values):
253+
""" '~' operator, regular expression does not match """
254+
255+
def comparator(dimension_value, it_val):
256+
return re.search(it_val.raw, dimension_value.raw) is None
257+
258+
return self._op_core(dimension_name, values, comparator)
259+
241260
def _op_minor_eq(self, dimension_name, values):
242261
""" '~=' operator """
243262

@@ -371,6 +390,8 @@ def _op_core(self, dimension_name, values, comparator):
371390
"~>=": _op_minor_greater_or_equal,
372391
">": _op_greater,
373392
"~>": _op_minor_greater,
393+
"~": _op_match,
394+
"!~": _op_not_match,
374395
}
375396

376397
# Triple expression: dimension operator values
@@ -379,7 +400,7 @@ def _op_core(self, dimension_name, values, comparator):
379400
r"(\w+)"
380401
+ r"\s*("
381402
+ r"|".join(
382-
set(operator_map.keys()) - {"is defined", "is not defined"})
403+
[key for key in operator_map if key not in ["is defined", "is not defined"]])
383404
+ r")\s*"
384405
+ r"([^=].*)")
385406
# Double expression: dimension operator

tests/unit/test_context.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,31 @@ def test_case_insensitive(self):
282282
assert python.matches("component > python3-3.7")
283283
assert python.matches("component < PYTHON3-3.9")
284284

285+
def test_regular_expression_matching(self):
286+
""" Matching regular expressions """
287+
288+
assert Context(distro="fedora-42").matches("distro ~ ^fedora-42$")
289+
assert Context(distro="fedora-42").matches("distro ~ fedora")
290+
assert Context(distro="fedora-42").matches("distro ~ fedora|rhel")
291+
assert Context(distro="fedora-42").matches("distro ~ fedora-4.*")
292+
assert not Context(distro="fedora-42").matches("distro ~ fedora-3.*")
293+
assert not Context(distro="fedora-42").matches("distro ~ ubuntu")
294+
295+
assert Context(arch="ppc64").matches("arch ~ ppc64.*")
296+
assert Context(arch="ppc64le").matches("arch ~ ppc64.*")
297+
assert not Context(arch="ppc64le").matches("arch ~ ppc64$")
298+
299+
assert not Context(distro="fedora-42").matches("distro !~ ^fedora-42$")
300+
assert not Context(distro="fedora-42").matches("distro !~ fedora")
301+
assert not Context(distro="fedora-42").matches("distro !~ fedora|rhel")
302+
assert not Context(distro="fedora-42").matches("distro !~ fedora-4.*")
303+
assert Context(distro="fedora-42").matches("distro !~ fedora-3.*")
304+
assert Context(distro="fedora-42").matches("distro !~ ubuntu")
305+
306+
assert not Context(arch="ppc64").matches("arch !~ ppc64.*")
307+
assert not Context(arch="ppc64le").matches("arch !~ ppc64.*")
308+
assert Context(arch="ppc64le").matches("arch !~ ppc64$")
309+
285310

286311
class TestContextValue:
287312
impossible_split = ["x86_64", "ppc64", "fips", "errata"]

0 commit comments

Comments
 (0)