Skip to content

Commit

Permalink
Selectors support raise_on_error keyword argument (returned False pre…
Browse files Browse the repository at this point in the history
…viously).
  • Loading branch information
ynikitenko committed Apr 22, 2021
1 parent 8c4745c commit 2eeb502
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 13 deletions.
55 changes: 42 additions & 13 deletions lena/flow/selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,32 @@
# to prohibit nested ()s and []s and force user
# to define type explicitly.
class _SelectorOr(object):
def __init__(self, *args):
def __init__(self, args, raise_on_error=False):
self._selectors = []
for arg in args:
if isinstance(arg, Selector):
self._selectors.append(arg)
else:
# may raise
self._selectors.append(Selector(arg))
self._selectors.append(
Selector(arg, raise_on_error=raise_on_error)
)

def __call__(self, val):
return any((f(val) for f in self._selectors))


class _SelectorAnd(object):
def __init__(self, *args):
def __init__(self, args, raise_on_error=False):
self._selectors = []
for arg in args:
if isinstance(arg, Selector):
self._selectors.append(arg)
else:
# may raise
self._selectors.append(Selector(arg))
self._selectors.append(
Selector(arg, raise_on_error=raise_on_error)
)

def __call__(self, val):
return all((f(val) for f in self._selectors))
Expand All @@ -46,7 +50,7 @@ class Selector(object):
but other values can be used as well.
"""

def __init__(self, selector):
def __init__(self, selector, raise_on_error=False):
"""The usage of *selector* depends on its type.
If *selector* is a class,
Expand All @@ -64,6 +68,13 @@ def __init__(self, selector):
results of each item.
If it is a *tuple*, boolean *and* is applied to the results.
*raise_on_error* is a boolean that sets
whether in case of an exception
the selector raises that exception
or returns ``False``.
If *selector* is a container, *raise_on_error*
will be used during its items initialization (recursively).
If incorrect arguments are provided,
:exc:`.LenaTypeError` is raised.
"""
Expand All @@ -80,31 +91,38 @@ def __init__(self, selector):
)
elif isinstance(selector, list):
try:
self._selector = _SelectorOr(*selector)
self._selector = _SelectorOr(selector, raise_on_error)
except lena.core.LenaTypeError as err:
raise err
elif isinstance(selector, tuple):
try:
self._selector = _SelectorAnd(*selector)
self._selector = _SelectorAnd(selector, raise_on_error)
except lena.core.LenaTypeError as err:
raise err
else:
raise lena.core.LenaTypeError(
"Selector must be initialized with a callable, list or tuple, "
"{} provided".format(selector)
)
self._raise_on_error = bool(raise_on_error)

def __call__(self, value):
"""Check whether *value* is selected.
If an exception occurs, the result is ``False``.
By default, if an exception occurs, the result is ``False``.
Thus it is safe to use non-existing attributes
or arbitrary contexts.
However, if *raise_on_error* was set to ``True``,
the exception will be raised.
Use it if you are confident in the data
and want to see any error.
"""
try:
sel = self._selector(value)
except Exception: # pylint: disable=broad-except
except Exception as err: # pylint: disable=broad-except
# it can be really any exception: AttributeError, etc.
if self._raise_on_error:
raise err
return False
else:
return sel
Expand All @@ -113,18 +131,29 @@ def __call__(self, value):
class Not(Selector):
"""Negate a selector."""

def __init__(self, selector):
"""*selector* will initialize a :class:`.Selector`."""
self._selector = Selector(selector)
def __init__(self, selector, raise_on_error=False):
"""*selector* is an instance of :class:`.Selector`
or will be used to initialize that.
*raise_on_error* is used during the initialization of
*selector* and has the same meaning as in :class:`.Selector`.
It has no effect if *selector* is already initialized.
"""
if not isinstance(selector, Selector):
selector = Selector(selector, raise_on_error)
self._selector = selector
super(Not, self).__init__(self._selector)

def __call__(self, value):
"""Negate the result of the initialized *selector*.
This is a complete negation (including the case of an error
If *raise_on_error* is ``False``, then this
is a complete negation (including the case of an error
encountered in the *selector*).
For example, if the *selector* is *variable.name*,
and *value*'s context contains no *"variable"*,
*Not("variable.name")(value)* will be ``True``.
If *raise_on_error* is ``True``,
then any occurred exception will be raised here.
"""
return not self._selector(value)
44 changes: 44 additions & 0 deletions tests/flow/test_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,50 @@ def test_selector():
dsel = Selector("name.x")
assert [dsel(val) for val in data] == [True, False]

## And and Or with initialized selectors work.
sel_and = Selector([Selector(len)])
assert [sel_and(dt[0]) for dt in data] == [False]*2
sel_or = Selector((Selector(len),))
assert [sel_or(dt[0]) for dt in data] == [False]*2


def test_raise_on_error():
data = [1, "s", []]
sel1 = Selector(lambda x: len(x))
# no errors is raised
assert list(map(sel1, data)) == [False, True, False]

sel2 = Selector(lambda x: len(x), raise_on_error=True)
assert sel2("s") == 1
# error is raised
with pytest.raises(TypeError):
sel2(1)

## Not works
# raise on error has no effect here
sel3 = Not(sel2, raise_on_error=False)
with pytest.raises(TypeError):
sel3(1)
# raise on error has no effect here
sel4 = Not(sel1, raise_on_error=True)
assert sel4(1) == True

# raise on error works
sel5 = Not(lambda x: len(x), raise_on_error=True)
assert sel5("s") == False
with pytest.raises(TypeError):
sel5(1)

## And and Or raise properly.
# Or works
sel_or = Selector([len], raise_on_error=True)
with pytest.raises(TypeError):
sel_or(1)
# And works
sel_and = Selector((len,), raise_on_error=True)
with pytest.raises(TypeError):
sel_and(1)


def test_not():
# simple type
Expand Down

0 comments on commit 2eeb502

Please sign in to comment.