From fc9fb6963e4e89fdef6df878124e2506523a6f7e Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 18 Nov 2022 14:55:04 +0000 Subject: [PATCH 1/4] Tests for float conversion --- optionsfactory/tests/test_mutableoptions.py | 46 +++++-- optionsfactory/tests/test_options.py | 129 +++++++++++++++++++- optionsfactory/tests/test_withmeta.py | 9 +- 3 files changed, 173 insertions(+), 11 deletions(-) diff --git a/optionsfactory/tests/test_mutableoptions.py b/optionsfactory/tests/test_mutableoptions.py index 5531f62..da19859 100644 --- a/optionsfactory/tests/test_mutableoptions.py +++ b/optionsfactory/tests/test_mutableoptions.py @@ -141,6 +141,37 @@ def test_defaults(self): with pytest.raises(KeyError): opts.is_default("x") + opts["f"] = 3 + + assert opts.a == 3 + assert opts.b == 3 + assert opts.c == 3 + assert opts.d == 6.0 + assert opts.e == 3 + assert opts.f == 3.0 + assert opts.g == 11 + assert opts.h == 5 + + assert opts["a"] == 3 + assert opts["b"] == 3 + assert opts["c"] == 3 + assert opts["d"] == 6.0 + assert opts["e"] == 3 + assert opts["f"] == 3.0 + assert opts["g"] == 11 + assert opts["h"] == 5 + + assert not opts.is_default("a") + assert opts.is_default("b") + assert opts.is_default("c") + assert opts.is_default("d") + assert opts.is_default("e") + assert not opts.is_default("f") + assert opts.is_default("g") + assert opts.is_default("h") + with pytest.raises(KeyError): + opts.is_default("x") + assert "a" in opts assert "b" in opts assert "c" in opts @@ -155,22 +186,23 @@ def test_defaults(self): assert sorted([k for k in opts]) == sorted( ["a", "b", "c", "d", "e", "f", "g", "h"] ) - assert sorted(opts.values()) == sorted([3, 3, 3, 5.0, 3, 2.0, 11, 5]) + assert sorted(opts.values()) == sorted([3, 3, 3, 6.0, 3, 3.0, 11, 5]) assert sorted(opts.items()) == sorted( [ ("a", 3), ("b", 3), ("c", 3), - ("d", 5.0), + ("d", 6.0), ("e", 3), - ("f", 2.0), + ("f", 3.0), ("g", 11), ("h", 5), ] ) - # Reset "a" to default + # Reset "a" and "f" to default del opts["a"] + del opts["f"] assert opts.a == 1 assert opts.b == 1 assert opts.c == 1 @@ -322,8 +354,7 @@ def test_initialise(self): opts = factory.create({"f": 2.5}) with pytest.raises(TypeError): opts = factory.create({"f": "2.0"}) - with pytest.raises(TypeError): - opts = factory.create({"f": 2}) + assert factory.create({"f": 2}).f == 2.0 with pytest.raises(ValueError): opts = factory.create({"g": -1}) with pytest.raises(ValueError): @@ -1054,8 +1085,7 @@ def test_initialise(self): opts = factory.create_immutable({"f": 2.5}) with pytest.raises(TypeError): opts = factory.create_immutable({"f": "2.0"}) - with pytest.raises(TypeError): - opts = factory.create_immutable({"f": 2}) + assert factory.create_immutable({"f": 2}).f == 2.0 with pytest.raises(ValueError): opts = factory.create_immutable({"g": -1}) with pytest.raises(ValueError): diff --git a/optionsfactory/tests/test_options.py b/optionsfactory/tests/test_options.py index 6303957..83c6246 100644 --- a/optionsfactory/tests/test_options.py +++ b/optionsfactory/tests/test_options.py @@ -252,8 +252,135 @@ def test_initialise(self): opts = factory.create({"f": 2.5}) with pytest.raises(TypeError): opts = factory.create({"f": "2.0"}) + assert factory.create({"f": 2}).f == 2.0 + with pytest.raises(ValueError): + opts = factory.create({"g": -1}) + with pytest.raises(ValueError): + opts = factory.create({"g": 30}) + with pytest.raises(TypeError): + opts = factory.create({"g": 3.5}) + with pytest.raises(ValueError): + opts = factory.create({"h": -7}) + 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}) + assert factory.create({"a": -23}).h == -21 + with pytest.raises(TypeError): + opts = factory.create({"a": 3.5}) + + def test_initialise_with_conversion_to_float(self): + factory = OptionsFactory( + a=1, + b=lambda options: options.a, + c=lambda options: options["a"], + d=lambda options: options.b + options.c, + e=WithMeta("b", value_type=int), + f=WithMeta(2.0, doc="option f", value_type=float, allowed=[2.0, 3.0]), + g=WithMeta( + 11, + doc="option g", + value_type=int, + check_all=[is_positive, lambda x: x < 20], + ), + h=WithMeta( + lambda options: options.a + 2, + doc="option h", + value_type=int, + check_any=[is_positive, lambda x: x < -20], + ), + ) + + opts = factory.create({"a": 4, "b": 5, "f": 3, "g": 13, "z": 17}) + + assert opts.a == 4 + assert opts.b == 5 + assert opts.c == 4 + assert opts.d == 9 + assert opts.e == 5 + assert opts.f == 3.0 + assert isinstance(opts.f, float) + assert opts.g == 13 + assert opts.h == 6 + + # "z" should have been ignored + with pytest.raises(AttributeError): + opts.z + + assert opts["a"] == 4 + assert opts["b"] == 5 + assert opts["c"] == 4 + assert opts["d"] == 9 + assert opts["e"] == 5 + assert opts["f"] == 3.0 + assert isinstance(opts["f"], float) + assert opts["g"] == 13 + assert opts["h"] == 6 + + # "z" should have been ignored + with pytest.raises(KeyError): + opts["z"] + + assert opts.doc["a"] is None + assert opts.doc["b"] is None + assert opts.doc["c"] is None + assert opts.doc["d"] is None + assert opts.doc["e"] is None + assert opts.doc["f"] == "option f" + assert opts.doc["g"] == "option g" + assert opts.doc["h"] == "option h" + + with pytest.raises(TypeError): + opts.a = 2 + with pytest.raises(TypeError): - opts = factory.create({"f": 2}) + opts["a"] = 2 + + assert not opts.is_default("a") + assert not opts.is_default("b") + assert opts.is_default("c") + assert opts.is_default("d") + assert opts.is_default("e") + assert not opts.is_default("f") + assert not opts.is_default("g") + assert opts.is_default("h") + with pytest.raises(KeyError): + opts.is_default("x") + + assert "a" in opts + assert "b" in opts + assert "c" in opts + assert "d" in opts + assert "e" in opts + assert "f" in opts + assert "g" in opts + assert "h" in opts + assert not ("x" in opts) + + assert len(opts) == 8 + assert sorted([k for k in opts]) == sorted( + ["a", "b", "c", "d", "e", "f", "g", "h"] + ) + assert sorted(opts.values()) == sorted([4, 5, 4, 9, 5, 3.0, 13, 6]) + assert sorted(opts.items()) == sorted( + [ + ("a", 4), + ("b", 5), + ("c", 4), + ("d", 9), + ("e", 5), + ("f", 3.0), + ("g", 13), + ("h", 6), + ] + ) + + with pytest.raises(ValueError): + opts = factory.create({"f": 2.5}) + with pytest.raises(TypeError): + opts = factory.create({"f": "2.0"}) + assert factory.create({"f": 2}).f == 2.0 with pytest.raises(ValueError): opts = factory.create({"g": -1}) with pytest.raises(ValueError): diff --git a/optionsfactory/tests/test_withmeta.py b/optionsfactory/tests/test_withmeta.py index a600a62..5f00e22 100644 --- a/optionsfactory/tests/test_withmeta.py +++ b/optionsfactory/tests/test_withmeta.py @@ -43,8 +43,13 @@ def test_value_type(self): assert x.evaluate_expression({}) == 3.0 x.value = 3 - with pytest.raises(TypeError): - x.evaluate_expression({}) + assert x.evaluate_expression({}) == 3.0 + + def test_float_conversion(self): + x = WithMeta(3, value_type=float) + assert x.value_type is float + assert x.evaluate_expression({}) == 3.0 + assert isinstance(x.evaluate_expression({}), float) def test_value_type_sequence(self): x = WithMeta(3.0, value_type=[float, NoneType]) From 7bb8144ed2b67c75d0e666ae8ea524c19ffd6845 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 18 Nov 2022 14:55:16 +0000 Subject: [PATCH 2/4] Allow converting any 'Number' to float When value_type=float, it is convenient to be able to pass other number types (especially int) as the value. --- optionsfactory/_utils.py | 6 ++++++ optionsfactory/withmeta.py | 7 ++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/optionsfactory/_utils.py b/optionsfactory/_utils.py index 2cae699..28dc77d 100644 --- a/optionsfactory/_utils.py +++ b/optionsfactory/_utils.py @@ -1,4 +1,10 @@ +from numbers import Number + + def _checked(value, *, meta=None, name=None): + if (meta is not None) and meta.value_type is float and isinstance(value, Number): + # Allow converting any numerical type to float + value = float(value) if ( (meta is not None) and (meta.value_type is not None) diff --git a/optionsfactory/withmeta.py b/optionsfactory/withmeta.py index 1330995..65e59c9 100644 --- a/optionsfactory/withmeta.py +++ b/optionsfactory/withmeta.py @@ -1,4 +1,5 @@ from collections.abc import Sequence +from numbers import Number from ._utils import _checked @@ -65,7 +66,11 @@ def __init__( self.check_any = value.check_any return - self.value = value + if value_type is float and isinstance(value, Number): + # Allow converting any numerical type to float + self.value = float(value) + else: + self.value = value self.doc = doc if isinstance(value_type, Sequence): From 75c76da06d8e820e955311b514ed4d1b2525a03b Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 18 Nov 2022 15:20:33 +0000 Subject: [PATCH 3/4] Fix flake8 config From version 5.0.0, brackets around lists are not allowed. --- .flake8 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.flake8 b/.flake8 index f6a04ca..1347fb2 100644 --- a/.flake8 +++ b/.flake8 @@ -1,11 +1,10 @@ [flake8] max-line-length = 88 -ignore = ( +ignore = E741, # 'ambiguous variable names' forbids using 'I', 'O' or 'l' W503, # 'line break before binary operator', but this is allowed and useful inside brackets E203, # 'whitespace before ':'', but black formats some slice expressions with space before ':' E231, # missing whitespace after ',', but black formats some expressions without space after ',' -) exclude = optionsfactory/_version.py versioneer.py From a1121d68a4ffacd284bc067006ebc0a5d8fa7b92 Mon Sep 17 00:00:00 2001 From: John Omotani Date: Fri, 18 Nov 2022 15:21:33 +0000 Subject: [PATCH 4/4] flake8 fix --- optionsfactory/optionsfactory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optionsfactory/optionsfactory.py b/optionsfactory/optionsfactory.py index 98df319..88cb092 100644 --- a/optionsfactory/optionsfactory.py +++ b/optionsfactory/optionsfactory.py @@ -88,8 +88,8 @@ def doc(self): return {key: value.doc for key, value in self.__defaults.items()} def add(self, **kwargs): - """Create a more specific version of the factory with extra options. For example, - may be useful for a subclass like + """Create a more specific version of the factory with extra options. For + example, may be useful for a subclass like class Parent: options_factory = OptionsFactory(...)