1from __future__ import division 2 3import contextlib 4import json 5import numbers 6 7try: 8 import requests 9except ImportError: 10 requests = None 11 12from jsonschema import _utils, _validators 13from jsonschema.compat import ( 14 Sequence, urljoin, urlsplit, urldefrag, unquote, urlopen, 15 str_types, int_types, iteritems, lru_cache, 16) 17from jsonschema.exceptions import ErrorTree # Backwards compat # noqa: F401 18from jsonschema.exceptions import RefResolutionError, SchemaError, UnknownType 19 20 21_unset = _utils.Unset() 22 23validators = {} 24meta_schemas = _utils.URIDict() 25 26 27def validates(version): 28 """ 29 Register the decorated validator for a ``version`` of the specification. 30 31 Registered validators and their meta schemas will be considered when 32 parsing ``$schema`` properties' URIs. 33 34 Arguments: 35 36 version (str): 37 38 An identifier to use as the version's name 39 40 Returns: 41 42 callable: a class decorator to decorate the validator with the version 43 44 """ 45 46 def _validates(cls): 47 validators[version] = cls 48 if u"id" in cls.META_SCHEMA: 49 meta_schemas[cls.META_SCHEMA[u"id"]] = cls 50 return cls 51 return _validates 52 53 54def create(meta_schema, validators=(), version=None, default_types=None): # noqa: C901, E501 55 if default_types is None: 56 default_types = { 57 u"array": list, u"boolean": bool, u"integer": int_types, 58 u"null": type(None), u"number": numbers.Number, u"object": dict, 59 u"string": str_types, 60 } 61 62 class Validator(object): 63 VALIDATORS = dict(validators) 64 META_SCHEMA = dict(meta_schema) 65 DEFAULT_TYPES = dict(default_types) 66 67 def __init__( 68 self, schema, types=(), resolver=None, format_checker=None, 69 ): 70 self._types = dict(self.DEFAULT_TYPES) 71 self._types.update(types) 72 73 if resolver is None: 74 resolver = RefResolver.from_schema(schema) 75 76 self.resolver = resolver 77 self.format_checker = format_checker 78 self.schema = schema 79 80 @classmethod 81 def check_schema(cls, schema): 82 for error in cls(cls.META_SCHEMA).iter_errors(schema): 83 raise SchemaError.create_from(error) 84 85 def iter_errors(self, instance, _schema=None): 86 if _schema is None: 87 _schema = self.schema 88 89 scope = _schema.get(u"id") 90 if scope: 91 self.resolver.push_scope(scope) 92 try: 93 ref = _schema.get(u"$ref") 94 if ref is not None: 95 validators = [(u"$ref", ref)] 96 else: 97 validators = iteritems(_schema) 98 99 for k, v in validators: 100 validator = self.VALIDATORS.get(k) 101 if validator is None: 102 continue 103 104 errors = validator(self, v, instance, _schema) or () 105 for error in errors: 106 # set details if not already set by the called fn 107 error._set( 108 validator=k, 109 validator_value=v, 110 instance=instance, 111 schema=_schema, 112 ) 113 if k != u"$ref": 114 error.schema_path.appendleft(k) 115 yield error 116 finally: 117 if scope: 118 self.resolver.pop_scope() 119 120 def descend(self, instance, schema, path=None, schema_path=None): 121 for error in self.iter_errors(instance, schema): 122 if path is not None: 123 error.path.appendleft(path) 124 if schema_path is not None: 125 error.schema_path.appendleft(schema_path) 126 yield error 127 128 def validate(self, *args, **kwargs): 129 for error in self.iter_errors(*args, **kwargs): 130 raise error 131 132 def is_type(self, instance, type): 133 if type not in self._types: 134 raise UnknownType(type, instance, self.schema) 135 pytypes = self._types[type] 136 137 # bool inherits from int, so ensure bools aren't reported as ints 138 if isinstance(instance, bool): 139 pytypes = _utils.flatten(pytypes) 140 is_number = any( 141 issubclass(pytype, numbers.Number) for pytype in pytypes 142 ) 143 if is_number and bool not in pytypes: 144 return False 145 return isinstance(instance, pytypes) 146 147 def is_valid(self, instance, _schema=None): 148 error = next(self.iter_errors(instance, _schema), None) 149 return error is None 150 151 if version is not None: 152 Validator = validates(version)(Validator) 153 Validator.__name__ = version.title().replace(" ", "") + "Validator" 154 155 return Validator 156 157 158def extend(validator, validators, version=None): 159 all_validators = dict(validator.VALIDATORS) 160 all_validators.update(validators) 161 return create( 162 meta_schema=validator.META_SCHEMA, 163 validators=all_validators, 164 version=version, 165 default_types=validator.DEFAULT_TYPES, 166 ) 167 168 169Draft3Validator = create( 170 meta_schema=_utils.load_schema("draft3"), 171 validators={ 172 u"$ref": _validators.ref, 173 u"additionalItems": _validators.additionalItems, 174 u"additionalProperties": _validators.additionalProperties, 175 u"dependencies": _validators.dependencies, 176 u"disallow": _validators.disallow_draft3, 177 u"divisibleBy": _validators.multipleOf, 178 u"enum": _validators.enum, 179 u"extends": _validators.extends_draft3, 180 u"format": _validators.format, 181 u"items": _validators.items, 182 u"maxItems": _validators.maxItems, 183 u"maxLength": _validators.maxLength, 184 u"maximum": _validators.maximum, 185 u"minItems": _validators.minItems, 186 u"minLength": _validators.minLength, 187 u"minimum": _validators.minimum, 188 u"multipleOf": _validators.multipleOf, 189 u"pattern": _validators.pattern, 190 u"patternProperties": _validators.patternProperties, 191 u"properties": _validators.properties_draft3, 192 u"type": _validators.type_draft3, 193 u"uniqueItems": _validators.uniqueItems, 194 }, 195 version="draft3", 196) 197 198Draft4Validator = create( 199 meta_schema=_utils.load_schema("draft4"), 200 validators={ 201 u"$ref": _validators.ref, 202 u"additionalItems": _validators.additionalItems, 203 u"additionalProperties": _validators.additionalProperties, 204 u"allOf": _validators.allOf_draft4, 205 u"anyOf": _validators.anyOf_draft4, 206 u"dependencies": _validators.dependencies, 207 u"enum": _validators.enum, 208 u"format": _validators.format, 209 u"items": _validators.items, 210 u"maxItems": _validators.maxItems, 211 u"maxLength": _validators.maxLength, 212 u"maxProperties": _validators.maxProperties_draft4, 213 u"maximum": _validators.maximum, 214 u"minItems": _validators.minItems, 215 u"minLength": _validators.minLength, 216 u"minProperties": _validators.minProperties_draft4, 217 u"minimum": _validators.minimum, 218 u"multipleOf": _validators.multipleOf, 219 u"not": _validators.not_draft4, 220 u"oneOf": _validators.oneOf_draft4, 221 u"pattern": _validators.pattern, 222 u"patternProperties": _validators.patternProperties, 223 u"properties": _validators.properties_draft4, 224 u"required": _validators.required_draft4, 225 u"type": _validators.type_draft4, 226 u"uniqueItems": _validators.uniqueItems, 227 }, 228 version="draft4", 229) 230 231 232class RefResolver(object): 233 """ 234 Resolve JSON References. 235 236 Arguments: 237 238 base_uri (str): 239 240 The URI of the referring document 241 242 referrer: 243 244 The actual referring document 245 246 store (dict): 247 248 A mapping from URIs to documents to cache 249 250 cache_remote (bool): 251 252 Whether remote refs should be cached after first resolution 253 254 handlers (dict): 255 256 A mapping from URI schemes to functions that should be used 257 to retrieve them 258 259 urljoin_cache (functools.lru_cache): 260 261 A cache that will be used for caching the results of joining 262 the resolution scope to subscopes. 263 264 remote_cache (functools.lru_cache): 265 266 A cache that will be used for caching the results of 267 resolved remote URLs. 268 269 """ 270 271 def __init__( 272 self, 273 base_uri, 274 referrer, 275 store=(), 276 cache_remote=True, 277 handlers=(), 278 urljoin_cache=None, 279 remote_cache=None, 280 ): 281 if urljoin_cache is None: 282 urljoin_cache = lru_cache(1024)(urljoin) 283 if remote_cache is None: 284 remote_cache = lru_cache(1024)(self.resolve_from_url) 285 286 self.referrer = referrer 287 self.cache_remote = cache_remote 288 self.handlers = dict(handlers) 289 290 self._scopes_stack = [base_uri] 291 self.store = _utils.URIDict( 292 (id, validator.META_SCHEMA) 293 for id, validator in iteritems(meta_schemas) 294 ) 295 self.store.update(store) 296 self.store[base_uri] = referrer 297 298 self._urljoin_cache = urljoin_cache 299 self._remote_cache = remote_cache 300 301 @classmethod 302 def from_schema(cls, schema, *args, **kwargs): 303 """ 304 Construct a resolver from a JSON schema object. 305 306 Arguments: 307 308 schema: 309 310 the referring schema 311 312 Returns: 313 314 :class:`RefResolver` 315 316 """ 317 318 return cls(schema.get(u"id", u""), schema, *args, **kwargs) 319 320 def push_scope(self, scope): 321 self._scopes_stack.append( 322 self._urljoin_cache(self.resolution_scope, scope), 323 ) 324 325 def pop_scope(self): 326 try: 327 self._scopes_stack.pop() 328 except IndexError: 329 raise RefResolutionError( 330 "Failed to pop the scope from an empty stack. " 331 "`pop_scope()` should only be called once for every " 332 "`push_scope()`" 333 ) 334 335 @property 336 def resolution_scope(self): 337 return self._scopes_stack[-1] 338 339 @property 340 def base_uri(self): 341 uri, _ = urldefrag(self.resolution_scope) 342 return uri 343 344 @contextlib.contextmanager 345 def in_scope(self, scope): 346 self.push_scope(scope) 347 try: 348 yield 349 finally: 350 self.pop_scope() 351 352 @contextlib.contextmanager 353 def resolving(self, ref): 354 """ 355 Context manager which resolves a JSON ``ref`` and enters the 356 resolution scope of this ref. 357 358 Arguments: 359 360 ref (str): 361 362 The reference to resolve 363 364 """ 365 366 url, resolved = self.resolve(ref) 367 self.push_scope(url) 368 try: 369 yield resolved 370 finally: 371 self.pop_scope() 372 373 def resolve(self, ref): 374 url = self._urljoin_cache(self.resolution_scope, ref) 375 return url, self._remote_cache(url) 376 377 def resolve_from_url(self, url): 378 url, fragment = urldefrag(url) 379 try: 380 document = self.store[url] 381 except KeyError: 382 try: 383 document = self.resolve_remote(url) 384 except Exception as exc: 385 raise RefResolutionError(exc) 386 387 return self.resolve_fragment(document, fragment) 388 389 def resolve_fragment(self, document, fragment): 390 """ 391 Resolve a ``fragment`` within the referenced ``document``. 392 393 Arguments: 394 395 document: 396 397 The referrant document 398 399 fragment (str): 400 401 a URI fragment to resolve within it 402 403 """ 404 405 fragment = fragment.lstrip(u"/") 406 parts = unquote(fragment).split(u"/") if fragment else [] 407 408 for part in parts: 409 part = part.replace(u"~1", u"/").replace(u"~0", u"~") 410 411 if isinstance(document, Sequence): 412 # Array indexes should be turned into integers 413 try: 414 part = int(part) 415 except ValueError: 416 pass 417 try: 418 document = document[part] 419 except (TypeError, LookupError): 420 raise RefResolutionError( 421 "Unresolvable JSON pointer: %r" % fragment 422 ) 423 424 return document 425 426 def resolve_remote(self, uri): 427 """ 428 Resolve a remote ``uri``. 429 430 If called directly, does not check the store first, but after 431 retrieving the document at the specified URI it will be saved in 432 the store if :attr:`cache_remote` is True. 433 434 .. note:: 435 436 If the requests_ library is present, ``jsonschema`` will use it to 437 request the remote ``uri``, so that the correct encoding is 438 detected and used. 439 440 If it isn't, or if the scheme of the ``uri`` is not ``http`` or 441 ``https``, UTF-8 is assumed. 442 443 Arguments: 444 445 uri (str): 446 447 The URI to resolve 448 449 Returns: 450 451 The retrieved document 452 453 .. _requests: http://pypi.python.org/pypi/requests/ 454 455 """ 456 457 scheme = urlsplit(uri).scheme 458 459 if scheme in self.handlers: 460 result = self.handlers[scheme](uri) 461 elif ( 462 scheme in [u"http", u"https"] and 463 requests and 464 getattr(requests.Response, "json", None) is not None 465 ): 466 # Requests has support for detecting the correct encoding of 467 # json over http 468 if callable(requests.Response.json): 469 result = requests.get(uri).json() 470 else: 471 result = requests.get(uri).json 472 else: 473 # Otherwise, pass off to urllib and assume utf-8 474 result = json.loads(urlopen(uri).read().decode("utf-8")) 475 476 if self.cache_remote: 477 self.store[uri] = result 478 return result 479 480 481def validator_for(schema, default=_unset): 482 if default is _unset: 483 default = Draft4Validator 484 return meta_schemas.get(schema.get(u"$schema", u""), default) 485 486 487def validate(instance, schema, cls=None, *args, **kwargs): 488 """ 489 Validate an instance under the given schema. 490 491 >>> validate([2, 3, 4], {"maxItems": 2}) 492 Traceback (most recent call last): 493 ... 494 ValidationError: [2, 3, 4] is too long 495 496 :func:`validate` will first verify that the provided schema is itself 497 valid, since not doing so can lead to less obvious error messages and fail 498 in less obvious or consistent ways. If you know you have a valid schema 499 already or don't care, you might prefer using the 500 :meth:`~IValidator.validate` method directly on a specific validator 501 (e.g. :meth:`Draft4Validator.validate`). 502 503 504 Arguments: 505 506 instance: 507 508 The instance to validate 509 510 schema: 511 512 The schema to validate with 513 514 cls (:class:`IValidator`): 515 516 The class that will be used to validate the instance. 517 518 If the ``cls`` argument is not provided, two things will happen in 519 accordance with the specification. First, if the schema has a 520 :validator:`$schema` property containing a known meta-schema [#]_ then the 521 proper validator will be used. The specification recommends that all 522 schemas contain :validator:`$schema` properties for this reason. If no 523 :validator:`$schema` property is found, the default validator class is 524 :class:`Draft4Validator`. 525 526 Any other provided positional and keyword arguments will be passed on when 527 instantiating the ``cls``. 528 529 Raises: 530 531 :exc:`ValidationError` if the instance is invalid 532 533 :exc:`SchemaError` if the schema itself is invalid 534 535 .. rubric:: Footnotes 536 .. [#] known by a validator registered with :func:`validates` 537 """ 538 if cls is None: 539 cls = validator_for(schema) 540 cls.check_schema(schema) 541 cls(schema, *args, **kwargs).validate(instance) 542