Skip to content

Commit edfbb67

Browse files
committed
Feature: Make Arrays first-class wrapper objects.
Prior to now, arrays objects were handled a little weirdly in that they were stored as native python lists with an associated validator object. This resulted in all sorts of unexpected behavior, with nested arrays being returned without their validators under certain circumstances. This resolves the issue by converting all arrays to first-class object wrappers (and renaming the wrapper to ArrayWrapper instead of ArrayValidator).
1 parent 53f576e commit edfbb67

File tree

8 files changed

+366
-245
lines changed

8 files changed

+366
-245
lines changed

python_jsonschema_objects/classbuilder.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import sys
99

1010
import logging
11+
12+
import python_jsonschema_objects.wrapper_types
13+
1114
logger = logging.getLogger(__name__)
1215

1316
logger.addHandler(logging.NullHandler())
@@ -153,7 +156,6 @@ def __init__(self, **props):
153156
six.moves.xrange(len(self.__prop_names__))]))
154157

155158
for prop in props:
156-
157159
try:
158160
logger.debug(util.lazy_format("Setting value for '{0}' to {1}", prop, props[prop]))
159161
setattr(self, prop, props[prop])
@@ -318,7 +320,11 @@ def __init__(self, value, typ=None):
318320
:value: @todo
319321
320322
"""
321-
self._value = value
323+
if isinstance(value, LiteralValue):
324+
self._value = value._value
325+
else:
326+
self._value = value
327+
322328
self.validate()
323329

324330
def as_dict(self):
@@ -456,7 +462,7 @@ def _construct(self, uri, clsdata, parent=(ProtocolBase,),**kw):
456462
elif clsdata.get('type') == 'array' and 'items' in clsdata:
457463
clsdata_copy = {}
458464
clsdata_copy.update(clsdata)
459-
self.resolved[uri] = validators.ArrayValidator.create(
465+
self.resolved[uri] = python_jsonschema_objects.wrapper_types.ArrayWrapper.create(
460466
uri,
461467
item_constraint=clsdata_copy.pop('items'),
462468
classbuilder=self,
@@ -581,7 +587,7 @@ def _build_object(self, nm, clsdata, parents,**kw):
581587
typ = self.construct(uri, detail['items'])
582588
propdata = {
583589
'type': 'array',
584-
'validator': validators.ArrayValidator.create(
590+
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(
585591
uri,
586592
item_constraint=typ)}
587593
else:
@@ -602,14 +608,14 @@ def _build_object(self, nm, clsdata, parents,**kw):
602608
else:
603609
typ = self.construct(uri, detail['items'])
604610
propdata = {'type': 'array',
605-
'validator': validators.ArrayValidator.create(uri, item_constraint=typ,
606-
addl_constraints=detail)}
611+
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(uri, item_constraint=typ,
612+
addl_constraints=detail)}
607613
except NotImplementedError:
608614
typ = detail['items']
609615
propdata = {'type': 'array',
610-
'validator': validators.ArrayValidator.create(uri,
611-
item_constraint=typ,
612-
addl_constraints=detail)}
616+
'validator': python_jsonschema_objects.wrapper_types.ArrayWrapper.create(uri,
617+
item_constraint=typ,
618+
addl_constraints=detail)}
613619

614620
props[prop] = make_property(prop,
615621
propdata,
@@ -725,7 +731,7 @@ def setprop(self, val):
725731
val.validate()
726732
ok = True
727733
break
728-
elif util.safe_issubclass(typ, validators.ArrayValidator):
734+
elif util.safe_issubclass(typ, python_jsonschema_objects.wrapper_types.ArrayWrapper):
729735
try:
730736
val = typ(val)
731737
except Exception as e:
@@ -743,13 +749,14 @@ def setprop(self, val):
743749
"Object must be one of {0}: \n{1}".format(info['type'], errstr))
744750

745751
elif info['type'] == 'array':
746-
instance = info['validator'](val)
747-
val = instance.validate()
752+
val = info['validator'](val)
753+
val.validate()
748754

749-
elif util.safe_issubclass(info['type'], validators.ArrayValidator):
755+
elif util.safe_issubclass(info['type'],
756+
python_jsonschema_objects.wrapper_types.ArrayWrapper):
750757
# An array type may have already been converted into an ArrayValidator
751-
instance = info['type'](val)
752-
val = instance.validate()
758+
val = info['type'](val)
759+
val.validate()
753760

754761
elif getattr(info['type'], 'isLiteralClass', False) is True:
755762
if not isinstance(val, info['type']):

python_jsonschema_objects/pattern_properties.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import collections
77

88
import logging
9+
10+
import python_jsonschema_objects.wrapper_types
11+
912
logger = logging.getLogger(__name__)
1013

1114
PatternDef = collections.namedtuple('PatternDef', 'pattern schema_type')
@@ -70,7 +73,7 @@ def _make_type(self, typ, val):
7073
if util.safe_issubclass(typ, cb.ProtocolBase):
7174
return typ(**util.coerce_for_expansion(val))
7275

73-
if util.safe_issubclass(typ, validators.ArrayValidator):
76+
if util.safe_issubclass(typ, python_jsonschema_objects.wrapper_types.ArrayWrapper):
7477
return typ(val)
7578

7679
raise validators.ValidationError(

python_jsonschema_objects/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ class ProtocolJSONEncoder(json.JSONEncoder):
4444

4545
def default(self, obj):
4646
from python_jsonschema_objects import classbuilder
47-
from python_jsonschema_objects import validators
47+
from python_jsonschema_objects import wrapper_types
4848

4949
if isinstance(obj, classbuilder.LiteralValue):
5050
return obj._value
51-
if isinstance(obj, validators.ArrayValidator):
51+
if isinstance(obj, wrapper_types.ArrayWrapper):
5252
return obj.for_json()
5353
if isinstance(obj, classbuilder.ProtocolBase):
5454
props = {}
Lines changed: 3 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import six
2-
from python_jsonschema_objects import util
3-
import collections
41
import logging
2+
3+
import six
4+
55
logger = logging.getLogger(__name__)
66

77
SCHEMA_TYPE_MAPPING = (
@@ -167,217 +167,3 @@ def check_type(param, value, type_data):
167167
type_check(param, value, type_data)
168168

169169

170-
class ArrayValidator(object):
171-
172-
def __init__(self, ary):
173-
if isinstance(ary, (list, tuple, collections.Sequence)):
174-
self.data = ary
175-
elif isinstance(ary, ArrayValidator):
176-
self.data = ary.data
177-
else:
178-
raise TypeError("Invalid value given to array validator: {0}"
179-
.format(ary))
180-
181-
@classmethod
182-
def from_json(cls, jsonmsg):
183-
import json
184-
msg = json.loads(jsonmsg)
185-
obj = cls(msg)
186-
obj.validate()
187-
return obj
188-
189-
def serialize(self):
190-
d = self.validate()
191-
enc = util.ProtocolJSONEncoder()
192-
return enc.encode(d)
193-
194-
def for_json(self):
195-
return self.validate()
196-
197-
def validate(self):
198-
converted = self.validate_items()
199-
self.validate_length()
200-
self.validate_uniqueness()
201-
return converted
202-
203-
def validate_uniqueness(self):
204-
from python_jsonschema_objects import classbuilder
205-
206-
if getattr(self, 'uniqueItems', None) is not None:
207-
testset = set(self.data)
208-
if len(testset) != len(self.data):
209-
raise ValidationError(
210-
"{0} has duplicate elements, but uniqueness required"
211-
.format(self.data))
212-
213-
def validate_length(self):
214-
from python_jsonschema_objects import classbuilder
215-
216-
if getattr(self, 'minItems', None) is not None:
217-
if len(self.data) < self.minItems:
218-
raise ValidationError(
219-
"{1} has too few elements. Wanted {0}."
220-
.format(self.minItems, self.data))
221-
222-
if getattr(self, 'maxItems', None) is not None:
223-
if len(self.data) > self.maxItems:
224-
raise ValidationError(
225-
"{1} has too few elements. Wanted {0}."
226-
.format(self.maxItems, self.data))
227-
228-
def validate_items(self):
229-
from python_jsonschema_objects import classbuilder
230-
231-
if self.__itemtype__ is None:
232-
return
233-
234-
type_checks = self.__itemtype__
235-
if not isinstance(type_checks, (tuple, list)):
236-
# we were given items = {'type': 'blah'} ; thus ensure the type for all data.
237-
type_checks = [type_checks] * len(self.data)
238-
elif len(type_checks) > len(self.data):
239-
raise ValidationError(
240-
"{1} does not have sufficient elements to validate against {0}"
241-
.format(self.__itemtype__, self.data))
242-
243-
typed_elems = []
244-
for elem, typ in zip(self.data, type_checks):
245-
if isinstance(typ, dict):
246-
for param, paramval in six.iteritems(typ):
247-
validator = registry(param)
248-
if validator is not None:
249-
validator(paramval, elem, typ)
250-
typed_elems.append(elem)
251-
252-
elif util.safe_issubclass(typ, classbuilder.LiteralValue):
253-
val = typ(elem)
254-
val.validate()
255-
typed_elems.append(val)
256-
elif util.safe_issubclass(typ, classbuilder.ProtocolBase):
257-
if not isinstance(elem, typ):
258-
try:
259-
if isinstance(elem, (six.string_types, six.integer_types, float)):
260-
val = typ(elem)
261-
else:
262-
val = typ(**util.coerce_for_expansion(elem))
263-
except TypeError as e:
264-
raise ValidationError("'{0}' is not a valid value for '{1}': {2}"
265-
.format(elem, typ, e))
266-
else:
267-
val = elem
268-
val.validate()
269-
typed_elems.append(val)
270-
271-
elif util.safe_issubclass(typ, ArrayValidator):
272-
val = typ(elem)
273-
val.validate()
274-
typed_elems.append(val)
275-
276-
elif isinstance(typ, classbuilder.TypeProxy):
277-
try:
278-
if isinstance(elem, (six.string_types, six.integer_types, float)):
279-
val = typ(elem)
280-
else:
281-
val = typ(**util.coerce_for_expansion(elem))
282-
except TypeError as e:
283-
raise ValidationError("'{0}' is not a valid value for '{1}': {2}"
284-
.format(elem, typ, e))
285-
else:
286-
val.validate()
287-
typed_elems.append(val)
288-
289-
return typed_elems
290-
291-
@staticmethod
292-
def create(name, item_constraint=None, **addl_constraints):
293-
""" Create an array validator based on the passed in constraints.
294-
295-
If item_constraint is a tuple, it is assumed that tuple validation
296-
is being performed. If it is a class or dictionary, list validation
297-
will be performed. Classes are assumed to be subclasses of ProtocolBase,
298-
while dictionaries are expected to be basic types ('string', 'number', ...).
299-
300-
addl_constraints is expected to be key-value pairs of any of the other
301-
constraints permitted by JSON Schema v4.
302-
"""
303-
from python_jsonschema_objects import classbuilder
304-
klassbuilder = addl_constraints.pop("classbuilder", None)
305-
props = {}
306-
307-
if item_constraint is not None:
308-
if isinstance(item_constraint, (tuple, list)):
309-
for i, elem in enumerate(item_constraint):
310-
isdict = isinstance(elem, (dict,))
311-
isklass = isinstance( elem, type) and util.safe_issubclass(
312-
elem, (classbuilder.ProtocolBase, classbuilder.LiteralValue))
313-
314-
if not any([isdict, isklass]):
315-
raise TypeError(
316-
"Item constraint (position {0}) is not a schema".format(i))
317-
elif isinstance(item_constraint, classbuilder.TypeProxy):
318-
pass
319-
elif util.safe_issubclass(item_constraint, ArrayValidator):
320-
pass
321-
else:
322-
isdict = isinstance(item_constraint, (dict,))
323-
isklass = isinstance( item_constraint, type) and util.safe_issubclass(
324-
item_constraint, (classbuilder.ProtocolBase, classbuilder.LiteralValue))
325-
326-
if not any([isdict, isklass]):
327-
raise TypeError("Item constraint is not a schema")
328-
329-
if isdict and '$ref' in item_constraint:
330-
if klassbuilder is None:
331-
raise TypeError("Cannot resolve {0} without classbuilder"
332-
.format(item_constraint['$ref']))
333-
334-
uri = item_constraint['$ref']
335-
if uri in klassbuilder.resolved:
336-
logger.debug(util.lazy_format(
337-
"Using previously resolved object for {0}", uri))
338-
else:
339-
logger.debug(util.lazy_format("Resolving object for {0}", uri))
340-
341-
with klassbuilder.resolver.resolving(uri) as resolved:
342-
# Set incase there is a circular reference in schema definition
343-
klassbuilder.resolved[uri] = None
344-
klassbuilder.resolved[uri] = klassbuilder.construct(
345-
uri,
346-
resolved,
347-
(classbuilder.ProtocolBase,))
348-
349-
item_constraint = klassbuilder.resolved[uri]
350-
351-
elif isdict and item_constraint.get('type') == 'array':
352-
# We need to create a sub-array validator.
353-
item_constraint = ArrayValidator.create(name + "#sub",
354-
item_constraint=item_constraint[
355-
'items'],
356-
addl_constraints=item_constraint)
357-
elif isdict and 'oneOf' in item_constraint:
358-
# We need to create a TypeProxy validator
359-
uri = "{0}_{1}".format(name, "<anonymous_list_type>")
360-
type_array = []
361-
for i, item_detail in enumerate(item_constraint['oneOf']):
362-
if '$ref' in item_detail:
363-
subtype = klassbuilder.construct(
364-
util.resolve_ref_uri(
365-
klassbuilder.resolver.resolution_scope,
366-
item_detail['$ref']),
367-
item_detail)
368-
else:
369-
subtype = klassbuilder.construct(
370-
uri + "_%s" % i, item_detail)
371-
372-
type_array.append(subtype)
373-
374-
item_constraint = classbuilder.TypeProxy(type_array)
375-
376-
props['__itemtype__'] = item_constraint
377-
378-
props.update(addl_constraints)
379-
380-
validator = type(str(name), (ArrayValidator,), props)
381-
382-
return validator
383-

0 commit comments

Comments
 (0)