From 786186f11919332896a7b8f32b49e2cbd0ccf500 Mon Sep 17 00:00:00 2001 From: Dan Tao Date: Tue, 7 Nov 2017 09:52:52 -0600 Subject: [PATCH 1/2] add Schema.infer method This introduces the class method Schema.infer, to infer a Schema from concrete data. This will be useful for converting existing known-good data (e.g. API responses) into enforceable schemas. --- voluptuous/schema_builder.py | 42 +++++++++++++++++++++ voluptuous/tests/tests.py | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 81940dc..332ee73 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -197,6 +197,48 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(schema) + @classmethod + def infer(cls, data, **kwargs): + """Create a Schema from concrete data (e.g. an API response). + + For example, this will take a dict like: + + { + 'foo': 1, + 'bar': { + 'a': True, + 'b': False + }, + 'baz': ['purple', 'monkey', 'dishwasher'] + } + + And return a Schema: + + { + 'foo': int, + 'bar': { + 'a': bool, + 'b': bool + }, + 'baz': [str] + } + """ + def value_to_schema_type(value): + if isinstance(value, dict): + if len(value) == 0: + return dict + return {k: value_to_schema_type(v) + for k, v in iteritems(value)} + if isinstance(value, list): + if len(value) == 0: + return list + else: + return [value_to_schema_type(v) + for v in value] + return type(value) + + return cls(value_to_schema_type(data), **kwargs) + def __eq__(self, other): if str(other) == str(self.schema): # Because repr is combination mixture of object and schema diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 351bb8c..cc0ed61 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -828,6 +828,79 @@ def test_marker_hashable(): assert_equal(definition.get('j'), None) +def test_schema_infer(): + schema = Schema.infer({ + 'str': 'foo', + 'bool': True, + 'int': 42, + 'float': 3.14 + }) + assert_equal(schema, Schema({ + Required('str'): str, + Required('bool'): bool, + Required('int'): int, + Required('float'): float + })) + + +def test_schema_infer_dict(): + schema = Schema.infer({ + 'a': { + 'b': { + 'c': 'foo' + } + } + }) + + assert_equal(schema, Schema({ + Required('a'): { + Required('b'): { + Required('c'): str + } + } + })) + + +def test_schema_infer_list(): + schema = Schema.infer({ + 'list': ['foo', True, 42, 3.14] + }) + + assert_equal(schema, Schema({ + Required('list'): [str, bool, int, float] + })) + + +def test_schema_infer_scalar(): + assert_equal(Schema.infer('foo'), Schema(str)) + assert_equal(Schema.infer(True), Schema(bool)) + assert_equal(Schema.infer(42), Schema(int)) + assert_equal(Schema.infer(3.14), Schema(float)) + assert_equal(Schema.infer({}), Schema(dict)) + assert_equal(Schema.infer([]), Schema(list)) + + +def test_schema_infer_accepts_kwargs(): + schema = Schema.infer({ + 'str': 'foo', + 'bool': True + }, required=False, extra=True) + + # Subset of schema should be acceptable thanks to required=False. + schema({'bool': False}) + + # Keys that are in schema should still match required types. + try: + schema({'str': 42}) + except Invalid: + pass + else: + assert False, 'Did not raise Invalid for Number' + + # Extra fields should be acceptable thanks to extra=True. + schema({'str': 'bar', 'int': 42}) + + def test_validation_performance(): """ This test comes to make sure the validation complexity of dictionaries is done in a linear time. From b710aa0240180f281da7b4d4f1b36ec1462abf6f Mon Sep 17 00:00:00 2001 From: Dan Tao Date: Fri, 1 Dec 2017 16:05:26 -0600 Subject: [PATCH 2/2] update docstring to indicate that Schema.infer only support basic inference --- voluptuous/schema_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 332ee73..f4244c8 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -222,6 +222,8 @@ def infer(cls, data, **kwargs): }, 'baz': [str] } + + Note: only very basic inference is supported. """ def value_to_schema_type(value): if isinstance(value, dict):