From b32ad1d238d25a5031eb3035572abd48bbfd4677 Mon Sep 17 00:00:00 2001 From: Richard Shaw Date: Sun, 16 Feb 2020 16:15:44 -0800 Subject: [PATCH] feat(config): add a list Property for validating input lists This property can validate the type and size (exact and maximum) of an incoming list. --- caput/config.py | 69 ++++++++++++++++++++++++++++++++++++++ caput/tests/test_config.py | 31 +++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/caput/config.py b/caput/config.py index 302395da..7b1e6ef1 100644 --- a/caput/config.py +++ b/caput/config.py @@ -328,6 +328,75 @@ def _prop(val): return prop +def list_type(type_=None, length=None, maxlength=None, default=None): + """A property type that validates lists against required properties. + + Parameters + ---------- + type_ : type, optional + Type to apply. If `None` does not attempt to validate elements against type. + length : int, optional + Exact length of the list we expect. If `None` (default) accept any length. + maxlength : int, optional + Maximum length of the list. If `None` (default) there is no maximum length. + default : optional + The optional default value. + + Returns + ------- + prop : Property + A property instance setup to validate the type. + + Examples + -------- + Should be used like:: + + class Project(object): + + mode = list_type(int, length=2, default=[3, 4]) + """ + + def _prop(val): + + if not isinstance(val, (list, tuple)): + raise ValueError("Expected to receive a list, but got '%s.'" % repr(val)) + + if type_: + for ii, item in enumerate(val): + if not isinstance(item, type_): + raise ValueError( + "Expected to receive a list with items of type %s, but got " + "'%s.' at position %i" % (type_, val, ii) + ) + + if length and len(val) != length: + raise ValueError( + "List expected to be of length %i, but was actually length %i" + % (length, len(val)) + ) + + if maxlength and len(val) > maxlength: + raise ValueError( + "Maximum length of list is %i is, but list was actually length %i" + % (maxlength, len(val)) + ) + + return val + + if default: + try: + _prop(default) + except ValueError as e: + raise ValueError( + "Default value %s does not satisfy property requirements: %s" + % (default, repr(e)) + ) + + prop = Property(proptype=_prop, default=default) + + return prop + + if __name__ == "__main__": import doctest diff --git a/caput/tests/test_config.py b/caput/tests/test_config.py index 2b513f1e..97d66c5b 100644 --- a/caput/tests/test_config.py +++ b/caput/tests/test_config.py @@ -22,6 +22,12 @@ class PersonWithPet(Person): petage = 36 +class ListTypeTests(Person): + list_max_length = config.list_type(maxlength=2) + list_exact_length = config.list_type(length=2) + list_type = config.list_type(type_=int) + + class TestConfig(unittest.TestCase): testdict = {"name": "Richard", "ageinyears": 40, "petname": "Sooty"} @@ -67,3 +73,28 @@ def test_pickle(self): self.assertEqual(person2.name, "Richard") self.assertEqual(person2.age, 40.0) self.assertEqual(person2.petname, "Sooty") + + def test_list_type(self): + + lt = ListTypeTests() + + with self.assertRaises(ValueError): + lt.read_config({"list_max_length": [1, 3, 4]}) + + # Should work fine + lt = ListTypeTests() + lt.read_config({"list_max_length": [1, 2]}) + + with self.assertRaises(ValueError): + lt.read_config({"list_exact_length": [3]}) + + # Work should fine + lt = ListTypeTests() + lt.read_config({"list_exact_length": [1, 2]}) + + with self.assertRaises(ValueError): + lt.read_config({"list_type": ["hello"]}) + + # Work should fine + lt = ListTypeTests() + lt.read_config({"list_type": [1, 2]})