Skip to content

Commit bb62b3d

Browse files
drmullChris Mildebrandt
authored and
Chris Mildebrandt
committed
Handle position with a custom class instead of string (#60)
* Handle position with a custom class instead of string Add datapath class to replace the 'dotted string' used to indicate the path in the data/schema - Fixes issue with nested maps in an include - Allows top level validators other than fixed map * Fix pep8 formating and avoid mutable-default-arguments * Add strict mode and include validators - Strict mode will give errors if there are additional elements present in the data that are not covered by the schema. - The include mechanism is made a bit more flexible and now allows arbitrary validators to be included, not just maps. * Fix issues with strict mode commit - Update count_exception_line to use assert. - Fix is_optional on map/list crash. - Add --strict flag to command line.
1 parent 8ff569a commit bb62b3d

31 files changed

+333
-191
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ person:
184184
##### Adding external includes
185185
After you construct a schema you can add extra, external include definitions by calling `schema.add_include(dict)`. This method takes a dictionary and adds each key as another include.
186186

187+
### Strict mode
188+
By default Yamale will not give any error for extra elements present in lists and maps that are not covered by the schema. With strict mode any additional element will give an error. Strict mode is enabled by passing the strict=True flag to the validate function.
189+
190+
It is possible to mix strict and non-strict mode by setting the strict=True/False flag in the include validator, setting the option only for the included validators.
191+
187192
Validators
188193
----------
189194
Here are all the validators Yamale knows about. Every validator takes a `required` keyword telling Yamale whether or not that node must exist. By default every node is required. Example: `str(required=False)`

yamale/command_line.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
schemas = {}
2020

2121

22-
def _validate(schema_path, data_path, parser):
22+
def _validate(schema_path, data_path, parser, strict):
2323
schema = schemas.get(schema_path)
2424
try:
2525
if not schema:
2626
schema = yamale.make_schema(schema_path, parser)
2727
schemas[schema_path] = schema
2828
data = yamale.make_data(data_path, parser)
29-
yamale.validate(schema, data)
29+
yamale.validate(schema, data, strict)
3030
except Exception as e:
3131
error = '\nError!\n'
3232
error += 'Schema: %s\n' % schema_path
@@ -60,15 +60,15 @@ def _find_schema(data_path, schema_name):
6060
return _find_data_path_schema(data_path, schema_name)
6161

6262

63-
def _validate_single(yaml_path, schema_name, parser):
63+
def _validate_single(yaml_path, schema_name, parser, strict):
6464
print('Validating %s...' % yaml_path)
6565
s = _find_schema(yaml_path, schema_name)
6666
if not s:
6767
raise ValueError("Invalid schema name for '{}' or schema not found.".format(schema_name))
68-
_validate(s, yaml_path, parser)
68+
_validate(s, yaml_path, parser, strict)
6969

7070

71-
def _validate_dir(root, schema_name, cpus, parser):
71+
def _validate_dir(root, schema_name, cpus, parser, strict):
7272
pool = Pool(processes=cpus)
7373
res = []
7474
print('Finding yaml files...')
@@ -78,7 +78,8 @@ def _validate_dir(root, schema_name, cpus, parser):
7878
d = os.path.join(root, f)
7979
s = _find_schema(d, schema_name)
8080
if s:
81-
res.append(pool.apply_async(_validate, (s, d, parser)))
81+
res.append(pool.apply_async(_validate,
82+
(s, d, parser, strict)))
8283
else:
8384
print('No schema found for: %s' % d)
8485

@@ -90,12 +91,12 @@ def _validate_dir(root, schema_name, cpus, parser):
9091
pool.join()
9192

9293

93-
def _router(root, schema_name, cpus, parser):
94+
def _router(root, schema_name, cpus, parser, strict=False):
9495
root = os.path.abspath(root)
9596
if os.path.isfile(root):
96-
_validate_single(root, schema_name, parser)
97+
_validate_single(root, schema_name, parser, strict)
9798
else:
98-
_validate_dir(root, schema_name, cpus, parser)
99+
_validate_dir(root, schema_name, cpus, parser, strict)
99100

100101

101102
def main():
@@ -108,8 +109,10 @@ def main():
108109
help='number of CPUs to use. Default is 4.')
109110
parser.add_argument('-p', '--parser', default='pyyaml',
110111
help='YAML library to load files. Choices are "ruamel" or "pyyaml" (default).')
112+
parser.add_argument('--strict', action='store_true',
113+
help='Enable strict mode, unexpected elements in the data will not be accepted.')
111114
args = parser.parse_args()
112-
_router(args.path, args.schema, args.cpu_num, args.parser)
115+
_router(args.path, args.schema, args.cpu_num, args.parser, args.strict)
113116
print('Validation success! 👍')
114117

115118

yamale/schema/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
from .schema import Schema
2-
from .data import Data

yamale/schema/data.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

yamale/schema/datapath.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class DataPath(object):
2+
3+
def __init__(self, *path):
4+
self._path = path
5+
6+
def __add__(self, other):
7+
dp = DataPath()
8+
dp._path = self._path + other._path
9+
return dp
10+
11+
def __str__(self):
12+
return '.'.join(map(str, (self._path)))
13+
14+
def __repr__(self):
15+
return 'DataPath({})'.format(repr(self._path))

yamale/schema/schema.py

Lines changed: 100 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import sys
2-
2+
from .datapath import DataPath
33
from .. import syntax, util
44
from .. import validators as val
5-
from yamale.util import YAMALE_SEP
65

76
# Fix Python 2.x.
87
PY2 = sys.version_info[0] == 2
@@ -13,123 +12,149 @@ class Schema(object):
1312
Makes a Schema object from a schema dict.
1413
Still acts like a dict.
1514
"""
16-
def __init__(self, schema_dict, name='', validators=None):
15+
def __init__(self, schema_dict, name='', validators=None, includes=None):
1716
self.validators = validators or val.DefaultValidators
1817
self.dict = schema_dict
1918
self.name = name
20-
self._schema = self._process_schema(schema_dict, self.validators)
21-
self.includes = {}
19+
self._schema = self._process_schema(DataPath(),
20+
schema_dict,
21+
self.validators)
22+
# if this schema is included it shares the includes with the top level
23+
# schema
24+
self.includes = {} if includes is None else includes
2225

2326
def add_include(self, type_dict):
2427
for include_name, custom_type in type_dict.items():
2528
t = Schema(custom_type, name=include_name,
26-
validators=self.validators)
29+
validators=self.validators, includes=self.includes)
2730
self.includes[include_name] = t
2831

29-
def __getitem__(self, key):
30-
return self._schema[key]
31-
32-
def _process_schema(self, schema_dict, validators):
32+
def _process_schema(self, path, schema_data, validators):
3333
"""
3434
Go through a schema and construct validators.
3535
"""
36-
schema_flat = util.flatten(schema_dict)
37-
38-
for key, expression in schema_flat.items():
39-
try:
40-
schema_flat[key] = syntax.parse(expression, validators)
41-
except SyntaxError as e:
42-
# Tack on some more context and rethrow.
43-
error = str(e) + ' at node \'%s\'' % key
44-
raise SyntaxError(error)
45-
return schema_flat
46-
47-
def validate(self, data):
48-
errors = []
49-
50-
for key, validator in self._schema.items():
51-
errors += self._validate(validator, data, key=key, includes=self.includes)
36+
if util.is_map(schema_data) or util.is_list(schema_data):
37+
for key, data in util.get_iter(schema_data):
38+
schema_data[key] = self._process_schema(path + DataPath(key),
39+
data,
40+
validators)
41+
else:
42+
schema_data = self._parse_schema_item(path,
43+
schema_data,
44+
validators)
45+
return schema_data
46+
47+
def _parse_schema_item(self, path, expression, validators):
48+
try:
49+
return syntax.parse(expression, validators)
50+
except SyntaxError as e:
51+
# Tack on some more context and rethrow.
52+
error = str(e) + ' at node \'%s\'' % str(path)
53+
raise SyntaxError(error)
54+
55+
def validate(self, data, data_name, strict):
56+
path = DataPath()
57+
errors = self._validate(self._schema, data, path, strict)
5258

5359
if errors:
54-
header = '\nError validating data %s with schema %s' % (data.name, self.name)
60+
header = '\nError validating data %s with schema %s' % (data_name,
61+
self.name)
5562
error_str = header + '\n\t' + '\n\t'.join(errors)
5663
if PY2:
5764
error_str = error_str.encode('utf-8')
5865
raise ValueError(error_str)
5966

60-
def _validate(self, validator, data, key, position=None, includes=None):
67+
def _validate_item(self, validator, data, path, strict, key):
6168
"""
62-
Run through a schema and a data structure,
63-
validating along the way.
64-
65-
Ignores fields that are in the data structure, but not in the schema.
69+
Fetch item from data at the postion key and validate with validator.
6670
6771
Returns an array of errors.
6872
"""
6973
errors = []
70-
71-
if position:
72-
position = '%s%s%s' % (position, util.YAMALE_SEP, key)
73-
else:
74-
position = key
75-
74+
path = path + DataPath(key)
7675
try: # Pull value out of data. Data can be a map or a list/sequence
77-
data_item = util.get_value(data, key)
76+
data_item = data[key]
7877
except KeyError: # Oops, that field didn't exist.
79-
if validator.is_optional: # Optional? Who cares.
78+
# Optional? Who cares.
79+
if isinstance(validator, val.Validator) and validator.is_optional:
8080
return errors
8181
# SHUT DOWN EVERTYHING
82-
errors.append('%s: Required field missing' % position.replace(util.YAMALE_SEP, '.'))
82+
errors.append('%s: Required field missing' % path)
8383
return errors
8484

85-
return self._validate_item(validator, data_item, position, includes)
85+
return self._validate(validator, data_item, path, strict)
8686

87-
def _validate_item(self, validator, data_item, position, includes):
87+
def _validate(self, validator, data, path, strict):
8888
"""
89-
Validates a single data item against validator.
89+
Validate data with validator.
90+
Special handling of non-primitive validators.
9091
9192
Returns an array of errors.
9293
"""
93-
errors = []
9494

95+
if util.is_list(validator) or util.is_map(validator):
96+
return self._validate_static_map_list(validator,
97+
data,
98+
path,
99+
strict)
100+
101+
errors = []
95102
# Optional field with optional value? Who cares.
96-
if data_item is None and validator.is_optional and validator.can_be_none:
103+
if (data is None and
104+
validator.is_optional and
105+
validator.can_be_none):
97106
return errors
98107

99-
errors += self._validate_primitive(validator, data_item, position)
108+
errors += self._validate_primitive(validator, data, path)
100109

101110
if errors:
102111
return errors
103112

104113
if isinstance(validator, val.Include):
105-
errors += self._validate_include(validator, data_item,
106-
includes, position)
114+
errors += self._validate_include(validator, data, path, strict)
107115

108116
elif isinstance(validator, (val.Map, val.List)):
109-
errors += self._validate_map_list(validator, data_item,
110-
includes, position)
117+
errors += self._validate_map_list(validator, data, path, strict)
111118

112119
elif isinstance(validator, val.Any):
113-
errors += self._validate_any(validator, data_item,
114-
includes, position)
120+
errors += self._validate_any(validator, data, path, strict)
115121

116122
return errors
117123

118-
def _validate_map_list(self, validator, data, includes, pos):
124+
def _validate_static_map_list(self, validator, data, path, strict):
125+
if util.is_map(validator) and not util.is_map(data):
126+
return ["%s : '%s' is not a map" % (path, data)]
127+
128+
if util.is_list(validator) and not util.is_list(data):
129+
return ["%s : '%s' is not a list" % (path, data)]
130+
131+
errors = []
132+
133+
if strict:
134+
data_keys = set(util.get_keys(data))
135+
validator_keys = set(util.get_keys(validator))
136+
for key in data_keys - validator_keys:
137+
error_path = path + DataPath(key)
138+
errors += ['%s: Unexpected element' % error_path]
139+
140+
for key, sub_validator in util.get_iter(validator):
141+
errors += self._validate_item(sub_validator,
142+
data,
143+
path,
144+
strict,
145+
key)
146+
return errors
147+
148+
def _validate_map_list(self, validator, data, path, strict):
119149
errors = []
120150

121151
if not validator.validators:
122152
return errors # No validators, user just wanted a map.
123153

124-
if isinstance(validator, val.List):
125-
keys = range(len(data))
126-
else:
127-
keys = data.keys()
128-
129-
for key in keys:
154+
for key in util.get_keys(data):
130155
sub_errors = []
131156
for v in validator.validators:
132-
err = self._validate(v, data, key, pos, includes)
157+
err = self._validate_item(v, data, path, strict, key)
133158
if err:
134159
sub_errors.append(err)
135160

@@ -140,21 +165,18 @@ def _validate_map_list(self, validator, data, includes, pos):
140165

141166
return errors
142167

143-
def _validate_include(self, validator, data, includes, pos):
144-
errors = []
145-
146-
include_schema = includes.get(validator.include_name)
168+
def _validate_include(self, validator, data, path, strict):
169+
include_schema = self.includes.get(validator.include_name)
147170
if not include_schema:
148-
errors.append('Include \'%s\' has not been defined.' % validator.include_name)
149-
return errors
150-
151-
for key, validator in include_schema._schema.items():
152-
errors += include_schema._validate(
153-
validator, data, includes=includes, key=key, position=pos)
154-
155-
return errors
156-
157-
def _validate_any(self, validator, data, includes, pos):
171+
return [('Include \'%s\' has not been defined.'
172+
% validator.include_name)]
173+
strict = strict if validator.strict is None else validator.strict
174+
return include_schema._validate(include_schema._schema,
175+
data,
176+
path,
177+
strict)
178+
179+
def _validate_any(self, validator, data, path, strict):
158180
errors = []
159181

160182
if not validator.validators:
@@ -163,7 +185,7 @@ def _validate_any(self, validator, data, includes, pos):
163185

164186
sub_errors = []
165187
for v in validator.validators:
166-
err = self._validate_item(v, data, pos, includes)
188+
err = self._validate(v, data, path, strict)
167189
if err:
168190
sub_errors.append(err)
169191

@@ -174,10 +196,10 @@ def _validate_any(self, validator, data, includes, pos):
174196

175197
return errors
176198

177-
def _validate_primitive(self, validator, data, pos):
199+
def _validate_primitive(self, validator, data, path):
178200
errors = validator.validate(data)
179201

180202
for i, error in enumerate(errors):
181-
errors[i] = '%s: ' % pos.replace(YAMALE_SEP, '.') + error
203+
errors[i] = ('%s: ' % path) + error
182204

183205
return errors
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
map:
2+
key: value
3+
key2: value2

0 commit comments

Comments
 (0)