Skip to content

Commit 4c00b05

Browse files
authored
Merge pull request #57 from cwacek/feature/pattern-properties
feature: Adds support for patternProperties
2 parents 03be156 + 4e6c26e commit 4c00b05

File tree

4 files changed

+208
-53
lines changed

4 files changed

+208
-53
lines changed

python_jsonschema_objects/classbuilder.py

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import python_jsonschema_objects.util as util
22
import python_jsonschema_objects.validators as validators
3+
import python_jsonschema_objects.pattern_properties as pattern_properties
34

45
import collections
56
import itertools
@@ -9,6 +10,8 @@
910
import logging
1011
logger = logging.getLogger(__name__)
1112

13+
logger.addHandler(logging.NullHandler())
14+
1215

1316

1417
# Long is no longer a thing in python3.x
@@ -34,16 +37,6 @@ class ProtocolBase(collections.MutableMapping):
3437
__propinfo__ = {}
3538
__required__ = set()
3639

37-
__SCHEMA_TYPES__ = {
38-
'array': list,
39-
'boolean': bool,
40-
'integer': int,
41-
'number': (float, int, long),
42-
'null': type(None),
43-
'string': six.string_types,
44-
'object': dict
45-
}
46-
4740
def as_dict(self):
4841
""" Return a dictionary containing the current values
4942
of the object.
@@ -182,22 +175,12 @@ def __setattr__(self, name, val):
182175
prop.fset(self, val)
183176
else:
184177
# This is an additional property of some kind
185-
typ = getattr(self, '__extensible__', None)
186-
if typ is False:
178+
try:
179+
val = self.__extensible__.instantiate(name, val)
180+
except Exception as e:
187181
raise validators.ValidationError(
188-
"Attempted to set unknown property '{0}', "
189-
"but 'additionalProperties' is false.".format(name))
190-
if typ is True:
191-
# There is no type defined, so just make it a basic literal
192-
# Pick the type based on the type of the values
193-
valtype = [k for k, t in six.iteritems(self.__SCHEMA_TYPES__)
194-
if t is not None and isinstance(val, t)]
195-
valtype = valtype[0]
196-
val = MakeLiteral(name, valtype, val)
197-
elif isinstance(typ, type) and getattr(typ, 'isLiteralClass', None) is True:
198-
val = typ(val)
199-
elif isinstance(typ, type) and util.safe_issubclass(typ, ProtocolBase):
200-
val = typ(**util.coerce_for_expansion(val))
182+
"Attempted to set unknown property '{0}': {1} "
183+
.format(name, e))
201184

202185
self._extended_properties[name] = val
203186

@@ -282,13 +265,14 @@ def validate(self):
282265

283266
return True
284267

268+
285269
def MakeLiteral(name, typ, value, **properties):
286-
properties.update({'type': typ})
287-
klass = type(str(name), tuple((LiteralValue,)), {
288-
'__propinfo__': { '__literal__': properties}
289-
})
270+
properties.update({'type': typ})
271+
klass = type(str(name), tuple((LiteralValue,)), {
272+
'__propinfo__': {'__literal__': properties}
273+
})
290274

291-
return klass(value)
275+
return klass(value)
292276

293277

294278
class TypeProxy(object):
@@ -655,28 +639,10 @@ def _build_object(self, nm, clsdata, parents,**kw):
655639
# Need a validation to check that it meets one of them
656640
props['__validation__'] = {'type': klasses}
657641

658-
props['__extensible__'] = True
659-
if 'additionalProperties' in clsdata:
660-
addlProp = clsdata['additionalProperties']
661-
662-
if addlProp is False:
663-
props['__extensible__'] = False
664-
elif addlProp is True:
665-
props['__extensible__'] = True
666-
else:
667-
if '$ref' in addlProp:
668-
refs = self.resolve_classes([addlProp])
669-
else:
670-
uri = "{0}/{1}_{2}".format(nm,
671-
"<additionalProperties>", "<anonymous>")
672-
self.resolved[uri] = self.construct(
673-
uri,
674-
addlProp,
675-
(ProtocolBase,))
676-
refs = [self.resolved[uri]]
677-
678-
props['__extensible__'] = refs[0]
679-
642+
props['__extensible__'] = pattern_properties.ExtensibleValidator(
643+
nm,
644+
clsdata,
645+
self)
680646

681647
props['__prop_names__'] = name_translation
682648

@@ -719,7 +685,7 @@ def setprop(self, val):
719685
if not isinstance(typ, dict):
720686
type_checks.append(typ)
721687
continue
722-
typ = ProtocolBase.__SCHEMA_TYPES__[typ['type']]
688+
typ = validators.SCHEMA_TYPE_MAPPING[typ['type']]
723689
if typ is None:
724690
typ = type(None)
725691
if isinstance(typ, (list, tuple)):
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
import six
3+
import re
4+
import python_jsonschema_objects.validators as validators
5+
import python_jsonschema_objects.util as util
6+
import collections
7+
8+
import logging
9+
logger = logging.getLogger(__name__)
10+
11+
PatternDef = collections.namedtuple('PatternDef', 'pattern schema_type')
12+
13+
14+
class ExtensibleValidator(object):
15+
16+
def __init__(self, name, schemadef, builder):
17+
import python_jsonschema_objects.classbuilder as cb
18+
19+
self._pattern_types = []
20+
self._additional_type = True
21+
22+
addlProp = schemadef.get('additionalProperties', True)
23+
24+
if addlProp is False:
25+
self._additional_type = False
26+
elif addlProp is True:
27+
self._additional_type = True
28+
else:
29+
if '$ref' in addlProp:
30+
refs = builder.resolve_classes([addlProp])
31+
else:
32+
uri = "{0}/{1}_{2}".format(
33+
name,
34+
"<additionalProperties>", "<anonymous>")
35+
builder.resolved[uri] = builder.construct(
36+
uri,
37+
addlProp,
38+
(cb.ProtocolBase,))
39+
refs = [builder.resolved[uri]]
40+
41+
self._additional_type = refs[0]
42+
43+
for pattern, typedef in six.iteritems(
44+
schemadef.get('patternProperties', {})):
45+
if '$ref' in typedef:
46+
refs = builder.resolve_classes([typedef])
47+
else:
48+
uri = "{0}/{1}_{2}".format(name,
49+
"<patternProperties>",
50+
pattern)
51+
52+
builder.resolved[uri] = builder.construct(
53+
uri,
54+
typedef,
55+
(cb.ProtocolBase,))
56+
refs = [builder.resolved[uri]]
57+
58+
self._pattern_types.append(PatternDef(
59+
pattern=re.compile(pattern),
60+
schema_type=refs[0]
61+
))
62+
63+
def _make_type(self, typ, val):
64+
import python_jsonschema_objects.classbuilder as cb
65+
66+
if getattr(
67+
typ, 'isLiteralClass', None) is True:
68+
return typ(val)
69+
70+
if util.safe_issubclass(typ, cb.ProtocolBase):
71+
return typ(**util.coerce_for_expansion(val))
72+
73+
if util.safe_issubclass(typ, validators.ArrayValidator):
74+
return typ(val)
75+
76+
raise validators.ValidationError(
77+
"additionalProperty type {0} was neither a literal "
78+
"nor a schema wrapper: {1}".format(typ, val))
79+
80+
def instantiate(self, name, val):
81+
import python_jsonschema_objects.classbuilder as cb
82+
83+
for p in self._pattern_types:
84+
if p.pattern.search(name):
85+
logger.debug(
86+
"Found patternProperties match: %s %s" % (
87+
p.pattern.pattern, name
88+
))
89+
return self._make_type(p.schema_type, val)
90+
91+
if self._additional_type is True:
92+
93+
valtype = [k for k, t
94+
in six.iteritems(validators.SCHEMA_TYPE_MAPPING)
95+
if t is not None and isinstance(val, t)]
96+
valtype = valtype[0]
97+
return cb.MakeLiteral(name, valtype, val)
98+
99+
elif isinstance(self._additional_type, type):
100+
return self._make_type(self._additional_type, val)
101+
102+
raise validators.ValidationError(
103+
"additionalProperties not permitted "
104+
"and no patternProperties specified")

python_jsonschema_objects/validators.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@
44
import logging
55
logger = logging.getLogger(__name__)
66

7+
SCHEMA_TYPE_MAPPING = {
8+
'array': list,
9+
'boolean': bool,
10+
'integer': six.integer_types,
11+
'number': six.integer_types + (float,),
12+
'null': type(None),
13+
'string': six.string_types,
14+
'object': dict
15+
}
16+
717

818
class ValidationError(Exception):
919
pass
1020

21+
1122
class ValidatorRegistry(object):
1223

1324
def __init__(self):

test/test_pattern_properties.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import pytest
2+
3+
import python_jsonschema_objects as pjo
4+
import json
5+
6+
7+
@pytest.fixture
8+
def base_schema():
9+
return {
10+
'title': 'example',
11+
'type': 'object',
12+
"properties": {
13+
'foobar': {"type": "boolean"}
14+
},
15+
"patternProperties": {
16+
"^foo.*": {
17+
"type": "string"
18+
},
19+
"^bar.*": {
20+
"type": "integer"
21+
}
22+
}
23+
}
24+
25+
26+
def test_standard_properties_take_precedence(base_schema):
27+
""" foobar is a boolean, and it's a standard property,
28+
so we expect it will validate properly as a boolean,
29+
not using the patternProperty that matches it.
30+
"""
31+
builder = pjo.ObjectBuilder(base_schema)
32+
ns = builder.build_classes()
33+
34+
t = ns.Example(foobar=True)
35+
t.validate()
36+
37+
with pytest.raises(pjo.ValidationError):
38+
# Try against the foo pattern
39+
x = ns.Example(foobar="hi")
40+
x. validate()
41+
42+
43+
@pytest.mark.parametrize('permit_addl,property,value,is_error',[
44+
(False, 'foo', 'hello', False),
45+
(False, 'foobarro', 'hello', False),
46+
(False, 'foo', 24, True),
47+
(False, 'barkeep', 24, False),
48+
(False, 'barkeep', "John", True),
49+
(False, 'extraprop', "John", True),
50+
(True, 'extraprop', "John", False),
51+
# Test that the pattern props take precedence.
52+
# because these should validate against them, not the
53+
# additionalProperties that match
54+
(True, 'foobar', True, False),
55+
(True, 'foobar', "John", True),
56+
(True, 'foobar', 24, True),
57+
])
58+
def test_pattern_properties_work(
59+
base_schema, permit_addl, property, value, is_error):
60+
61+
base_schema['additionalProperties'] = permit_addl
62+
63+
builder = pjo.ObjectBuilder(base_schema)
64+
ns = builder.build_classes()
65+
66+
props = dict([(property, value)])
67+
68+
if is_error:
69+
with pytest.raises(pjo.ValidationError):
70+
t = ns.Example(**props)
71+
t.validate()
72+
else:
73+
t = ns.Example(**props)
74+
t.validate()

0 commit comments

Comments
 (0)