From ab7a0cb02924f9a96b9d23e64827b40314d2d918 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Sun, 3 May 2020 21:01:59 +0100 Subject: [PATCH] Replace 'checks' with 'check_all' and add 'check_any' For convenience allow either or both of check_all or check_any, instead of just 'checks' when constructing WithMeta objects. --- optionsfactory/_utils.py | 18 ++++- optionsfactory/checks.py | 14 +++- optionsfactory/tests/test_check_utilities.py | 15 ++-- optionsfactory/tests/test_mutableoptions.py | 38 +++++----- optionsfactory/tests/test_options.py | 18 ++--- optionsfactory/tests/test_withmeta.py | 76 +++++++++++++++++-- optionsfactory/withmeta.py | 78 +++++++++++++++----- 7 files changed, 189 insertions(+), 68 deletions(-) diff --git a/optionsfactory/_utils.py b/optionsfactory/_utils.py index c3efca9..2cae699 100644 --- a/optionsfactory/_utils.py +++ b/optionsfactory/_utils.py @@ -16,15 +16,27 @@ def _checked(value, *, meta=None, name=None): f"{'' if name is None else ' for key=' + str(name)}" ) - if meta.checks is not None: - for check in meta.checks: + if meta.check_all is not None: + for check in meta.check_all: if not check(value): raise ValueError( f"The value {value}" f"{'' if name is None else ' of key=' + str(name)} is not " - f"compatible with the checks" + f"compatible with check_all" ) + if meta.check_any is not None: + success = False + for check in meta.check_any: + if check(value): + success = True + if not success: + raise ValueError( + f"The value {value}" + f"{'' if name is None else ' of key=' + str(name)} is not " + f"compatible with check_any" + ) + return value diff --git a/optionsfactory/checks.py b/optionsfactory/checks.py index 5f20987..ae36c4f 100644 --- a/optionsfactory/checks.py +++ b/optionsfactory/checks.py @@ -3,7 +3,10 @@ def is_positive(x): - return x > 0 + try: + return x > 0 + except TypeError: + return False def is_positive_or_None(x): @@ -13,7 +16,10 @@ def is_positive_or_None(x): def is_non_negative(x): - return x >= 0 + try: + return x >= 0 + except TypeError: + return False def is_non_negative_or_None(x): @@ -22,4 +28,8 @@ def is_non_negative_or_None(x): return is_non_negative(x) +def is_None(x): + return x is None + + NoneType = type(None) diff --git a/optionsfactory/tests/test_check_utilities.py b/optionsfactory/tests/test_check_utilities.py index aa925f5..9072c01 100644 --- a/optionsfactory/tests/test_check_utilities.py +++ b/optionsfactory/tests/test_check_utilities.py @@ -1,10 +1,9 @@ -import pytest - from ..checks import ( is_positive, is_positive_or_None, is_non_negative, is_non_negative_or_None, + is_None, ) @@ -13,8 +12,7 @@ def test_is_positive(self): assert is_positive(1) assert not is_positive(-1) assert not is_positive(0.0) - with pytest.raises(TypeError): - is_positive(None) + assert not is_positive(None) def test_is_positive_or_None(self): assert is_positive_or_None(1) @@ -26,11 +24,16 @@ def test_is_non_negative(self): assert is_non_negative(1) assert not is_non_negative(-1) assert is_non_negative(0.0) - with pytest.raises(TypeError): - is_non_negative(None) + assert not is_non_negative(None) def test_is_non_negative_or_None(self): assert is_non_negative_or_None(1) assert not is_non_negative_or_None(-1) assert is_non_negative_or_None(0.0) assert is_non_negative_or_None(None) + + def test_is_None(self): + assert is_None(None) + assert not is_None(3.0) + assert not is_None(-1) + assert not is_None("foo") diff --git a/optionsfactory/tests/test_mutableoptions.py b/optionsfactory/tests/test_mutableoptions.py index b742fa4..8986f1d 100644 --- a/optionsfactory/tests/test_mutableoptions.py +++ b/optionsfactory/tests/test_mutableoptions.py @@ -21,13 +21,13 @@ def test_defaults(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -192,13 +192,13 @@ def test_initialise(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -323,16 +323,14 @@ def test_initialise(self): opts = factory.create({"g": 3.5}) with pytest.raises(ValueError): opts = factory.create({"h": -7}) - with pytest.raises(ValueError): - opts = factory.create({"h": 21}) + assert factory.create({"h": -21}).h == -21 with pytest.raises(TypeError): opts = factory.create({"h": 3.5}) with pytest.raises(ValueError): opts = factory.create({"a": -7}) opts.h - with pytest.raises(ValueError): - opts = factory.create({"a": 21}) - opts.h + opts = factory.create({"a": -23}) + assert opts.h == -21 with pytest.raises(TypeError): opts = factory.create({"a": 3.5}) opts.h @@ -349,13 +347,13 @@ def test_initialise_from_options(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -853,13 +851,13 @@ def test_defaults(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -950,13 +948,13 @@ def test_initialise(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -1056,14 +1054,12 @@ def test_initialise(self): opts = factory.create_immutable({"g": 3.5}) with pytest.raises(ValueError): opts = factory.create_immutable({"h": -7}) - with pytest.raises(ValueError): - opts = factory.create_immutable({"h": 21}) + assert factory.create_immutable({"h": -21}).h == -21 with pytest.raises(TypeError): opts = factory.create_immutable({"h": 3.5}) with pytest.raises(ValueError): opts = factory.create_immutable({"a": -7}) - with pytest.raises(ValueError): - opts = factory.create_immutable({"a": 21}) + assert factory.create_immutable({"a": -23}).h == -21 with pytest.raises(TypeError): opts = factory.create_immutable({"a": 3.5}) @@ -1079,13 +1075,13 @@ def test_initialise_from_options(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) diff --git a/optionsfactory/tests/test_options.py b/optionsfactory/tests/test_options.py index 517e626..929ee5b 100644 --- a/optionsfactory/tests/test_options.py +++ b/optionsfactory/tests/test_options.py @@ -50,13 +50,13 @@ def test_defaults(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -147,13 +147,13 @@ def test_initialise(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) @@ -253,14 +253,12 @@ def test_initialise(self): opts = factory.create({"g": 3.5}) with pytest.raises(ValueError): opts = factory.create({"h": -7}) - with pytest.raises(ValueError): - opts = factory.create({"h": 21}) + assert factory.create({"h": -21}).h == -21 with pytest.raises(TypeError): opts = factory.create({"h": 3.5}) with pytest.raises(ValueError): opts = factory.create({"a": -7}) - with pytest.raises(ValueError): - opts = factory.create({"a": 21}) + assert factory.create({"a": -23}).h == -21 with pytest.raises(TypeError): opts = factory.create({"a": 3.5}) @@ -276,13 +274,13 @@ def test_initialise_from_options(self): 11, doc="option g", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_all=[is_positive, lambda x: x < 20], ), h=WithMeta( lambda options: options.a + 2, doc="option h", value_type=int, - checks=[is_positive, lambda x: x < 20], + check_any=[is_positive, lambda x: x < -20], ), ) diff --git a/optionsfactory/tests/test_withmeta.py b/optionsfactory/tests/test_withmeta.py index 8afe10e..81c6911 100644 --- a/optionsfactory/tests/test_withmeta.py +++ b/optionsfactory/tests/test_withmeta.py @@ -1,7 +1,7 @@ import pytest from ..optionsfactory import WithMeta -from ..checks import NoneType, is_positive, is_positive_or_None +from ..checks import NoneType, is_positive, is_positive_or_None, is_None class TestWithMeta: @@ -76,8 +76,8 @@ def test_allowed_sequence(self): with pytest.raises(ValueError): x.evaluate_expression({}) - def test_checks(self): - x = WithMeta(4.0, checks=is_positive_or_None) + def test_check_all(self): + x = WithMeta(4.0, check_all=is_positive_or_None) assert x.value == 4.0 assert x.evaluate_expression({}) == 4.0 x.value = -2.0 @@ -86,8 +86,8 @@ def test_checks(self): x.value = None assert x.evaluate_expression({}) is None - def test_checks_sequence(self): - x = WithMeta(5.0, checks=[is_positive, lambda x: x < 6.0]) + def test_check_all_sequence(self): + x = WithMeta(5.0, check_all=[is_positive, lambda x: x < 6.0]) assert x.value == 5.0 assert x.evaluate_expression({}) == 5.0 x.value = -3.0 @@ -104,10 +104,10 @@ def test_expression_allowed(self): with pytest.raises(ValueError): x.evaluate_expression({"foo": 1.0}) - def test_expression_checks(self): + def test_expression_check_all(self): x = WithMeta( lambda options: 2.0 + options["foo"], - checks=[is_positive, lambda x: x < 10.0], + check_all=[is_positive, lambda x: x < 10.0], ) assert x.evaluate_expression({"foo": 2.0}) == 4.0 assert x.evaluate_expression({"foo": 4.0}) == 6.0 @@ -115,3 +115,65 @@ def test_expression_checks(self): x.evaluate_expression({"foo": -3.0}) with pytest.raises(ValueError): x.evaluate_expression({"foo": 9.0}) + + def test_check_any(self): + x = WithMeta(4.0, check_any=is_positive_or_None) + assert x.value == 4.0 + assert x.evaluate_expression({}) == 4.0 + x.value = -2.0 + with pytest.raises(ValueError): + x.evaluate_expression({}) + x.value = None + assert x.evaluate_expression({}) is None + + def test_check_any_sequence(self): + x = WithMeta(5.0, check_any=[is_positive, lambda x: x < -6.0]) + assert x.value == 5.0 + assert x.evaluate_expression({}) == 5.0 + x.value = -7.0 + assert x.evaluate_expression({}) == -7.0 + x.value = -3.0 + with pytest.raises(ValueError): + x.evaluate_expression({}) + + def test_expression_check_any(self): + x = WithMeta( + lambda options: 2.0 + options["foo"], + check_any=[is_positive, lambda x: x < -10.0], + ) + assert x.evaluate_expression({"foo": 2.0}) == 4.0 + assert x.evaluate_expression({"foo": -14.0}) == -12.0 + with pytest.raises(ValueError): + x.evaluate_expression({"foo": -3.0}) + with pytest.raises(ValueError): + x.evaluate_expression({"foo": -11.0}) + + def test_combined_check_all_any(self): + x = WithMeta( + 5.0, check_all=is_positive, check_any=[lambda x: x < 10.0, is_None], + ) + assert x.evaluate_expression({}) == 5.0 + x.value = -2.0 + with pytest.raises(ValueError): + x.evaluate_expression({}) + x.value = 10.5 + with pytest.raises(ValueError): + x.evaluate_expression({}) + x.value = None + with pytest.raises(ValueError): + x.evaluate_expression({}) + + def test_combined_check_all_any_expression(self): + x = WithMeta( + lambda options: -1.0 * options["foo"], + check_all=is_positive, + check_any=[lambda x: x < 10.0, is_None], + ) + assert x.evaluate_expression({"foo": -5.0}) == 5.0 + with pytest.raises(ValueError): + x.evaluate_expression({"foo": 2.0}) + with pytest.raises(ValueError): + x.evaluate_expression({"foo": -10.5}) + x.value = None + with pytest.raises(ValueError): + x.evaluate_expression({}) diff --git a/optionsfactory/withmeta.py b/optionsfactory/withmeta.py index 4c6c02e..8184909 100644 --- a/optionsfactory/withmeta.py +++ b/optionsfactory/withmeta.py @@ -8,7 +8,16 @@ class WithMeta: """ - def __init__(self, value, *, doc=None, value_type=None, allowed=None, checks=None): + def __init__( + self, + value, + *, + doc=None, + value_type=None, + allowed=None, + check_all=None, + check_any=None, + ): """ Parameters ---------- @@ -28,27 +37,34 @@ def __init__(self, value, *, doc=None, value_type=None, allowed=None, checks=Non allowed : value or sequence of values, optional When the option is set, it must have one of these values. Cannot be set if 'checks' is given. - checks : expression or sequence of expressions, optional + check_all : expression or sequence of expressions, optional When a value is set for this option, all the expressions must return True when called with that value. - Cannot be set if 'allowed' is given. + Cannot be set if 'allowed' is given, but can be combined with check_any. + check_any : expression or sequence of expressions, optional + When a value is set for this option, at least one of the expressions must + return True when called with that value. + Cannot be set if 'allowed' is given, but can be combined with check_all. """ if isinstance(value, WithMeta): if ( (doc is not None) or (value_type is not None) or (allowed is not None) - or (checks is not None) + or (check_all is not None) + or (check_any is not None) ): raise ValueError( - f"doc={doc}, value_type={value_type}, allowed={allowed}, and " - f"checks={checks} should all be None when value is a WithMeta" + f"doc={doc}, value_type={value_type}, allowed={allowed}, " + f"check_all={check_all}, and check_any={check_any} should all be " + f"None when value is a WithMeta" ) self.value = value.value self.doc = value.doc self.value_type = value.value_type self.allowed = value.allowed - self.checks = value.checks + self.check_all = value.check_all + self.check_any = value.check_any return self.value = value @@ -58,8 +74,15 @@ def __init__(self, value, *, doc=None, value_type=None, allowed=None, checks=Non value_type = tuple(value_type) self.value_type = value_type - if (allowed is not None) and (checks is not None): - raise ValueError("Cannot set both 'allowed' and 'checks'") + if (allowed is not None) and (check_all is not None or check_any is not None): + if check_any is None: + raise ValueError("Cannot set both 'allowed' and 'check_all'") + elif check_all is None: + raise ValueError("Cannot set both 'allowed' and 'check_any'") + else: + raise ValueError( + "Cannot set both 'allowed' and 'check_all' or 'check_any'" + ) if allowed is not None: if (not isinstance(allowed, Sequence)) or isinstance(allowed, str): @@ -69,18 +92,33 @@ def __init__(self, value, *, doc=None, value_type=None, allowed=None, checks=Non else: self.allowed = None - if checks is not None: - if (not isinstance(checks, Sequence)) or isinstance(checks, str): - # make checks expressions a sequence - checks = (checks,) - self.checks = tuple(checks) - for check in self.checks: + if check_all is not None: + if (not isinstance(check_all, Sequence)) or isinstance(check_all, str): + # make check_all expressions a sequence + check_all = (check_all,) + self.check_all = tuple(check_all) + for check in self.check_all: + if not callable(check): + raise ValueError( + f"{check} is not callable, but was passed as a check to " + f"check_all" + ) + else: + self.check_all = None + + if check_any is not None: + if (not isinstance(check_any, Sequence)) or isinstance(check_any, str): + # make check_any expressions a sequence + check_any = (check_any,) + self.check_any = tuple(check_any) + for check in self.check_any: if not callable(check): raise ValueError( - f"{check} is not callable, but was passed as a check" + f"{check} is not callable, but was passed as a check to " + f"check_any" ) else: - self.checks = None + self.check_any = None def __eq__(self, other): if not isinstance(other, WithMeta): @@ -89,13 +127,15 @@ def __eq__(self, other): self.value == other.value and self.doc == other.doc and self.allowed == other.allowed - and self.checks == other.checks + and self.check_all == other.check_all + and self.check_any == other.check_any ) def __str__(self): return ( f"WithMeta({self.value}, doc={self.doc}, value_type={self.value_type}), " - f"allowed={self.allowed}, checks={self.checks})" + f"allowed={self.allowed}, check_all={self.check_all}, " + f"check_any={self.check_any})" ) def evaluate_expression(self, options, *, name=None):