diff options
-rw-r--r-- | .appveyor.yml | 40 | ||||
-rw-r--r-- | .coveragerc | 5 | ||||
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | .travis.yml | 31 | ||||
-rw-r--r-- | CHANGELOG.rst | 166 | ||||
-rw-r--r-- | COPYING | 19 | ||||
-rw-r--r-- | MANIFEST.in | 4 | ||||
-rw-r--r-- | README.rst | 154 | ||||
-rw-r--r-- | codecov.yml | 11 | ||||
-rw-r--r-- | docs/Makefile | 227 | ||||
-rw-r--r-- | docs/conf.py | 255 | ||||
-rw-r--r-- | docs/creating.rst | 25 | ||||
-rw-r--r-- | docs/errors.rst | 445 | ||||
-rw-r--r-- | docs/faq.rst | 148 | ||||
-rw-r--r-- | docs/index.rst | 56 | ||||
-rw-r--r-- | docs/jsonschema_role.py | 151 | ||||
-rw-r--r-- | docs/make.bat | 190 | ||||
-rw-r--r-- | docs/references.rst | 13 | ||||
-rw-r--r-- | docs/requirements.txt | 4 | ||||
-rw-r--r-- | docs/spelling-wordlist.txt | 30 | ||||
-rw-r--r-- | docs/validate.rst | 380 | ||||
-rw-r--r-- | json/.gitignore | 1 | ||||
-rw-r--r-- | json/.travis.yml | 9 | ||||
-rw-r--r-- | json/LICENSE (renamed from LICENSE) | 0 | ||||
-rw-r--r-- | json/README.md (renamed from README.md) | 0 | ||||
-rwxr-xr-x | json/bin/jsonschema_suite (renamed from bin/jsonschema_suite) | 0 | ||||
-rw-r--r-- | json/index.js (renamed from index.js) | 0 | ||||
-rw-r--r-- | json/package.json (renamed from package.json) | 0 | ||||
-rw-r--r-- | json/remotes/folder/folderInteger.json (renamed from remotes/folder/folderInteger.json) | 0 | ||||
-rw-r--r-- | json/remotes/integer.json (renamed from remotes/integer.json) | 0 | ||||
-rw-r--r-- | json/remotes/name.json (renamed from remotes/name.json) | 0 | ||||
-rw-r--r-- | json/remotes/subSchemas.json (renamed from remotes/subSchemas.json) | 0 | ||||
-rw-r--r-- | json/test-schema.json (renamed from test-schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalItems.json (renamed from tests/draft3/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/additionalProperties.json (renamed from tests/draft3/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/default.json (renamed from tests/draft3/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/dependencies.json (renamed from tests/draft3/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/disallow.json (renamed from tests/draft3/disallow.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/divisibleBy.json (renamed from tests/draft3/divisibleBy.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/enum.json (renamed from tests/draft3/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/extends.json (renamed from tests/draft3/extends.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/items.json (renamed from tests/draft3/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxItems.json (renamed from tests/draft3/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maxLength.json (renamed from tests/draft3/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/maximum.json (renamed from tests/draft3/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minItems.json (renamed from tests/draft3/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minLength.json (renamed from tests/draft3/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/minimum.json (renamed from tests/draft3/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/bignum.json (renamed from tests/draft3/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/format.json (renamed from tests/draft3/optional/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/jsregex.json (renamed from tests/draft3/optional/jsregex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/optional/zeroTerminatedFloats.json (renamed from tests/draft3/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/pattern.json (renamed from tests/draft3/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/patternProperties.json (renamed from tests/draft3/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/properties.json (renamed from tests/draft3/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/ref.json (renamed from tests/draft3/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/refRemote.json (renamed from tests/draft3/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/required.json (renamed from tests/draft3/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/type.json (renamed from tests/draft3/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft3/uniqueItems.json (renamed from tests/draft3/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalItems.json (renamed from tests/draft4/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/additionalProperties.json (renamed from tests/draft4/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/allOf.json (renamed from tests/draft4/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/anyOf.json (renamed from tests/draft4/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/default.json (renamed from tests/draft4/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/definitions.json (renamed from tests/draft4/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/dependencies.json (renamed from tests/draft4/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/enum.json (renamed from tests/draft4/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/items.json (renamed from tests/draft4/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxItems.json (renamed from tests/draft4/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxLength.json (renamed from tests/draft4/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maxProperties.json (renamed from tests/draft4/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/maximum.json (renamed from tests/draft4/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minItems.json (renamed from tests/draft4/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minLength.json (renamed from tests/draft4/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minProperties.json (renamed from tests/draft4/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/minimum.json (renamed from tests/draft4/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/multipleOf.json (renamed from tests/draft4/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/not.json (renamed from tests/draft4/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/oneOf.json (renamed from tests/draft4/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/bignum.json (renamed from tests/draft4/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/ecmascript-regex.json (renamed from tests/draft4/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/format.json (renamed from tests/draft4/optional/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/optional/zeroTerminatedFloats.json (renamed from tests/draft4/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/pattern.json (renamed from tests/draft4/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/patternProperties.json (renamed from tests/draft4/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/properties.json (renamed from tests/draft4/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/ref.json (renamed from tests/draft4/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/refRemote.json (renamed from tests/draft4/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/required.json (renamed from tests/draft4/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/type.json (renamed from tests/draft4/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft4/uniqueItems.json (renamed from tests/draft4/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/additionalItems.json (renamed from tests/draft6/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/additionalProperties.json (renamed from tests/draft6/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/allOf.json (renamed from tests/draft6/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/anyOf.json (renamed from tests/draft6/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/boolean_schema.json (renamed from tests/draft6/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/const.json (renamed from tests/draft6/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/contains.json (renamed from tests/draft6/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/default.json (renamed from tests/draft6/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/definitions.json (renamed from tests/draft6/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/dependencies.json (renamed from tests/draft6/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/enum.json (renamed from tests/draft6/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/exclusiveMaximum.json (renamed from tests/draft6/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/exclusiveMinimum.json (renamed from tests/draft6/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/items.json (renamed from tests/draft6/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxItems.json (renamed from tests/draft6/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxLength.json (renamed from tests/draft6/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maxProperties.json (renamed from tests/draft6/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/maximum.json (renamed from tests/draft6/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minItems.json (renamed from tests/draft6/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minLength.json (renamed from tests/draft6/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minProperties.json (renamed from tests/draft6/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/minimum.json (renamed from tests/draft6/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/multipleOf.json (renamed from tests/draft6/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/not.json (renamed from tests/draft6/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/oneOf.json (renamed from tests/draft6/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/bignum.json (renamed from tests/draft6/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/ecmascript-regex.json (renamed from tests/draft6/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/format.json (renamed from tests/draft6/optional/format.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/optional/zeroTerminatedFloats.json (renamed from tests/draft6/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/pattern.json (renamed from tests/draft6/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/patternProperties.json (renamed from tests/draft6/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/properties.json (renamed from tests/draft6/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/propertyNames.json (renamed from tests/draft6/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/ref.json (renamed from tests/draft6/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/refRemote.json (renamed from tests/draft6/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/required.json (renamed from tests/draft6/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/type.json (renamed from tests/draft6/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft6/uniqueItems.json (renamed from tests/draft6/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/additionalItems.json (renamed from tests/draft7/additionalItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/additionalProperties.json (renamed from tests/draft7/additionalProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/allOf.json (renamed from tests/draft7/allOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/anyOf.json (renamed from tests/draft7/anyOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/boolean_schema.json (renamed from tests/draft7/boolean_schema.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/const.json (renamed from tests/draft7/const.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/contains.json (renamed from tests/draft7/contains.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/default.json (renamed from tests/draft7/default.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/definitions.json (renamed from tests/draft7/definitions.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/dependencies.json (renamed from tests/draft7/dependencies.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/enum.json (renamed from tests/draft7/enum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/exclusiveMaximum.json (renamed from tests/draft7/exclusiveMaximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/exclusiveMinimum.json (renamed from tests/draft7/exclusiveMinimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/if-then-else.json (renamed from tests/draft7/if-then-else.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/items.json (renamed from tests/draft7/items.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxItems.json (renamed from tests/draft7/maxItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxLength.json (renamed from tests/draft7/maxLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maxProperties.json (renamed from tests/draft7/maxProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/maximum.json (renamed from tests/draft7/maximum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minItems.json (renamed from tests/draft7/minItems.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minLength.json (renamed from tests/draft7/minLength.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minProperties.json (renamed from tests/draft7/minProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/minimum.json (renamed from tests/draft7/minimum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/multipleOf.json (renamed from tests/draft7/multipleOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/not.json (renamed from tests/draft7/not.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/oneOf.json (renamed from tests/draft7/oneOf.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/bignum.json (renamed from tests/draft7/optional/bignum.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/content.json (renamed from tests/draft7/optional/content.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/ecmascript-regex.json (renamed from tests/draft7/optional/ecmascript-regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/date-time.json (renamed from tests/draft7/optional/format/date-time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/date.json (renamed from tests/draft7/optional/format/date.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/email.json (renamed from tests/draft7/optional/format/email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/hostname.json (renamed from tests/draft7/optional/format/hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/idn-email.json (renamed from tests/draft7/optional/format/idn-email.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/idn-hostname.json (renamed from tests/draft7/optional/format/idn-hostname.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/ipv4.json (renamed from tests/draft7/optional/format/ipv4.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/ipv6.json (renamed from tests/draft7/optional/format/ipv6.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/iri-reference.json (renamed from tests/draft7/optional/format/iri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/iri.json (renamed from tests/draft7/optional/format/iri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/json-pointer.json (renamed from tests/draft7/optional/format/json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/regex.json (renamed from tests/draft7/optional/format/regex.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/relative-json-pointer.json (renamed from tests/draft7/optional/format/relative-json-pointer.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/time.json (renamed from tests/draft7/optional/format/time.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri-reference.json (renamed from tests/draft7/optional/format/uri-reference.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri-template.json (renamed from tests/draft7/optional/format/uri-template.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/format/uri.json (renamed from tests/draft7/optional/format/uri.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/optional/zeroTerminatedFloats.json (renamed from tests/draft7/optional/zeroTerminatedFloats.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/pattern.json (renamed from tests/draft7/pattern.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/patternProperties.json (renamed from tests/draft7/patternProperties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/properties.json (renamed from tests/draft7/properties.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/propertyNames.json (renamed from tests/draft7/propertyNames.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/ref.json (renamed from tests/draft7/ref.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/refRemote.json (renamed from tests/draft7/refRemote.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/required.json (renamed from tests/draft7/required.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/type.json (renamed from tests/draft7/type.json) | 0 | ||||
-rw-r--r-- | json/tests/draft7/uniqueItems.json (renamed from tests/draft7/uniqueItems.json) | 0 | ||||
-rw-r--r-- | json/tox.ini | 8 | ||||
-rw-r--r-- | jsonschema/__init__.py | 33 | ||||
-rw-r--r-- | jsonschema/__main__.py | 2 | ||||
-rw-r--r-- | jsonschema/_format.py | 402 | ||||
-rw-r--r-- | jsonschema/_legacy_validators.py | 141 | ||||
-rw-r--r-- | jsonschema/_reflect.py | 155 | ||||
-rw-r--r-- | jsonschema/_types.py | 188 | ||||
-rw-r--r-- | jsonschema/_utils.py | 217 | ||||
-rw-r--r-- | jsonschema/_validators.py | 361 | ||||
-rw-r--r-- | jsonschema/benchmarks/__init__.py | 0 | ||||
-rw-r--r-- | jsonschema/benchmarks/issue232.py | 25 | ||||
-rw-r--r-- | jsonschema/benchmarks/issue232/issue.json | 2653 | ||||
-rw-r--r-- | jsonschema/benchmarks/json_schema_test_suite.py | 14 | ||||
-rw-r--r-- | jsonschema/cli.py | 81 | ||||
-rw-r--r-- | jsonschema/compat.py | 65 | ||||
-rw-r--r-- | jsonschema/exceptions.py | 300 | ||||
-rw-r--r-- | jsonschema/schemas/draft3.json | 199 | ||||
-rw-r--r-- | jsonschema/schemas/draft4.json | 222 | ||||
-rw-r--r-- | jsonschema/schemas/draft6.json | 153 | ||||
-rw-r--r-- | jsonschema/schemas/draft7.json | 166 | ||||
-rw-r--r-- | jsonschema/tests/__init__.py | 0 | ||||
-rw-r--r-- | jsonschema/tests/_suite.py | 237 | ||||
-rw-r--r-- | jsonschema/tests/test_cli.py | 141 | ||||
-rw-r--r-- | jsonschema/tests/test_exceptions.py | 468 | ||||
-rw-r--r-- | jsonschema/tests/test_format.py | 80 | ||||
-rw-r--r-- | jsonschema/tests/test_jsonschema_test_suite.py | 202 | ||||
-rw-r--r-- | jsonschema/tests/test_types.py | 190 | ||||
-rw-r--r-- | jsonschema/tests/test_validators.py | 1786 | ||||
-rw-r--r-- | jsonschema/validators.py | 935 | ||||
-rw-r--r-- | setup.cfg | 56 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | test-requirements.txt | 1 | ||||
-rw-r--r-- | tox.ini | 141 |
219 files changed, 11983 insertions, 9 deletions
diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..fd2e57c --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,40 @@ +build: false +environment: + VENV: "%APPVEYOR_BUILD_FOLDER%\\venv" + + matrix: + - TOXENV: py27-tests + PYTHON: "C:\\Python27" + + - TOXENV: py27-tests + PYTHON: "C:\\Python27-x64" + + - TOXENV: py35-tests + PYTHON: "C:\\Python35" + + - TOXENV: py35-tests + PYTHON: "C:\\Python35-x64" + + - TOXENV: py36-tests + PYTHON: "C:\\Python36" + + - TOXENV: py36-tests + PYTHON: "C:\\Python36-x64" + # Twisted currently failing to install. + # + # - TOXENV: py37-tests + # PYTHON: "C:\\Python37" + # + # - TOXENV: py37-tests + # PYTHON: "C:\\Python37-x64" + +init: + - echo "TOXENV - %TOXENV%" + +install: + - ps: Update-AppveyorBuild -Version "v$(python setup.py --version) b$Env:APPVEYOR_BUILD_NUMBER" + - virtualenv -p "%PYTHON%\\python.exe" "%VENV%" + - "%VENV%\\Scripts\\pip install tox" + +test_script: + - "%VENV%\\Scripts\\tox" diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..0294caf --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +# vim: filetype=dosini: +[run] +branch = True +source = jsonschema +omit = */jsonschema/_reflect.py,*/jsonschema/__main__.py @@ -1 +1,5 @@ +_cache +_static +_templates + TODO diff --git a/.travis.yml b/.travis.yml index 9c50823..b0ecbec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,30 @@ +sudo: false + language: python -python: "2.7" -node_js: "9" + +dist: xenial + +python: + - 2.7 + - 3.5 + - 3.6 + - 3.7 + - pypy2.7-6.0 + - pypy3.5-6.0 + install: - - pip install tox - - npm install + - pip install tox-travis + script: - tox - - npm test + +after_success: + - tox -e codecov + +addons: + apt: + packages: + - libenchant-dev + +git: + depth: false diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..7c24e95 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,166 @@ +v3.0.0 +------ + +* Support for Draft 6 and Draft 7 +* Draft 7 is now the default +* New ``TypeChecker`` object for more complex type definitions (and overrides) +* Falling back to isodate for the date-time format checker is no longer + attempted, in accordance with the specification + +v2.6.0 +------ + +* Support for Python 2.6 has been dropped. +* Improve a few error messages for ``uniqueItems`` (#224) and + ``additionalProperties`` (#317) +* Fix an issue with ``ErrorTree``'s handling of multiple errors (#288) + +v2.5.0 +------ + +* Improved performance on CPython by adding caching around ref resolution + (#203) + +v2.4.0 +------ + +* Added a CLI (#134) +* Added absolute path and absolute schema path to errors (#120) +* Added ``relevance`` +* Meta-schemas are now loaded via ``pkgutil`` + +v2.3.0 +------ + +* Added ``by_relevance`` and ``best_match`` (#91) +* Fixed ``format`` to allow adding formats for non-strings (#125) +* Fixed the ``uri`` format to reject URI references (#131) + +v2.2.0 +------ + +* Compile the host name regex (#127) +* Allow arbitrary objects to be types (#129) + +v2.1.0 +------ + +* Support RFC 3339 datetimes in conformance with the spec +* Fixed error paths for additionalItems + items (#122) +* Fixed wording for min / maxProperties (#117) + + +v2.0.0 +------ + +* Added ``create`` and ``extend`` to ``jsonschema.validators`` +* Removed ``ValidatorMixin`` +* Fixed array indices ref resolution (#95) +* Fixed unknown scheme defragmenting and handling (#102) + + +v1.3.0 +------ + +* Better error tracebacks (#83) +* Raise exceptions in ``ErrorTree``\s for keys not in the instance (#92) +* __cause__ (#93) + + +v1.2.0 +------ + +* More attributes for ValidationError (#86) +* Added ``ValidatorMixin.descend`` +* Fixed bad ``RefResolutionError`` message (#82) + + +v1.1.0 +------ + +* Canonicalize URIs (#70) +* Allow attaching exceptions to ``format`` errors (#77) + + +v1.0.0 +------ + +* Support for Draft 4 +* Support for format +* Longs are ints too! +* Fixed a number of issues with ``$ref`` support (#66) +* Draft4Validator is now the default +* ``ValidationError.path`` is now in sequential order +* Added ``ValidatorMixin`` + + +v0.8.0 +------ + +* Full support for JSON References +* ``validates`` for registering new validators +* Documentation +* Bugfixes + + * uniqueItems not so unique (#34) + * Improper any (#47) + + +v0.7 +---- + +* Partial support for (JSON Pointer) ``$ref`` +* Deprecations + + * ``Validator`` is replaced by ``Draft3Validator`` with a slightly different + interface + * ``validator(meta_validate=False)`` + + +v0.6 +---- + +* Bugfixes + + * Issue #30 - Wrong behavior for the dependencies property validation + * Fix a miswritten test + + +v0.5 +---- + +* Bugfixes + + * Issue #17 - require path for error objects + * Issue #18 - multiple type validation for non-objects + + +v0.4 +---- + +* Preliminary support for programmatic access to error details (Issue #5). + There are certainly some corner cases that don't do the right thing yet, but + this works mostly. + + In order to make this happen (and also to clean things up a bit), a number + of deprecations are necessary: + + * ``stop_on_error`` is deprecated in ``Validator.__init__``. Use + ``Validator.iter_errors()`` instead. + * ``number_types`` and ``string_types`` are deprecated there as well. + Use ``types={"number" : ..., "string" : ...}`` instead. + * ``meta_validate`` is also deprecated, and instead is now accepted as + an argument to ``validate``, ``iter_errors`` and ``is_valid``. + +* A bugfix or two + + +v0.3 +---- + +* Default for unknown types and properties is now to *not* error (consistent + with the schema). +* Python 3 support +* Removed dependency on SecureTypes now that the hash bug has been resolved. +* "Numerous bug fixes" -- most notably, a divisibleBy error for floats and a + bunch of missing typechecks for irrelevant properties. @@ -0,0 +1,19 @@ +Copyright (c) 2013 Julian Berman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a951c8a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include *.rst +include COPYING +include tox.ini +recursive-include json * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c776456 --- /dev/null +++ b/README.rst @@ -0,0 +1,154 @@ +========== +jsonschema +========== + +|PyPI| |Pythons| |Travis| |AppVeyor| |ReadTheDocs| + +.. |PyPI| image:: https://img.shields.io/pypi/v/jsonschema.svg + :alt: PyPI version + :target: https://pypi.org/project/jsonschema/ + +.. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonschema.svg + :alt: Supported Python versions + :target: https://pypi.org/project/jsonschema/ + +.. |Travis| image:: https://travis-ci.org/Julian/jsonschema.svg?branch=master + :alt: Travis build status + :target: https://travis-ci.org/Julian/jsonschema + +.. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/adtt0aiaihy6muyn?svg=true + :alt: AppVeyor build status + :target: https://ci.appveyor.com/project/Julian/jsonschema + +.. |ReadTheDocs| image:: https://readthedocs.org/projects/python-jsonschema/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://python-jsonschema.readthedocs.io/en/stable/ + + +``jsonschema`` is an implementation of `JSON Schema <https://json-schema.org>`_ +for Python (supporting 2.7+ including Python 3). + +.. code-block:: python + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate(instance={"name" : "Eggs", "price" : 34.99}, schema=schema) + + >>> validate( + ... instance={"name" : "Eggs", "price" : "Invalid"}, schema=schema, + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + +It can also be used from console: + +.. code-block:: bash + + $ jsonschema -i sample.json sample.schema + +Features +-------- + +* Full support for + `Draft 7 <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.Draft7Validator>`_, + `Draft 6 <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.Draft6Validator>`_, + `Draft 4 <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.Draft4Validator>`_ + and + `Draft 3 <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.Draft3Validator>`_ + +* `Lazy validation <https://python-jsonschema.readthedocs.io/en/latest/validate/#jsonschema.IValidator.iter_errors>`_ + that can iteratively report *all* validation errors. + +* `Programmatic querying <https://python-jsonschema.readthedocs.io/en/latest/errors/#module-jsonschema>`_ + of which properties or items failed validation. + + +Installation +------------ + +``jsonschema`` is available on `PyPI <https://pypi.org/project/jsonschema/>`_. You can install using `pip <https://pip.pypa.io/en/stable/>`_: + +.. code-block:: bash + + $ pip install jsonschema + + +Release Notes +------------- + +Version 3.0 brings support for Draft 7 (and 6). The interface for redefining +types has also been majorly overhauled to support easier redefinition of the +types a Validator will accept or allow. + +jsonschema is also now tested under Windows via AppVeyor. + +Thanks to all who contributed pull requests along the way. + + +Running the Test Suite +---------------------- + +If you have ``tox`` installed (perhaps via ``pip install tox`` or your +package manager), running ``tox`` in the directory of your source +checkout will run ``jsonschema``'s test suite on all of the versions +of Python ``jsonschema`` supports. If you don't have all of the +versions that ``jsonschema`` is tested under, you'll likely want to run +using ``tox``'s ``--skip-missing-interpreters`` option. + +Of course you're also free to just run the tests on a single version with your +favorite test runner. The tests live in the ``jsonschema.tests`` package. + + +Benchmarks +---------- + +``jsonschema``'s benchmarks make use of `perf <https://perf.readthedocs.io>`_. + +Running them can be done via ``tox -e perf``, or by invoking the ``perf`` +commands externally (after ensuring that both it and ``jsonschema`` itself are +installed):: + + $ python -m perf jsonschema/benchmarks/test_suite.py --hist --output results.json + +To compare to a previous run, use:: + + $ python -m perf compare_to --table reference.json results.json + +See the ``perf`` documentation for more details. + + +Community +--------- + +There's a `mailing list <https://groups.google.com/forum/#!forum/jsonschema>`_ +for this implementation on Google Groups. + +Please join, and feel free to send questions there. + + +Contributing +------------ + +I'm Julian Berman. + +``jsonschema`` is on `GitHub <https://github.com/Julian/jsonschema>`_. + +Get in touch, via GitHub or otherwise, if you've got something to contribute, +it'd be most welcome! + +You can also generally find me on Freenode (nick: ``tos9``) in various +channels, including ``#python``. + +If you feel overwhelmingly grateful, you can woo me with beer money via +Google Pay with the email in my GitHub profile. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..a370000 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,11 @@ +coverage: + precision: 2 + round: down + status: + patch: + default: + target: 100% + +comment: + layout: "header, diff, uncovered" + behavior: default diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..f6315df --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,227 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +PYTHON = python +PAPER = +BUILDDIR = _build +SOURCEDIR = $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SOURCEDIR) +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " spelling to run a spell check of the documentation" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(PYTHON) -m sphinx -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(PYTHON) -m sphinx -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(PYTHON) -m sphinx -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: json +json: + $(PYTHON) -m sphinx -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(PYTHON) -m sphinx -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(PYTHON) -m sphinx -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jsonschema.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jsonschema.qhc" + +.PHONY: applehelp +applehelp: + $(PYTHON) -m sphinx -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(PYTHON) -m sphinx -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/jsonschema" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jsonschema" + @echo "# devhelp" + +.PHONY: epub +epub: + $(PYTHON) -m sphinx -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(PYTHON) -m sphinx -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(PYTHON) -m sphinx -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(PYTHON) -m sphinx -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(PYTHON) -m sphinx -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(PYTHON) -m sphinx -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(PYTHON) -m sphinx -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(PYTHON) -m sphinx -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(PYTHON) -m sphinx -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(PYTHON) -m sphinx -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(PYTHON) -m sphinx -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(PYTHON) -m sphinx -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(PYTHON) -m sphinx -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(PYTHON) -m sphinx -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: spelling +spelling: + $(PYTHON) -m sphinx -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling + @echo + @echo "Build finished. The spelling files are in $(BUILDDIR)/spelling." + +.PHONY: dummy +dummy: + $(PYTHON) -m sphinx -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c7126f2 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its containing dir. + +from textwrap import dedent +import os +import sys + +import jsonschema + + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +ext_paths = [os.path.abspath(os.path.pardir), os.path.dirname(__file__)] +sys.path = ext_paths + sys.path + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = "1.0" + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named "sphinx.ext.*") or your custom ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinxcontrib.spelling", + "jsonschema_role", +] + +cache_path = "_cache" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = "utf-8-sig" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +project = u"jsonschema" +author = u"Julian Berman" +copyright = u"2013, " + author + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# version: The short X.Y version +# release: The full version, including alpha/beta/rc tags. +release = jsonschema.__version__ +version = release.partition("-")[0] + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = "" +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = "%B %d, %Y" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build", "_cache", "_static", "_templates"] + +# The reST default role (used for this markup: `text`) to use for all documents. +default_role = "any" + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +doctest_global_setup = dedent(""" + from __future__ import print_function + from jsonschema import * +""") + +intersphinx_mapping = { + "python": ("https://docs.python.org/2.7", None), + "python3": ("https://docs.python.org/3", None), +} + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "pyramid" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ["_static"] + +# If not "", a "Last updated on:" timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = "%b %d, %Y" + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = "" + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = "jsonschemadoc" + + +# -- Options for LaTeX output -------------------------------------------------- + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ( + "index", + "jsonschema.tex", + u"jsonschema Documentation", + author, + "manual", + ), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ( + "index", + "jsonschema", + u"jsonschema Documentation", + [author], + 1, + ), +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + "index", + "jsonschema", + u"jsonschema Documentation", + author, + "jsonschema", + "One line description of project.", + "Miscellaneous", + ), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: "footnote", "no", or "inline". +# texinfo_show_urls = "footnote" + +# -- Options for sphinxcontrib-spelling ----------------------------------- + +spelling_word_list_filename = "spelling-wordlist.txt" diff --git a/docs/creating.rst b/docs/creating.rst new file mode 100644 index 0000000..a9f18d1 --- /dev/null +++ b/docs/creating.rst @@ -0,0 +1,25 @@ +.. currentmodule:: jsonschema.validators + +.. _creating-validators: + +======================================= +Creating or Extending Validator Classes +======================================= + +.. autofunction:: create + +.. autofunction:: extend + +.. autofunction:: validator_for + +.. autofunction:: validates + + +Creating Validation Errors +-------------------------- + +Any validating function that validates against a subschema should call +``descend``, rather than ``iter_errors``. If it recurses into the +instance, or schema, it should pass one or both of the ``path`` or +``schema_path`` arguments to ``descend`` in order to properly maintain +where in the instance or schema respectively the error occurred. diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000..708425f --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,445 @@ +========================== +Handling Validation Errors +========================== + +.. currentmodule:: jsonschema.exceptions + +When an invalid instance is encountered, a `ValidationError` will be +raised or returned, depending on which method or function is used. + +.. autoexception:: ValidationError + + The instance didn't properly validate under the provided schema. + + The information carried by an error roughly breaks down into: + + =============== ================= ======================== + What Happened Why Did It Happen What Was Being Validated + =============== ================= ======================== + `message` `context` `instance` + + `cause` `path` + + `schema` + + `schema_path` + + `validator` + + `validator_value` + =============== ================= ======================== + + + .. attribute:: message + + A human readable message explaining the error. + + .. attribute:: validator + + The name of the failed `validator + <https://json-schema.org/draft-04/json-schema-validation.html#rfc.section.5>`_. + + .. attribute:: validator_value + + The value for the failed validator in the schema. + + .. attribute:: schema + + The full schema that this error came from. This is potentially a + subschema from within the schema that was passed in originally, + or even an entirely different schema if a :validator:`$ref` was + followed. + + .. attribute:: relative_schema_path + + A `collections.deque` containing the path to the failed + validator within the schema. + + .. attribute:: absolute_schema_path + + A `collections.deque` containing the path to the failed + validator within the schema, but always relative to the + *original* schema as opposed to any subschema (i.e. the one + originally passed into a validator class, *not* `schema`\). + + .. attribute:: schema_path + + Same as `relative_schema_path`. + + .. attribute:: relative_path + + A `collections.deque` containing the path to the + offending element within the instance. The deque can be empty if + the error happened at the root of the instance. + + .. attribute:: absolute_path + + A `collections.deque` containing the path to the + offending element within the instance. The absolute path + is always relative to the *original* instance that was + validated (i.e. the one passed into a validation method, *not* + `instance`\). The deque can be empty if the error happened + at the root of the instance. + + .. attribute:: path + + Same as `relative_path`. + + .. attribute:: instance + + The instance that was being validated. This will differ from + the instance originally passed into ``validate`` if the + validator object was in the process of validating a (possibly + nested) element within the top-level instance. The path within + the top-level instance (i.e. `ValidationError.path`) could + be used to find this object, but it is provided for convenience. + + .. attribute:: context + + If the error was caused by errors in subschemas, the list of errors + from the subschemas will be available on this property. The + `schema_path` and `path` of these errors will be relative + to the parent error. + + .. attribute:: cause + + If the error was caused by a *non*-validation error, the + exception object will be here. Currently this is only used + for the exception raised by a failed format checker in + `jsonschema.FormatChecker.check`. + + .. attribute:: parent + + A validation error which this error is the `context` of. + ``None`` if there wasn't one. + + +In case an invalid schema itself is encountered, a `SchemaError` is +raised. + +.. autoexception:: SchemaError + + The provided schema is malformed. + + The same attributes are present as for `ValidationError`\s. + + +These attributes can be clarified with a short example: + +.. testcode:: + + schema = { + "items": { + "anyOf": [ + {"type": "string", "maxLength": 2}, + {"type": "integer", "minimum": 5} + ] + } + } + instance = [{}, 3, "foo"] + v = Draft7Validator(schema) + errors = sorted(v.iter_errors(instance), key=lambda e: e.path) + +The error messages in this situation are not very helpful on their own. + +.. testcode:: + + for error in errors: + print(error.message) + +outputs: + +.. testoutput:: + + {} is not valid under any of the given schemas + 3 is not valid under any of the given schemas + 'foo' is not valid under any of the given schemas + +If we look at `ValidationError.path` on each of the errors, we can find +out which elements in the instance correspond to each of the errors. In +this example, `ValidationError.path` will have only one element, which +will be the index in our list. + +.. testcode:: + + for error in errors: + print(list(error.path)) + +.. testoutput:: + + [0] + [1] + [2] + +Since our schema contained nested subschemas, it can be helpful to look at +the specific part of the instance and subschema that caused each of the errors. +This can be seen with the `ValidationError.instance` and +`ValidationError.schema` attributes. + +With validators like :validator:`anyOf`, the `ValidationError.context` +attribute can be used to see the sub-errors which caused the failure. Since +these errors actually came from two separate subschemas, it can be helpful to +look at the `ValidationError.schema_path` attribute as well to see where +exactly in the schema each of these errors come from. In the case of sub-errors +from the `ValidationError.context` attribute, this path will be relative +to the `ValidationError.schema_path` of the parent error. + +.. testcode:: + + for error in errors: + for suberror in sorted(error.context, key=lambda e: e.schema_path): + print(list(suberror.schema_path), suberror.message, sep=", ") + +.. testoutput:: + + [0, 'type'], {} is not of type 'string' + [1, 'type'], {} is not of type 'integer' + [0, 'type'], 3 is not of type 'string' + [1, 'minimum'], 3 is less than the minimum of 5 + [0, 'maxLength'], 'foo' is too long + [1, 'type'], 'foo' is not of type 'integer' + +The string representation of an error combines some of these attributes for +easier debugging. + +.. testcode:: + + print(errors[1]) + +.. testoutput:: + + 3 is not valid under any of the given schemas + + Failed validating 'anyOf' in schema['items']: + {'anyOf': [{'maxLength': 2, 'type': 'string'}, + {'minimum': 5, 'type': 'integer'}]} + + On instance[1]: + 3 + + +ErrorTrees +---------- + +If you want to programmatically be able to query which properties or validators +failed when validating a given instance, you probably will want to do so using +`jsonschema.exceptions.ErrorTree` objects. + +.. autoclass:: jsonschema.exceptions.ErrorTree + :members: + :special-members: + :exclude-members: __dict__,__weakref__ + + .. attribute:: errors + + The mapping of validator names to the error objects (usually + `jsonschema.exceptions.ValidationError`\s) at this level + of the tree. + +Consider the following example: + +.. testcode:: + + schema = { + "type" : "array", + "items" : {"type" : "number", "enum" : [1, 2, 3]}, + "minItems" : 3, + } + instance = ["spam", 2] + +For clarity's sake, the given instance has three errors under this schema: + +.. testcode:: + + v = Draft3Validator(schema) + for error in sorted(v.iter_errors(["spam", 2]), key=str): + print(error.message) + +.. testoutput:: + + 'spam' is not of type 'number' + 'spam' is not one of [1, 2, 3] + ['spam', 2] is too short + +Let's construct an `jsonschema.exceptions.ErrorTree` so that we +can query the errors a bit more easily than by just iterating over the +error objects. + +.. testcode:: + + tree = ErrorTree(v.iter_errors(instance)) + +As you can see, `jsonschema.exceptions.ErrorTree` takes an +iterable of `ValidationError`\s when constructing a tree so +you can directly pass it the return value of a validator object's +`jsonschema.IValidator.iter_errors` method. + +`ErrorTree`\s support a number of useful operations. The first one we +might want to perform is to check whether a given element in our instance +failed validation. We do so using the :keyword:`in` operator: + +.. doctest:: + + >>> 0 in tree + True + + >>> 1 in tree + False + +The interpretation here is that the 0th index into the instance (``"spam"``) +did have an error (in fact it had 2), while the 1th index (``2``) did not (i.e. +it was valid). + +If we want to see which errors a child had, we index into the tree and look at +the `ErrorTree.errors` attribute. + +.. doctest:: + + >>> sorted(tree[0].errors) + ['enum', 'type'] + +Here we see that the :validator:`enum` and :validator:`type` validators failed +for index ``0``. In fact `ErrorTree.errors` is a dict, whose values are +the `ValidationError`\s, so we can get at those directly if we want +them. + +.. doctest:: + + >>> print(tree[0].errors["type"].message) + 'spam' is not of type 'number' + +Of course this means that if we want to know if a given named +validator failed for a given index, we check for its presence in +`ErrorTree.errors`: + +.. doctest:: + + >>> "enum" in tree[0].errors + True + + >>> "minimum" in tree[0].errors + False + +Finally, if you were paying close enough attention, you'll notice that we +haven't seen our :validator:`minItems` error appear anywhere yet. This is +because :validator:`minItems` is an error that applies globally to the instance +itself. So it appears in the root node of the tree. + +.. doctest:: + + >>> "minItems" in tree.errors + True + +That's all you need to know to use error trees. + +To summarize, each tree contains child trees that can be accessed by +indexing the tree to get the corresponding child tree for a given index +into the instance. Each tree and child has a `ErrorTree.errors` +attribute, a dict, that maps the failed validator name to the +corresponding validation error. + + +best_match and relevance +------------------------ + +The `best_match` function is a simple but useful function for attempting +to guess the most relevant error in a given bunch. + +.. doctest:: + + >>> from jsonschema import Draft7Validator + >>> from jsonschema.exceptions import best_match + + >>> schema = { + ... "type": "array", + ... "minItems": 3, + ... } + >>> print(best_match(Draft7Validator(schema).iter_errors(11)).message) + 11 is not of type 'array' + + +.. autofunction:: best_match + + Try to find an error that appears to be the best match among given errors. + + In general, errors that are higher up in the instance (i.e. for which + `ValidationError.path` is shorter) are considered better matches, + since they indicate "more" is wrong with the instance. + + If the resulting match is either :validator:`oneOf` or :validator:`anyOf`, + the *opposite* assumption is made -- i.e. the deepest error is picked, + since these validators only need to match once, and any other errors may + not be relevant. + + :argument collections.Iterable errors: the errors to select from. Do not + provide a mixture of errors from different validation attempts + (i.e. from different instances or schemas), since it won't + produce sensical output. + :argument callable key: the key to use when sorting errors. See + `relevance` and transitively `by_relevance` for more + details (the default is to sort with the defaults of that function). + Changing the default is only useful if you want to change the function + that rates errors but still want the error context descent done by + this function. + + Returns: + + the best matching error, or ``None`` if the iterable was empty + + .. note:: + + This function is a heuristic. Its return value may change for a given + set of inputs from version to version if better heuristics are added. + + +.. function:: relevance(validation_error) + + A key function that sorts errors based on heuristic relevance. + + If you want to sort a bunch of errors entirely, you can use + this function to do so. Using this function as a key to e.g. + `sorted` or `max` will cause more relevant errors to be + considered greater than less relevant ones. + + Within the different validators that can fail, this function + considers :validator:`anyOf` and :validator:`oneOf` to be *weak* + validation errors, and will sort them lower than other validators at + the same level in the instance. + + If you want to change the set of weak [or strong] validators you can create + a custom version of this function with `by_relevance` and provide a + different set of each. + +.. doctest:: + + >>> schema = { + ... "properties": { + ... "name": {"type": "string"}, + ... "phones": { + ... "properties": { + ... "home": {"type": "string"} + ... }, + ... }, + ... }, + ... } + >>> instance = {"name": 123, "phones": {"home": [123]}} + >>> errors = Draft7Validator(schema).iter_errors(instance) + >>> [ + ... e.path[-1] + ... for e in sorted(errors, key=exceptions.relevance) + ... ] + ['home', 'name'] + + +.. autofunction:: by_relevance + + Create a key function that can be used to sort errors by relevance. + + :argument set weak: a collection of validator names to consider to + be "weak". If there are two errors at the same level of the + instance and one is in the set of weak validator names, the + other error will take priority. By default, :validator:`anyOf` + and :validator:`oneOf` are considered weak validators and will + be superseded by other same-level validation errors. + :argument set strong: a collection of validator names to consider to + be "strong" diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..dbfcbb5 --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,148 @@ +========================== +Frequently Asked Questions +========================== + + +Why doesn't my schema's default property set the default on my instance? +------------------------------------------------------------------------ + +The basic answer is that the specification does not require that +:validator:`default` actually do anything. + +For an inkling as to *why* it doesn't actually do anything, consider that none +of the other validators modify the instance either. More importantly, having +:validator:`default` modify the instance can produce quite peculiar things. +It's perfectly valid (and perhaps even useful) to have a default that is not +valid under the schema it lives in! So an instance modified by the default +would pass validation the first time, but fail the second! + +Still, filling in defaults is a thing that is useful. `jsonschema` allows +you to `define your own validator classes and callables <creating>`, so you can +easily create an `jsonschema.IValidator` that does do default setting. Here's +some code to get you started. (In this code, we add the default properties to +each object *before* the properties are validated, so the default values +themselves will need to be valid under the schema.) + + .. code-block:: python + + from jsonschema import Draft7Validator, validators + + + def extend_with_default(validator_class): + validate_properties = validator_class.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + for property, subschema in properties.iteritems(): + if "default" in subschema: + instance.setdefault(property, subschema["default"]) + + for error in validate_properties( + validator, properties, instance, schema, + ): + yield error + + return validators.extend( + validator_class, {"properties" : set_defaults}, + ) + + + DefaultValidatingDraft7Validator = extend_with_default(Draft7Validator) + + + # Example usage: + obj = {} + schema = {'properties': {'foo': {'default': 'bar'}}} + # Note jsonschem.validate(obj, schema, cls=DefaultValidatingDraft7Validator) + # will not work because the metaschema contains `default` directives. + DefaultValidatingDraft7Validator(schema).validate(obj) + assert obj == {'foo': 'bar'} + + +See the above-linked document for more info on how this works, but +basically, it just extends the :validator:`properties` validator on a +`jsonschema.Draft7Validator` to then go ahead and update all the +defaults. + +.. note:: + + If you're interested in a more interesting solution to a larger class of these + types of transformations, keep an eye on `Seep + <https://github.com/Julian/Seep>`_, which is an experimental data + transformation and extraction library written on top of `jsonschema`. + + +.. hint:: + + The above code can provide default values for an entire object and all of its properties, + but only if your schema provides a default value for the object itself, like so: + + .. code-block:: python + + schema = { + "type": "object", + "properties": { + "outer-object": { + "type": "object", + "properties" : { + "inner-object": { + "type": "string", + "default": "INNER-DEFAULT" + } + }, + "default": {} # <-- MUST PROVIDE DEFAULT OBJECT + } + } + } + + obj = {} + DefaultValidatingDraft7Validator(schema).validate(obj) + assert obj == {'outer-object': {'inner-object': 'INNER-DEFAULT'}} + + ...but if you don't provide a default value for your object, + then it won't be instantiated at all, much less populated with default properties. + + .. code-block:: python + + del schema["properties"]["outer-object"]["default"] + obj2 = {} + DefaultValidatingDraft7Validator(schema).validate(obj2) + assert obj2 == {} # whoops + + +How do jsonschema version numbers work? +--------------------------------------- + +``jsonschema`` tries to follow the `Semantic Versioning <https://semver.org/>`_ +specification. + +This means broadly that no backwards-incompatible changes should be made in +minor releases (and certainly not in dot releases). + +The full picture requires defining what constitutes a backwards-incompatible +change. + +The following are simple examples of things considered public API, and +therefore should *not* be changed without bumping a major version number: + + * module names and contents, when not marked private by Python convention + (a single leading underscore) + + * function and object signature (parameter order and name) + +The following are *not* considered public API and may change without notice: + + * the exact wording and contents of error messages; typical + reasons to do this seem to involve unit tests. API users are + encouraged to use the extensive introspection provided in + `jsonschema.exceptions.ValidationError`\s instead to make + meaningful assertions about what failed. + + * the order in which validation errors are returned or raised + + * the ``compat.py`` module, which is for internal compatibility use + + * anything marked private + +With the exception of the last two of those, flippant changes are avoided, but +changes can and will be made if there is improvement to be had. Feel free to +open an issue ticket if there is a specific issue or question worth raising. diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..1fd4ba6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,56 @@ +========== +jsonschema +========== + + +.. module:: jsonschema + + +``jsonschema`` is an implementation of `JSON Schema <https://json-schema.org>`_ +for Python (supporting 2.7+ including Python 3). + +.. code-block:: python + + >>> from jsonschema import validate + + >>> # A sample schema, like what we'd get from json.load() + >>> schema = { + ... "type" : "object", + ... "properties" : { + ... "price" : {"type" : "number"}, + ... "name" : {"type" : "string"}, + ... }, + ... } + + >>> # If no exception is raised by validate(), the instance is valid. + >>> validate(instance={"name" : "Eggs", "price" : 34.99}, schema=schema) + + >>> validate( + ... instance={"name" : "Eggs", "price" : "Invalid"}, schema=schema, + ... ) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValidationError: 'Invalid' is not of type 'number' + + +You can find further information (installation instructions, mailing list) +as well as the source code and issue tracker on our +`GitHub page <https://github.com/Julian/jsonschema/>`__. + +Contents: + +.. toctree:: + :maxdepth: 2 + + validate + errors + references + creating + faq + + +Indices and tables +================== + +* `genindex` +* `search` diff --git a/docs/jsonschema_role.py b/docs/jsonschema_role.py new file mode 100644 index 0000000..607f8de --- /dev/null +++ b/docs/jsonschema_role.py @@ -0,0 +1,151 @@ +from datetime import datetime +from docutils import nodes +import errno +import os + +try: + import urllib2 as urllib +except ImportError: + import urllib.request as urllib + +import certifi +from lxml import html + + +VALIDATION_SPEC = "https://json-schema.org/draft-04/json-schema-validation.html" + + +def setup(app): + """ + Install the plugin. + + Arguments: + + app (sphinx.application.Sphinx): + + the Sphinx application context + + """ + + app.add_config_value("cache_path", "_cache", "") + + try: + os.makedirs(app.config.cache_path) + except OSError as error: + if error.errno != errno.EEXIST: + raise + + path = os.path.join(app.config.cache_path, "spec.html") + spec = fetch_or_load(path) + app.add_role("validator", docutils_sucks(spec)) + + +def fetch_or_load(spec_path): + """ + Fetch a new specification or use the cache if it's current. + + Arguments: + + cache_path: + + the path to a cached specification + + """ + + headers = {} + + try: + modified = datetime.utcfromtimestamp(os.path.getmtime(spec_path)) + date = modified.strftime("%a, %d %b %Y %I:%M:%S UTC") + headers["If-Modified-Since"] = date + except OSError as error: + if error.errno != errno.ENOENT: + raise + + request = urllib.Request(VALIDATION_SPEC, headers=headers) + response = urllib.urlopen(request, cafile=certifi.where()) + + if response.code == 200: + with open(spec_path, "w+b") as spec: + spec.writelines(response) + spec.seek(0) + return html.parse(spec) + + with open(spec_path) as spec: + return html.parse(spec) + + +def docutils_sucks(spec): + """ + Yeah. + + It doesn't allow using a class because it does stupid stuff like try to set + attributes on the callable object rather than just keeping a dict. + + """ + + base_url = VALIDATION_SPEC + ref_url = "https://json-schema.org/draft-04/json-schema-core.html#rfc.section.4.1" + schema_url = "https://json-schema.org/draft-04/json-schema-core.html#rfc.section.6" + + def validator(name, raw_text, text, lineno, inliner): + """ + Link to the JSON Schema documentation for a validator. + + Arguments: + + name (str): + + the name of the role in the document + + raw_source (str): + + the raw text (role with argument) + + text (str): + + the argument given to the role + + lineno (int): + + the line number + + inliner (docutils.parsers.rst.states.Inliner): + + the inliner + + Returns: + + tuple: + + a 2-tuple of nodes to insert into the document and an + iterable of system messages, both possibly empty + + """ + + if text == "$ref": + return [nodes.reference(raw_text, text, refuri=ref_url)], [] + elif text == "$schema": + return [nodes.reference(raw_text, text, refuri=schema_url)], [] + + # find the header in the validation spec containing matching text + header = spec.xpath("//h1[contains(text(), '{0}')]".format(text)) + + if len(header) == 0: + inliner.reporter.warning( + "Didn't find a target for {0}".format(text), + ) + uri = base_url + else: + if len(header) > 1: + inliner.reporter.info( + "Found multiple targets for {0}".format(text), + ) + + # get the href from link in the header + uri = base_url + header[0].find('a').attrib["href"] + + reference = nodes.reference(raw_text, text, refuri=uri) + return [reference], [] + + return validator diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..fcb914f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^<target^>` where ^<target^> is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\jsonschema.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\jsonschema.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/docs/references.rst b/docs/references.rst new file mode 100644 index 0000000..9f24299 --- /dev/null +++ b/docs/references.rst @@ -0,0 +1,13 @@ +========================= +Resolving JSON References +========================= + + +.. currentmodule:: jsonschema + +.. autoclass:: RefResolver + :members: + +.. autoexception:: RefResolutionError + + A JSON reference failed to resolve. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..eb3007a --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +certifi +lxml +sphinx==1.6.7 +sphinxcontrib-spelling diff --git a/docs/spelling-wordlist.txt b/docs/spelling-wordlist.txt new file mode 100644 index 0000000..e43b09c --- /dev/null +++ b/docs/spelling-wordlist.txt @@ -0,0 +1,30 @@ +# this appears to be misinterpreting Napoleon types as prose, sigh... +IValidator +TypeChecker +UnknownType +ValidationError + +# 0th, sigh... +th +callables +deque +hostname +implementers +indices +# ipv4/6, sigh... +ipv +iterable +jsonschema +pre +programmatically +recurses +regex +sensical +subschema +subschemas +subscopes +uri +validator +validators +versioned +schemas diff --git a/docs/validate.rst b/docs/validate.rst new file mode 100644 index 0000000..5a4448d --- /dev/null +++ b/docs/validate.rst @@ -0,0 +1,380 @@ +================= +Schema Validation +================= + + +.. currentmodule:: jsonschema + + +The Basics +---------- + +The simplest way to validate an instance under a given schema is to use the +:func:`validate` function. + +.. autofunction:: validate + +.. [#] For information on creating JSON schemas to validate + your data, there is a good introduction to JSON Schema + fundamentals underway at `Understanding JSON Schema + <https://json-schema.org/understanding-json-schema/>`_ + + +The Validator Interface +----------------------- + +`jsonschema` defines an (informal) interface that all validator +classes should adhere to. + +.. class:: IValidator(schema, types=(), resolver=None, format_checker=None) + + :argument dict schema: the schema that the validator object + will validate with. It is assumed to be valid, and providing + an invalid schema can lead to undefined behavior. See + `IValidator.check_schema` to validate a schema first. + :argument resolver: an instance of `RefResolver` that will be + used to resolve :validator:`$ref` properties (JSON references). If + unprovided, one will be created. + :argument format_checker: an instance of `FormatChecker` + whose `FormatChecker.conforms` method will be called to + check and see if instances conform to each :validator:`format` + property present in the schema. If unprovided, no validation + will be done for :validator:`format`. Certain formats require + additional packages to be installed (ipv5, uri, color, date-time). + The required packages can be found at the bottom of this page. + :argument types: + .. deprecated:: 3.0.0 + + Use `TypeChecker.redefine` and + `jsonschema.validators.extend` instead of this argument. + + See `validating-types` for details. + + If used, this overrides or extends the list of known types when + validating the :validator:`type` property. + + What is provided should map strings (type names) to class objects + that will be checked via `isinstance`. + + + .. attribute:: META_SCHEMA + + An object representing the validator's meta schema (the schema that + describes valid schemas in the given version). + + .. attribute:: VALIDATORS + + A mapping of validator names (`str`\s) to functions + that validate the validator property with that name. For more + information see `creating-validators`. + + .. attribute:: TYPE_CHECKER + + A `TypeChecker` that will be used when validating :validator:`type` + properties in JSON schemas. + + .. attribute:: schema + + The schema that was passed in when initializing the object. + + .. attribute:: DEFAULT_TYPES + + .. deprecated:: 3.0.0 + + Use of this attribute is deprecated in favor of the new `type + checkers <TypeChecker>`. + + See `validating-types` for details. + + For backwards compatibility on existing validator classes, a mapping of + JSON types to Python class objects which define the Python types for + each JSON type. + + Any existing code using this attribute should likely transition to + using `TypeChecker.is_type`. + + + .. classmethod:: check_schema(schema) + + Validate the given schema against the validator's `META_SCHEMA`. + + :raises: `jsonschema.exceptions.SchemaError` if the schema + is invalid + + .. method:: is_type(instance, type) + + Check if the instance is of the given (JSON Schema) type. + + :type type: str + :rtype: bool + :raises: `jsonschema.exceptions.UnknownType` if ``type`` + is not a known type. + + .. method:: is_valid(instance) + + Check if the instance is valid under the current `schema`. + + :rtype: bool + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).is_valid([2, 3, 4]) + False + + .. method:: iter_errors(instance) + + Lazily yield each of the validation errors in the given instance. + + :rtype: an `collections.Iterable` of + `jsonschema.exceptions.ValidationError`\s + + >>> schema = { + ... "type" : "array", + ... "items" : {"enum" : [1, 2, 3]}, + ... "maxItems" : 2, + ... } + >>> v = Draft3Validator(schema) + >>> for error in sorted(v.iter_errors([2, 3, 4]), key=str): + ... print(error.message) + 4 is not one of [1, 2, 3] + [2, 3, 4] is too long + + .. method:: validate(instance) + + Check if the instance is valid under the current `schema`. + + :raises: `jsonschema.exceptions.ValidationError` if the + instance is invalid + + >>> schema = {"maxItems" : 2} + >>> Draft3Validator(schema).validate([2, 3, 4]) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + +All of the `versioned validators <versioned-validators>` that are included with +`jsonschema` adhere to the interface, and implementers of validator classes +that extend or complement the ones included should adhere to it as well. For +more information see `creating-validators`. + +Type Checking +------------- + +To handle JSON Schema's :validator:`type` property, a `IValidator` uses +an associated `TypeChecker`. The type checker provides an immutable +mapping between names of types and functions that can test if an instance is +of that type. The defaults are suitable for most users - each of the +`versioned validators <versioned-validators>` that are included with +`jsonschema` have a `TypeChecker` that can correctly handle their respective +versions. + +.. seealso:: `validating-types` + + For an example of providing a custom type check. + +.. autoclass:: TypeChecker + :members: + +.. autoexception:: jsonschema.exceptions.UndefinedTypeCheck + + Raised when trying to remove a type check that is not known to this + TypeChecker, or when calling `jsonschema.TypeChecker.is_type` + directly. + +.. _validating-types: + +Validating With Additional Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Occasionally it can be useful to provide additional or alternate types when +validating the JSON Schema's :validator:`type` property. + +`jsonschema` tries to strike a balance between performance in the common +case and generality. For instance, JSON Schema defines a ``number`` type, which +can be validated with a schema such as ``{"type" : "number"}``. By default, +this will accept instances of Python `numbers.Number`. This includes in +particular `int`\s and `float`\s, along with +`decimal.Decimal` objects, `complex` numbers etc. For +``integer`` and ``object``, however, rather than checking for +`numbers.Integral` and `collections.abc.Mapping`, +`jsonschema` simply checks for `int` and `dict`, since the +more general instance checks can introduce significant slowdown, especially +given how common validating these types are. + +If you *do* want the generality, or just want to add a few specific additional +types as being acceptable for a validator object, then you should update an +existing `TypeChecker` or create a new one. You may then create a new +`IValidator` via `jsonschema.validators.extend`. + +.. code-block:: python + + class MyInteger(object): + pass + + def is_my_int(checker, instance): + return ( + Draft3Validator.TYPE_CHECKER.is_type(instance, "number") or + isinstance(instance, MyInteger) + ) + + type_checker = Draft3Validator.TYPE_CHECKER.redefine("number", is_my_int) + + CustomValidator = extend(Draft3Validator, type_checker=type_checker) + validator = CustomValidator(schema={"type" : "number"}) + + +.. autoexception:: jsonschema.exceptions.UnknownType + +.. _versioned-validators: + +Versioned Validators +-------------------- + +`jsonschema` ships with validator classes for various versions of +the JSON Schema specification. For details on the methods and attributes +that each validator class provides see the `IValidator` interface, +which each included validator class implements. + +.. autoclass:: Draft7Validator + +.. autoclass:: Draft6Validator + +.. autoclass:: Draft4Validator + +.. autoclass:: Draft3Validator + + +For example, if you wanted to validate a schema you created against the +Draft 6 meta-schema, you could use: + +.. code-block:: python + + from jsonschema import Draft6Validator + + schema = { + "$schema": "https://json-schema.org/schema#", + + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + }, + "required": ["email"] + } + Draft6Validator.check_schema(schema) + + +Validating Formats +------------------ + +JSON Schema defines the :validator:`format` property which can be used to check +if primitive types (``string``\s, ``number``\s, ``boolean``\s) conform to +well-defined formats. By default, no validation is enforced, but optionally, +validation can be enabled by hooking in a format-checking object into an +`IValidator`. + +.. doctest:: + + >>> validate("localhost", {"format" : "hostname"}) + >>> validate( + ... instance="-12", + ... schema={"format" : "hostname"}, + ... format_checker=draft7_format_checker, + ... ) + Traceback (most recent call last): + ... + ValidationError: "-12" is not a "hostname" + +.. autoclass:: FormatChecker + :members: + :exclude-members: cls_checks + + .. attribute:: checkers + + A mapping of currently known formats to tuple of functions that + validate them and errors that should be caught. New checkers can be + added and removed either per-instance or globally for all checkers + using the `FormatChecker.checks` or `FormatChecker.cls_checks` + decorators respectively. + + .. classmethod:: cls_checks(format, raises=()) + + Register a decorated function as *globally* validating a new format. + + Any instance created after this function is called will pick up the + supplied checker. + + :argument str format: the format that the decorated function will check + :argument Exception raises: the exception(s) raised + by the decorated function when an invalid instance is + found. The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute + of the resulting validation error. + + +.. autoexception:: FormatError + :members: + + +There are a number of default checkers that `FormatChecker`\s know how +to validate. Their names can be viewed by inspecting the +`FormatChecker.checkers` attribute. Certain checkers will only be +available if an appropriate package is available for use. The easiest way to +ensure you have what is needed is to install ``jsonschema`` using the +``format`` setuptools extra -- i.e. + +.. code-block:: sh + + $ pip install jsonschema[format] + +which will install all of the below dependencies for all formats. The +more specific list of available checkers, along with their requirement +(if any,) are listed below. + +.. note:: + + If the following packages are not installed when using a checker + that requires it, validation will succeed without throwing an error, + as specified by the JSON Schema specification. + +========================= ==================== +Checker Notes +========================= ==================== +``color`` requires webcolors_ +``date`` +``date-time`` requires strict-rfc3339_ +``email`` +``hostname`` +``idn-hostname`` requires idna_ +``ipv4`` +``ipv6`` OS must have `socket.inet_pton` function +``iri`` requires rfc3987_ +``iri-reference`` requires rfc3987_ +``json-pointer`` requires jsonpointer_ +``regex`` +``relative-json-pointer`` requires jsonpointer_ +``time`` requires strict-rfc3339_ +``uri`` requires rfc3987_ +``uri-reference`` requires rfc3987_ +========================= ==================== + + +.. _idna: https://pypi.org/pypi/idna/ +.. _jsonpointer: https://pypi.org/pypi/jsonpointer/ +.. _rfc3987: https://pypi.org/pypi/rfc3987/ +.. _rfc5322: https://tools.ietf.org/html/rfc5322#section-3.4.1 +.. _strict-rfc3339: https://pypi.org/pypi/strict-rfc3339/ +.. _webcolors: https://pypi.org/pypi/webcolors/ + + +.. note:: + + Since in most cases "validating" an email address is an attempt + instead to confirm that mail sent to it will deliver to a recipient, + and that that recipient is the correct one the email is intended + for, and since many valid email addresses are in many places + incorrectly rejected, and many invalid email addresses are in many + places incorrectly accepted, the ``email`` format validator only + provides a sanity check, not full rfc5322_ validation. + + The same applies to the ``idn-email`` format. diff --git a/json/.gitignore b/json/.gitignore new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/json/.gitignore @@ -0,0 +1 @@ +TODO diff --git a/json/.travis.yml b/json/.travis.yml new file mode 100644 index 0000000..9c50823 --- /dev/null +++ b/json/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: "2.7" +node_js: "9" +install: + - pip install tox + - npm install +script: + - tox + - npm test diff --git a/README.md b/json/README.md index 8242a44..8242a44 100644 --- a/README.md +++ b/json/README.md diff --git a/bin/jsonschema_suite b/json/bin/jsonschema_suite index 11aaa19..11aaa19 100755 --- a/bin/jsonschema_suite +++ b/json/bin/jsonschema_suite diff --git a/package.json b/json/package.json index 3980136..3980136 100644 --- a/package.json +++ b/json/package.json diff --git a/remotes/folder/folderInteger.json b/json/remotes/folder/folderInteger.json index dbe5c75..dbe5c75 100644 --- a/remotes/folder/folderInteger.json +++ b/json/remotes/folder/folderInteger.json diff --git a/remotes/integer.json b/json/remotes/integer.json index dbe5c75..dbe5c75 100644 --- a/remotes/integer.json +++ b/json/remotes/integer.json diff --git a/remotes/name.json b/json/remotes/name.json index 19ba093..19ba093 100644 --- a/remotes/name.json +++ b/json/remotes/name.json diff --git a/remotes/subSchemas.json b/json/remotes/subSchemas.json index 8b6d8f8..8b6d8f8 100644 --- a/remotes/subSchemas.json +++ b/json/remotes/subSchemas.json diff --git a/test-schema.json b/json/test-schema.json index 547c5c2..547c5c2 100644 --- a/test-schema.json +++ b/json/test-schema.json diff --git a/tests/draft3/additionalItems.json b/json/tests/draft3/additionalItems.json index 6d4bff5..6d4bff5 100644 --- a/tests/draft3/additionalItems.json +++ b/json/tests/draft3/additionalItems.json diff --git a/tests/draft3/additionalProperties.json b/json/tests/draft3/additionalProperties.json index bfb0844..bfb0844 100644 --- a/tests/draft3/additionalProperties.json +++ b/json/tests/draft3/additionalProperties.json diff --git a/tests/draft3/default.json b/json/tests/draft3/default.json index 1762977..1762977 100644 --- a/tests/draft3/default.json +++ b/json/tests/draft3/default.json diff --git a/tests/draft3/dependencies.json b/json/tests/draft3/dependencies.json index d7e0925..d7e0925 100644 --- a/tests/draft3/dependencies.json +++ b/json/tests/draft3/dependencies.json diff --git a/tests/draft3/disallow.json b/json/tests/draft3/disallow.json index a5c9d90..a5c9d90 100644 --- a/tests/draft3/disallow.json +++ b/json/tests/draft3/disallow.json diff --git a/tests/draft3/divisibleBy.json b/json/tests/draft3/divisibleBy.json index ef7cc14..ef7cc14 100644 --- a/tests/draft3/divisibleBy.json +++ b/json/tests/draft3/divisibleBy.json diff --git a/tests/draft3/enum.json b/json/tests/draft3/enum.json index fc3e070..fc3e070 100644 --- a/tests/draft3/enum.json +++ b/json/tests/draft3/enum.json diff --git a/tests/draft3/extends.json b/json/tests/draft3/extends.json index 909bce5..909bce5 100644 --- a/tests/draft3/extends.json +++ b/json/tests/draft3/extends.json diff --git a/tests/draft3/items.json b/json/tests/draft3/items.json index f5e18a1..f5e18a1 100644 --- a/tests/draft3/items.json +++ b/json/tests/draft3/items.json diff --git a/tests/draft3/maxItems.json b/json/tests/draft3/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft3/maxItems.json +++ b/json/tests/draft3/maxItems.json diff --git a/tests/draft3/maxLength.json b/json/tests/draft3/maxLength.json index 4de42bc..4de42bc 100644 --- a/tests/draft3/maxLength.json +++ b/json/tests/draft3/maxLength.json diff --git a/tests/draft3/maximum.json b/json/tests/draft3/maximum.json index 86c7b89..86c7b89 100644 --- a/tests/draft3/maximum.json +++ b/json/tests/draft3/maximum.json diff --git a/tests/draft3/minItems.json b/json/tests/draft3/minItems.json index ed51188..ed51188 100644 --- a/tests/draft3/minItems.json +++ b/json/tests/draft3/minItems.json diff --git a/tests/draft3/minLength.json b/json/tests/draft3/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft3/minLength.json +++ b/json/tests/draft3/minLength.json diff --git a/tests/draft3/minimum.json b/json/tests/draft3/minimum.json index d5bf000..d5bf000 100644 --- a/tests/draft3/minimum.json +++ b/json/tests/draft3/minimum.json diff --git a/tests/draft3/optional/bignum.json b/json/tests/draft3/optional/bignum.json index ccc7c17..ccc7c17 100644 --- a/tests/draft3/optional/bignum.json +++ b/json/tests/draft3/optional/bignum.json diff --git a/tests/draft3/optional/format.json b/json/tests/draft3/optional/format.json index 9864589..9864589 100644 --- a/tests/draft3/optional/format.json +++ b/json/tests/draft3/optional/format.json diff --git a/tests/draft3/optional/jsregex.json b/json/tests/draft3/optional/jsregex.json index 03fe977..03fe977 100644 --- a/tests/draft3/optional/jsregex.json +++ b/json/tests/draft3/optional/jsregex.json diff --git a/tests/draft3/optional/zeroTerminatedFloats.json b/json/tests/draft3/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft3/optional/zeroTerminatedFloats.json +++ b/json/tests/draft3/optional/zeroTerminatedFloats.json diff --git a/tests/draft3/pattern.json b/json/tests/draft3/pattern.json index 25e7299..25e7299 100644 --- a/tests/draft3/pattern.json +++ b/json/tests/draft3/pattern.json diff --git a/tests/draft3/patternProperties.json b/json/tests/draft3/patternProperties.json index 2ca9aae..2ca9aae 100644 --- a/tests/draft3/patternProperties.json +++ b/json/tests/draft3/patternProperties.json diff --git a/tests/draft3/properties.json b/json/tests/draft3/properties.json index a830c67..a830c67 100644 --- a/tests/draft3/properties.json +++ b/json/tests/draft3/properties.json diff --git a/tests/draft3/ref.json b/json/tests/draft3/ref.json index 31414ad..31414ad 100644 --- a/tests/draft3/ref.json +++ b/json/tests/draft3/ref.json diff --git a/tests/draft3/refRemote.json b/json/tests/draft3/refRemote.json index 4ca8047..4ca8047 100644 --- a/tests/draft3/refRemote.json +++ b/json/tests/draft3/refRemote.json diff --git a/tests/draft3/required.json b/json/tests/draft3/required.json index aaaf024..aaaf024 100644 --- a/tests/draft3/required.json +++ b/json/tests/draft3/required.json diff --git a/tests/draft3/type.json b/json/tests/draft3/type.json index 49c9b40..49c9b40 100644 --- a/tests/draft3/type.json +++ b/json/tests/draft3/type.json diff --git a/tests/draft3/uniqueItems.json b/json/tests/draft3/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft3/uniqueItems.json +++ b/json/tests/draft3/uniqueItems.json diff --git a/tests/draft4/additionalItems.json b/json/tests/draft4/additionalItems.json index abecc57..abecc57 100644 --- a/tests/draft4/additionalItems.json +++ b/json/tests/draft4/additionalItems.json diff --git a/tests/draft4/additionalProperties.json b/json/tests/draft4/additionalProperties.json index ffeac6b..ffeac6b 100644 --- a/tests/draft4/additionalProperties.json +++ b/json/tests/draft4/additionalProperties.json diff --git a/tests/draft4/allOf.json b/json/tests/draft4/allOf.json index ce9fdd4..ce9fdd4 100644 --- a/tests/draft4/allOf.json +++ b/json/tests/draft4/allOf.json diff --git a/tests/draft4/anyOf.json b/json/tests/draft4/anyOf.json index 769adcf..769adcf 100644 --- a/tests/draft4/anyOf.json +++ b/json/tests/draft4/anyOf.json diff --git a/tests/draft4/default.json b/json/tests/draft4/default.json index 1762977..1762977 100644 --- a/tests/draft4/default.json +++ b/json/tests/draft4/default.json diff --git a/tests/draft4/definitions.json b/json/tests/draft4/definitions.json index cf935a3..cf935a3 100644 --- a/tests/draft4/definitions.json +++ b/json/tests/draft4/definitions.json diff --git a/tests/draft4/dependencies.json b/json/tests/draft4/dependencies.json index 38effa1..38effa1 100644 --- a/tests/draft4/dependencies.json +++ b/json/tests/draft4/dependencies.json diff --git a/tests/draft4/enum.json b/json/tests/draft4/enum.json index 8fb9d7a..8fb9d7a 100644 --- a/tests/draft4/enum.json +++ b/json/tests/draft4/enum.json diff --git a/tests/draft4/items.json b/json/tests/draft4/items.json index 7bf9f02..7bf9f02 100644 --- a/tests/draft4/items.json +++ b/json/tests/draft4/items.json diff --git a/tests/draft4/maxItems.json b/json/tests/draft4/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft4/maxItems.json +++ b/json/tests/draft4/maxItems.json diff --git a/tests/draft4/maxLength.json b/json/tests/draft4/maxLength.json index 811d35b..811d35b 100644 --- a/tests/draft4/maxLength.json +++ b/json/tests/draft4/maxLength.json diff --git a/tests/draft4/maxProperties.json b/json/tests/draft4/maxProperties.json index 513731e..513731e 100644 --- a/tests/draft4/maxProperties.json +++ b/json/tests/draft4/maxProperties.json diff --git a/tests/draft4/maximum.json b/json/tests/draft4/maximum.json index 02581f6..02581f6 100644 --- a/tests/draft4/maximum.json +++ b/json/tests/draft4/maximum.json diff --git a/tests/draft4/minItems.json b/json/tests/draft4/minItems.json index ed51188..ed51188 100644 --- a/tests/draft4/minItems.json +++ b/json/tests/draft4/minItems.json diff --git a/tests/draft4/minLength.json b/json/tests/draft4/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft4/minLength.json +++ b/json/tests/draft4/minLength.json diff --git a/tests/draft4/minProperties.json b/json/tests/draft4/minProperties.json index 49a0726..49a0726 100644 --- a/tests/draft4/minProperties.json +++ b/json/tests/draft4/minProperties.json diff --git a/tests/draft4/minimum.json b/json/tests/draft4/minimum.json index 98f08d5..98f08d5 100644 --- a/tests/draft4/minimum.json +++ b/json/tests/draft4/minimum.json diff --git a/tests/draft4/multipleOf.json b/json/tests/draft4/multipleOf.json index ca3b761..ca3b761 100644 --- a/tests/draft4/multipleOf.json +++ b/json/tests/draft4/multipleOf.json diff --git a/tests/draft4/not.json b/json/tests/draft4/not.json index cbb7f46..cbb7f46 100644 --- a/tests/draft4/not.json +++ b/json/tests/draft4/not.json diff --git a/tests/draft4/oneOf.json b/json/tests/draft4/oneOf.json index 9dfffe1..9dfffe1 100644 --- a/tests/draft4/oneOf.json +++ b/json/tests/draft4/oneOf.json diff --git a/tests/draft4/optional/bignum.json b/json/tests/draft4/optional/bignum.json index ccc7c17..ccc7c17 100644 --- a/tests/draft4/optional/bignum.json +++ b/json/tests/draft4/optional/bignum.json diff --git a/tests/draft4/optional/ecmascript-regex.json b/json/tests/draft4/optional/ecmascript-regex.json index 08dc936..08dc936 100644 --- a/tests/draft4/optional/ecmascript-regex.json +++ b/json/tests/draft4/optional/ecmascript-regex.json diff --git a/tests/draft4/optional/format.json b/json/tests/draft4/optional/format.json index 4bf4ea8..4bf4ea8 100644 --- a/tests/draft4/optional/format.json +++ b/json/tests/draft4/optional/format.json diff --git a/tests/draft4/optional/zeroTerminatedFloats.json b/json/tests/draft4/optional/zeroTerminatedFloats.json index 9b50ea2..9b50ea2 100644 --- a/tests/draft4/optional/zeroTerminatedFloats.json +++ b/json/tests/draft4/optional/zeroTerminatedFloats.json diff --git a/tests/draft4/pattern.json b/json/tests/draft4/pattern.json index 25e7299..25e7299 100644 --- a/tests/draft4/pattern.json +++ b/json/tests/draft4/pattern.json diff --git a/tests/draft4/patternProperties.json b/json/tests/draft4/patternProperties.json index 5f741df..5f741df 100644 --- a/tests/draft4/patternProperties.json +++ b/json/tests/draft4/patternProperties.json diff --git a/tests/draft4/properties.json b/json/tests/draft4/properties.json index a830c67..a830c67 100644 --- a/tests/draft4/properties.json +++ b/json/tests/draft4/properties.json diff --git a/tests/draft4/ref.json b/json/tests/draft4/ref.json index 52cf50a..52cf50a 100644 --- a/tests/draft4/ref.json +++ b/json/tests/draft4/ref.json diff --git a/tests/draft4/refRemote.json b/json/tests/draft4/refRemote.json index 8611fad..8611fad 100644 --- a/tests/draft4/refRemote.json +++ b/json/tests/draft4/refRemote.json diff --git a/tests/draft4/required.json b/json/tests/draft4/required.json index 1e2a4f0..1e2a4f0 100644 --- a/tests/draft4/required.json +++ b/json/tests/draft4/required.json diff --git a/tests/draft4/type.json b/json/tests/draft4/type.json index ea33b18..ea33b18 100644 --- a/tests/draft4/type.json +++ b/json/tests/draft4/type.json diff --git a/tests/draft4/uniqueItems.json b/json/tests/draft4/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft4/uniqueItems.json +++ b/json/tests/draft4/uniqueItems.json diff --git a/tests/draft6/additionalItems.json b/json/tests/draft6/additionalItems.json index abecc57..abecc57 100644 --- a/tests/draft6/additionalItems.json +++ b/json/tests/draft6/additionalItems.json diff --git a/tests/draft6/additionalProperties.json b/json/tests/draft6/additionalProperties.json index ffeac6b..ffeac6b 100644 --- a/tests/draft6/additionalProperties.json +++ b/json/tests/draft6/additionalProperties.json diff --git a/tests/draft6/allOf.json b/json/tests/draft6/allOf.json index eb61209..eb61209 100644 --- a/tests/draft6/allOf.json +++ b/json/tests/draft6/allOf.json diff --git a/tests/draft6/anyOf.json b/json/tests/draft6/anyOf.json index bad3e77..bad3e77 100644 --- a/tests/draft6/anyOf.json +++ b/json/tests/draft6/anyOf.json diff --git a/tests/draft6/boolean_schema.json b/json/tests/draft6/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft6/boolean_schema.json +++ b/json/tests/draft6/boolean_schema.json diff --git a/tests/draft6/const.json b/json/tests/draft6/const.json index 0fe00f2..0fe00f2 100644 --- a/tests/draft6/const.json +++ b/json/tests/draft6/const.json diff --git a/tests/draft6/contains.json b/json/tests/draft6/contains.json index b7ae5a2..b7ae5a2 100644 --- a/tests/draft6/contains.json +++ b/json/tests/draft6/contains.json diff --git a/tests/draft6/default.json b/json/tests/draft6/default.json index 1762977..1762977 100644 --- a/tests/draft6/default.json +++ b/json/tests/draft6/default.json diff --git a/tests/draft6/definitions.json b/json/tests/draft6/definitions.json index 7f3b899..7f3b899 100644 --- a/tests/draft6/definitions.json +++ b/json/tests/draft6/definitions.json diff --git a/tests/draft6/dependencies.json b/json/tests/draft6/dependencies.json index 5af1894..5af1894 100644 --- a/tests/draft6/dependencies.json +++ b/json/tests/draft6/dependencies.json diff --git a/tests/draft6/enum.json b/json/tests/draft6/enum.json index 8fb9d7a..8fb9d7a 100644 --- a/tests/draft6/enum.json +++ b/json/tests/draft6/enum.json diff --git a/tests/draft6/exclusiveMaximum.json b/json/tests/draft6/exclusiveMaximum.json index dc3cd70..dc3cd70 100644 --- a/tests/draft6/exclusiveMaximum.json +++ b/json/tests/draft6/exclusiveMaximum.json diff --git a/tests/draft6/exclusiveMinimum.json b/json/tests/draft6/exclusiveMinimum.json index b38d7ec..b38d7ec 100644 --- a/tests/draft6/exclusiveMinimum.json +++ b/json/tests/draft6/exclusiveMinimum.json diff --git a/tests/draft6/items.json b/json/tests/draft6/items.json index 67f1184..67f1184 100644 --- a/tests/draft6/items.json +++ b/json/tests/draft6/items.json diff --git a/tests/draft6/maxItems.json b/json/tests/draft6/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft6/maxItems.json +++ b/json/tests/draft6/maxItems.json diff --git a/tests/draft6/maxLength.json b/json/tests/draft6/maxLength.json index 811d35b..811d35b 100644 --- a/tests/draft6/maxLength.json +++ b/json/tests/draft6/maxLength.json diff --git a/tests/draft6/maxProperties.json b/json/tests/draft6/maxProperties.json index 513731e..513731e 100644 --- a/tests/draft6/maxProperties.json +++ b/json/tests/draft6/maxProperties.json diff --git a/tests/draft6/maximum.json b/json/tests/draft6/maximum.json index 8150984..8150984 100644 --- a/tests/draft6/maximum.json +++ b/json/tests/draft6/maximum.json diff --git a/tests/draft6/minItems.json b/json/tests/draft6/minItems.json index ed51188..ed51188 100644 --- a/tests/draft6/minItems.json +++ b/json/tests/draft6/minItems.json diff --git a/tests/draft6/minLength.json b/json/tests/draft6/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft6/minLength.json +++ b/json/tests/draft6/minLength.json diff --git a/tests/draft6/minProperties.json b/json/tests/draft6/minProperties.json index 49a0726..49a0726 100644 --- a/tests/draft6/minProperties.json +++ b/json/tests/draft6/minProperties.json diff --git a/tests/draft6/minimum.json b/json/tests/draft6/minimum.json index bd1e95b..bd1e95b 100644 --- a/tests/draft6/minimum.json +++ b/json/tests/draft6/minimum.json diff --git a/tests/draft6/multipleOf.json b/json/tests/draft6/multipleOf.json index ca3b761..ca3b761 100644 --- a/tests/draft6/multipleOf.json +++ b/json/tests/draft6/multipleOf.json diff --git a/tests/draft6/not.json b/json/tests/draft6/not.json index 98de0ed..98de0ed 100644 --- a/tests/draft6/not.json +++ b/json/tests/draft6/not.json diff --git a/tests/draft6/oneOf.json b/json/tests/draft6/oneOf.json index 57640b7..57640b7 100644 --- a/tests/draft6/oneOf.json +++ b/json/tests/draft6/oneOf.json diff --git a/tests/draft6/optional/bignum.json b/json/tests/draft6/optional/bignum.json index fac275e..fac275e 100644 --- a/tests/draft6/optional/bignum.json +++ b/json/tests/draft6/optional/bignum.json diff --git a/tests/draft6/optional/ecmascript-regex.json b/json/tests/draft6/optional/ecmascript-regex.json index 08dc936..08dc936 100644 --- a/tests/draft6/optional/ecmascript-regex.json +++ b/json/tests/draft6/optional/ecmascript-regex.json diff --git a/tests/draft6/optional/format.json b/json/tests/draft6/optional/format.json index 74743ff..74743ff 100644 --- a/tests/draft6/optional/format.json +++ b/json/tests/draft6/optional/format.json diff --git a/tests/draft6/optional/zeroTerminatedFloats.json b/json/tests/draft6/optional/zeroTerminatedFloats.json index 1bcdf96..1bcdf96 100644 --- a/tests/draft6/optional/zeroTerminatedFloats.json +++ b/json/tests/draft6/optional/zeroTerminatedFloats.json diff --git a/tests/draft6/pattern.json b/json/tests/draft6/pattern.json index 25e7299..25e7299 100644 --- a/tests/draft6/pattern.json +++ b/json/tests/draft6/pattern.json diff --git a/tests/draft6/patternProperties.json b/json/tests/draft6/patternProperties.json index 1d04a16..1d04a16 100644 --- a/tests/draft6/patternProperties.json +++ b/json/tests/draft6/patternProperties.json diff --git a/tests/draft6/properties.json b/json/tests/draft6/properties.json index c8ad719..c8ad719 100644 --- a/tests/draft6/properties.json +++ b/json/tests/draft6/properties.json diff --git a/tests/draft6/propertyNames.json b/json/tests/draft6/propertyNames.json index 8423690..8423690 100644 --- a/tests/draft6/propertyNames.json +++ b/json/tests/draft6/propertyNames.json diff --git a/tests/draft6/ref.json b/json/tests/draft6/ref.json index 5b58964..5b58964 100644 --- a/tests/draft6/ref.json +++ b/json/tests/draft6/ref.json diff --git a/tests/draft6/refRemote.json b/json/tests/draft6/refRemote.json index 819d326..819d326 100644 --- a/tests/draft6/refRemote.json +++ b/json/tests/draft6/refRemote.json diff --git a/tests/draft6/required.json b/json/tests/draft6/required.json index bd96907..bd96907 100644 --- a/tests/draft6/required.json +++ b/json/tests/draft6/required.json diff --git a/tests/draft6/type.json b/json/tests/draft6/type.json index ea33b18..ea33b18 100644 --- a/tests/draft6/type.json +++ b/json/tests/draft6/type.json diff --git a/tests/draft6/uniqueItems.json b/json/tests/draft6/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft6/uniqueItems.json +++ b/json/tests/draft6/uniqueItems.json diff --git a/tests/draft7/additionalItems.json b/json/tests/draft7/additionalItems.json index abecc57..abecc57 100644 --- a/tests/draft7/additionalItems.json +++ b/json/tests/draft7/additionalItems.json diff --git a/tests/draft7/additionalProperties.json b/json/tests/draft7/additionalProperties.json index ffeac6b..ffeac6b 100644 --- a/tests/draft7/additionalProperties.json +++ b/json/tests/draft7/additionalProperties.json diff --git a/tests/draft7/allOf.json b/json/tests/draft7/allOf.json index eb61209..eb61209 100644 --- a/tests/draft7/allOf.json +++ b/json/tests/draft7/allOf.json diff --git a/tests/draft7/anyOf.json b/json/tests/draft7/anyOf.json index bad3e77..bad3e77 100644 --- a/tests/draft7/anyOf.json +++ b/json/tests/draft7/anyOf.json diff --git a/tests/draft7/boolean_schema.json b/json/tests/draft7/boolean_schema.json index 6d40f23..6d40f23 100644 --- a/tests/draft7/boolean_schema.json +++ b/json/tests/draft7/boolean_schema.json diff --git a/tests/draft7/const.json b/json/tests/draft7/const.json index 0fe00f2..0fe00f2 100644 --- a/tests/draft7/const.json +++ b/json/tests/draft7/const.json diff --git a/tests/draft7/contains.json b/json/tests/draft7/contains.json index b7ae5a2..b7ae5a2 100644 --- a/tests/draft7/contains.json +++ b/json/tests/draft7/contains.json diff --git a/tests/draft7/default.json b/json/tests/draft7/default.json index 1762977..1762977 100644 --- a/tests/draft7/default.json +++ b/json/tests/draft7/default.json diff --git a/tests/draft7/definitions.json b/json/tests/draft7/definitions.json index 4360406..4360406 100644 --- a/tests/draft7/definitions.json +++ b/json/tests/draft7/definitions.json diff --git a/tests/draft7/dependencies.json b/json/tests/draft7/dependencies.json index 5af1894..5af1894 100644 --- a/tests/draft7/dependencies.json +++ b/json/tests/draft7/dependencies.json diff --git a/tests/draft7/enum.json b/json/tests/draft7/enum.json index 8fb9d7a..8fb9d7a 100644 --- a/tests/draft7/enum.json +++ b/json/tests/draft7/enum.json diff --git a/tests/draft7/exclusiveMaximum.json b/json/tests/draft7/exclusiveMaximum.json index dc3cd70..dc3cd70 100644 --- a/tests/draft7/exclusiveMaximum.json +++ b/json/tests/draft7/exclusiveMaximum.json diff --git a/tests/draft7/exclusiveMinimum.json b/json/tests/draft7/exclusiveMinimum.json index b38d7ec..b38d7ec 100644 --- a/tests/draft7/exclusiveMinimum.json +++ b/json/tests/draft7/exclusiveMinimum.json diff --git a/tests/draft7/if-then-else.json b/json/tests/draft7/if-then-else.json index 37a229c..37a229c 100644 --- a/tests/draft7/if-then-else.json +++ b/json/tests/draft7/if-then-else.json diff --git a/tests/draft7/items.json b/json/tests/draft7/items.json index 67f1184..67f1184 100644 --- a/tests/draft7/items.json +++ b/json/tests/draft7/items.json diff --git a/tests/draft7/maxItems.json b/json/tests/draft7/maxItems.json index 3b53a6b..3b53a6b 100644 --- a/tests/draft7/maxItems.json +++ b/json/tests/draft7/maxItems.json diff --git a/tests/draft7/maxLength.json b/json/tests/draft7/maxLength.json index 811d35b..811d35b 100644 --- a/tests/draft7/maxLength.json +++ b/json/tests/draft7/maxLength.json diff --git a/tests/draft7/maxProperties.json b/json/tests/draft7/maxProperties.json index 513731e..513731e 100644 --- a/tests/draft7/maxProperties.json +++ b/json/tests/draft7/maxProperties.json diff --git a/tests/draft7/maximum.json b/json/tests/draft7/maximum.json index 8150984..8150984 100644 --- a/tests/draft7/maximum.json +++ b/json/tests/draft7/maximum.json diff --git a/tests/draft7/minItems.json b/json/tests/draft7/minItems.json index ed51188..ed51188 100644 --- a/tests/draft7/minItems.json +++ b/json/tests/draft7/minItems.json diff --git a/tests/draft7/minLength.json b/json/tests/draft7/minLength.json index 3f09158..3f09158 100644 --- a/tests/draft7/minLength.json +++ b/json/tests/draft7/minLength.json diff --git a/tests/draft7/minProperties.json b/json/tests/draft7/minProperties.json index 49a0726..49a0726 100644 --- a/tests/draft7/minProperties.json +++ b/json/tests/draft7/minProperties.json diff --git a/tests/draft7/minimum.json b/json/tests/draft7/minimum.json index bd1e95b..bd1e95b 100644 --- a/tests/draft7/minimum.json +++ b/json/tests/draft7/minimum.json diff --git a/tests/draft7/multipleOf.json b/json/tests/draft7/multipleOf.json index ca3b761..ca3b761 100644 --- a/tests/draft7/multipleOf.json +++ b/json/tests/draft7/multipleOf.json diff --git a/tests/draft7/not.json b/json/tests/draft7/not.json index 98de0ed..98de0ed 100644 --- a/tests/draft7/not.json +++ b/json/tests/draft7/not.json diff --git a/tests/draft7/oneOf.json b/json/tests/draft7/oneOf.json index 57640b7..57640b7 100644 --- a/tests/draft7/oneOf.json +++ b/json/tests/draft7/oneOf.json diff --git a/tests/draft7/optional/bignum.json b/json/tests/draft7/optional/bignum.json index fac275e..fac275e 100644 --- a/tests/draft7/optional/bignum.json +++ b/json/tests/draft7/optional/bignum.json diff --git a/tests/draft7/optional/content.json b/json/tests/draft7/optional/content.json index 3f5a743..3f5a743 100644 --- a/tests/draft7/optional/content.json +++ b/json/tests/draft7/optional/content.json diff --git a/tests/draft7/optional/ecmascript-regex.json b/json/tests/draft7/optional/ecmascript-regex.json index 08dc936..08dc936 100644 --- a/tests/draft7/optional/ecmascript-regex.json +++ b/json/tests/draft7/optional/ecmascript-regex.json diff --git a/tests/draft7/optional/format/date-time.json b/json/tests/draft7/optional/format/date-time.json index dfccee6..dfccee6 100644 --- a/tests/draft7/optional/format/date-time.json +++ b/json/tests/draft7/optional/format/date-time.json diff --git a/tests/draft7/optional/format/date.json b/json/tests/draft7/optional/format/date.json index cd23baa..cd23baa 100644 --- a/tests/draft7/optional/format/date.json +++ b/json/tests/draft7/optional/format/date.json diff --git a/tests/draft7/optional/format/email.json b/json/tests/draft7/optional/format/email.json index c837c84..c837c84 100644 --- a/tests/draft7/optional/format/email.json +++ b/json/tests/draft7/optional/format/email.json diff --git a/tests/draft7/optional/format/hostname.json b/json/tests/draft7/optional/format/hostname.json index d22e57d..d22e57d 100644 --- a/tests/draft7/optional/format/hostname.json +++ b/json/tests/draft7/optional/format/hostname.json diff --git a/tests/draft7/optional/format/idn-email.json b/json/tests/draft7/optional/format/idn-email.json index 637409e..637409e 100644 --- a/tests/draft7/optional/format/idn-email.json +++ b/json/tests/draft7/optional/format/idn-email.json diff --git a/tests/draft7/optional/format/idn-hostname.json b/json/tests/draft7/optional/format/idn-hostname.json index 3291820..3291820 100644 --- a/tests/draft7/optional/format/idn-hostname.json +++ b/json/tests/draft7/optional/format/idn-hostname.json diff --git a/tests/draft7/optional/format/ipv4.json b/json/tests/draft7/optional/format/ipv4.json index 661148a..661148a 100644 --- a/tests/draft7/optional/format/ipv4.json +++ b/json/tests/draft7/optional/format/ipv4.json diff --git a/tests/draft7/optional/format/ipv6.json b/json/tests/draft7/optional/format/ipv6.json index f67559b..f67559b 100644 --- a/tests/draft7/optional/format/ipv6.json +++ b/json/tests/draft7/optional/format/ipv6.json diff --git a/tests/draft7/optional/format/iri-reference.json b/json/tests/draft7/optional/format/iri-reference.json index 1fd779c..1fd779c 100644 --- a/tests/draft7/optional/format/iri-reference.json +++ b/json/tests/draft7/optional/format/iri-reference.json diff --git a/tests/draft7/optional/format/iri.json b/json/tests/draft7/optional/format/iri.json index ed54094..ed54094 100644 --- a/tests/draft7/optional/format/iri.json +++ b/json/tests/draft7/optional/format/iri.json diff --git a/tests/draft7/optional/format/json-pointer.json b/json/tests/draft7/optional/format/json-pointer.json index 65c2f06..65c2f06 100644 --- a/tests/draft7/optional/format/json-pointer.json +++ b/json/tests/draft7/optional/format/json-pointer.json diff --git a/tests/draft7/optional/format/regex.json b/json/tests/draft7/optional/format/regex.json index d99d021..d99d021 100644 --- a/tests/draft7/optional/format/regex.json +++ b/json/tests/draft7/optional/format/regex.json diff --git a/tests/draft7/optional/format/relative-json-pointer.json b/json/tests/draft7/optional/format/relative-json-pointer.json index ceeb743..ceeb743 100644 --- a/tests/draft7/optional/format/relative-json-pointer.json +++ b/json/tests/draft7/optional/format/relative-json-pointer.json diff --git a/tests/draft7/optional/format/time.json b/json/tests/draft7/optional/format/time.json index 4ec8a01..4ec8a01 100644 --- a/tests/draft7/optional/format/time.json +++ b/json/tests/draft7/optional/format/time.json diff --git a/tests/draft7/optional/format/uri-reference.json b/json/tests/draft7/optional/format/uri-reference.json index e4c9eef..e4c9eef 100644 --- a/tests/draft7/optional/format/uri-reference.json +++ b/json/tests/draft7/optional/format/uri-reference.json diff --git a/tests/draft7/optional/format/uri-template.json b/json/tests/draft7/optional/format/uri-template.json index d8396a5..d8396a5 100644 --- a/tests/draft7/optional/format/uri-template.json +++ b/json/tests/draft7/optional/format/uri-template.json diff --git a/tests/draft7/optional/format/uri.json b/json/tests/draft7/optional/format/uri.json index 25cc40c..25cc40c 100644 --- a/tests/draft7/optional/format/uri.json +++ b/json/tests/draft7/optional/format/uri.json diff --git a/tests/draft7/optional/zeroTerminatedFloats.json b/json/tests/draft7/optional/zeroTerminatedFloats.json index 1bcdf96..1bcdf96 100644 --- a/tests/draft7/optional/zeroTerminatedFloats.json +++ b/json/tests/draft7/optional/zeroTerminatedFloats.json diff --git a/tests/draft7/pattern.json b/json/tests/draft7/pattern.json index 25e7299..25e7299 100644 --- a/tests/draft7/pattern.json +++ b/json/tests/draft7/pattern.json diff --git a/tests/draft7/patternProperties.json b/json/tests/draft7/patternProperties.json index 1d04a16..1d04a16 100644 --- a/tests/draft7/patternProperties.json +++ b/json/tests/draft7/patternProperties.json diff --git a/tests/draft7/properties.json b/json/tests/draft7/properties.json index c8ad719..c8ad719 100644 --- a/tests/draft7/properties.json +++ b/json/tests/draft7/properties.json diff --git a/tests/draft7/propertyNames.json b/json/tests/draft7/propertyNames.json index 8423690..8423690 100644 --- a/tests/draft7/propertyNames.json +++ b/json/tests/draft7/propertyNames.json diff --git a/tests/draft7/ref.json b/json/tests/draft7/ref.json index 7579507..7579507 100644 --- a/tests/draft7/ref.json +++ b/json/tests/draft7/ref.json diff --git a/tests/draft7/refRemote.json b/json/tests/draft7/refRemote.json index 819d326..819d326 100644 --- a/tests/draft7/refRemote.json +++ b/json/tests/draft7/refRemote.json diff --git a/tests/draft7/required.json b/json/tests/draft7/required.json index bd96907..bd96907 100644 --- a/tests/draft7/required.json +++ b/json/tests/draft7/required.json diff --git a/tests/draft7/type.json b/json/tests/draft7/type.json index ea33b18..ea33b18 100644 --- a/tests/draft7/type.json +++ b/json/tests/draft7/type.json diff --git a/tests/draft7/uniqueItems.json b/json/tests/draft7/uniqueItems.json index c1f4ab9..c1f4ab9 100644 --- a/tests/draft7/uniqueItems.json +++ b/json/tests/draft7/uniqueItems.json diff --git a/json/tox.ini b/json/tox.ini new file mode 100644 index 0000000..5301222 --- /dev/null +++ b/json/tox.ini @@ -0,0 +1,8 @@ +[tox] +minversion = 1.6 +envlist = py27 +skipsdist = True + +[testenv] +deps = jsonschema +commands = {envpython} bin/jsonschema_suite check diff --git a/jsonschema/__init__.py b/jsonschema/__init__.py new file mode 100644 index 0000000..0da56aa --- /dev/null +++ b/jsonschema/__init__.py @@ -0,0 +1,33 @@ +""" +An implementation of JSON Schema for Python + +The main functionality is provided by the validator classes for each of the +supported JSON Schema versions. + +Most commonly, `validate` is the quickest way to simply validate a given +instance under a schema, and will create a validator for you. + +""" + +from jsonschema.exceptions import ( + ErrorTree, FormatError, RefResolutionError, SchemaError, ValidationError +) +from jsonschema._format import ( + FormatChecker, + draft3_format_checker, + draft4_format_checker, + draft6_format_checker, + draft7_format_checker, +) +from jsonschema._types import TypeChecker +from jsonschema.validators import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + RefResolver, + validate, +) + +from pkg_resources import get_distribution +__version__ = get_distribution(__name__).version diff --git a/jsonschema/__main__.py b/jsonschema/__main__.py new file mode 100644 index 0000000..82c29fd --- /dev/null +++ b/jsonschema/__main__.py @@ -0,0 +1,2 @@ +from jsonschema.cli import main +main() diff --git a/jsonschema/_format.py b/jsonschema/_format.py new file mode 100644 index 0000000..d3f1345 --- /dev/null +++ b/jsonschema/_format.py @@ -0,0 +1,402 @@ +import datetime +import re +import socket +import struct + +from jsonschema.compat import str_types +from jsonschema.exceptions import FormatError + + +class FormatChecker(object): + """ + A ``format`` property checker. + + JSON Schema does not mandate that the ``format`` property actually do any + validation. If validation is desired however, instances of this class can + be hooked into validators to enable format validation. + + `FormatChecker` objects always return ``True`` when asked about + formats that they do not know how to validate. + + To check a custom format using a function that takes an instance and + returns a ``bool``, use the `FormatChecker.checks` or + `FormatChecker.cls_checks` decorators. + + Arguments: + + formats (~collections.Iterable): + + The known formats to validate. This argument can be used to + limit which formats will be used during validation. + + """ + + checkers = {} + + def __init__(self, formats=None): + if formats is None: + self.checkers = self.checkers.copy() + else: + self.checkers = dict((k, self.checkers[k]) for k in formats) + + def checks(self, format, raises=()): + """ + Register a decorated function as validating a new format. + + Arguments: + + format (str): + + The format that the decorated function will check. + + raises (Exception): + + The exception(s) raised by the decorated function when an + invalid instance is found. + + The exception object will be accessible as the + `jsonschema.exceptions.ValidationError.cause` attribute of the + resulting validation error. + + """ + + def _checks(func): + self.checkers[format] = (func, raises) + return func + return _checks + + cls_checks = classmethod(checks) + + def check(self, instance, format): + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format (str): + + The format that instance should conform to + + + Raises: + + FormatError: if the instance does not conform to ``format`` + + """ + + if format not in self.checkers: + return + + func, raises = self.checkers[format] + result, cause = None, None + try: + result = func(instance) + except raises as e: + cause = e + if not result: + raise FormatError( + "%r is not a %r" % (instance, format), cause=cause, + ) + + def conforms(self, instance, format): + """ + Check whether the instance conforms to the given format. + + Arguments: + + instance (*any primitive type*, i.e. str, number, bool): + + The instance to check + + format (str): + + The format that instance should conform to + + Returns: + + bool: whether it conformed + + """ + + try: + self.check(instance, format) + except FormatError: + return False + else: + return True + + +draft3_format_checker = FormatChecker() +draft4_format_checker = FormatChecker() +draft6_format_checker = FormatChecker() +draft7_format_checker = FormatChecker() + + +_draft_checkers = dict( + draft3=draft3_format_checker, + draft4=draft4_format_checker, + draft6=draft6_format_checker, + draft7=draft7_format_checker, +) + + +def _checks_drafts( + name=None, + draft3=None, + draft4=None, + draft6=None, + draft7=None, + raises=(), +): + draft3 = draft3 or name + draft4 = draft4 or name + draft6 = draft6 or name + draft7 = draft7 or name + + def wrap(func): + if draft3: + func = _draft_checkers["draft3"].checks(draft3, raises)(func) + if draft4: + func = _draft_checkers["draft4"].checks(draft4, raises)(func) + if draft6: + func = _draft_checkers["draft6"].checks(draft6, raises)(func) + if draft7: + func = _draft_checkers["draft7"].checks(draft7, raises)(func) + + # Oy. This is bad global state, but relied upon for now, until + # deprecation. See https://github.com/Julian/jsonschema/issues/519 + # and test_format_checkers_come_with_defaults + FormatChecker.cls_checks(draft7 or draft6 or draft4 or draft3, raises)( + func, + ) + return func + return wrap + + +@_checks_drafts(name="idn-email") +@_checks_drafts(name="email") +def is_email(instance): + if not isinstance(instance, str_types): + return True + return "@" in instance + + +_ipv4_re = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") + + +@_checks_drafts( + draft3="ip-address", draft4="ipv4", draft6="ipv4", draft7="ipv4", +) +def is_ipv4(instance): + if not isinstance(instance, str_types): + return True + if not _ipv4_re.match(instance): + return False + return all(0 <= int(component) <= 255 for component in instance.split(".")) + + +if hasattr(socket, "inet_pton"): + # FIXME: Really this only should raise struct.error, but see the sadness + # that is https://twistedmatrix.com/trac/ticket/9409 + @_checks_drafts( + name="ipv6", raises=(socket.error, struct.error, ValueError), + ) + def is_ipv6(instance): + if not isinstance(instance, str_types): + return True + return socket.inet_pton(socket.AF_INET6, instance) + + +_host_name_re = re.compile(r"^[A-Za-z0-9][A-Za-z0-9\.\-]{1,255}$") + + +@_checks_drafts( + draft3="host-name", + draft4="hostname", + draft6="hostname", + draft7="hostname", +) +def is_host_name(instance): + if not isinstance(instance, str_types): + return True + if not _host_name_re.match(instance): + return False + components = instance.split(".") + for component in components: + if len(component) > 63: + return False + return True + + +try: + # The built-in `idna` codec only implements RFC 3890, so we go elsewhere. + import idna +except ImportError: + pass +else: + @_checks_drafts(draft7="idn-hostname", raises=idna.IDNAError) + def is_idn_host_name(instance): + if not isinstance(instance, str_types): + return True + idna.encode(instance) + return True + + +try: + import rfc3987 +except ImportError: + pass +else: + @_checks_drafts(draft7="iri", raises=ValueError) + def is_iri(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="IRI") + + @_checks_drafts(draft7="iri-reference", raises=ValueError) + def is_iri_reference(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="IRI_reference") + + @_checks_drafts(name="uri", raises=ValueError) + def is_uri(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="URI") + + @_checks_drafts( + draft6="uri-reference", + draft7="uri-reference", + raises=ValueError, + ) + def is_uri_reference(instance): + if not isinstance(instance, str_types): + return True + return rfc3987.parse(instance, rule="URI_reference") + + +try: + import strict_rfc3339 +except ImportError: + pass +else: + @_checks_drafts(name="date-time") + def is_datetime(instance): + if not isinstance(instance, str_types): + return True + return strict_rfc3339.validate_rfc3339(instance) + + @_checks_drafts(draft7="time") + def is_time(instance): + if not isinstance(instance, str_types): + return True + return is_datetime("1970-01-01T" + instance) + + +@_checks_drafts(name="regex", raises=re.error) +def is_regex(instance): + if not isinstance(instance, str_types): + return True + return re.compile(instance) + + +@_checks_drafts(draft3="date", draft7="date", raises=ValueError) +def is_date(instance): + if not isinstance(instance, str_types): + return True + return datetime.datetime.strptime(instance, "%Y-%m-%d") + + +@_checks_drafts(draft3="time", raises=ValueError) +def is_draft3_time(instance): + if not isinstance(instance, str_types): + return True + return datetime.datetime.strptime(instance, "%H:%M:%S") + + +try: + import webcolors +except ImportError: + pass +else: + def is_css_color_code(instance): + return webcolors.normalize_hex(instance) + + @_checks_drafts(draft3="color", raises=(ValueError, TypeError)) + def is_css21_color(instance): + if ( + not isinstance(instance, str_types) or + instance.lower() in webcolors.css21_names_to_hex + ): + return True + return is_css_color_code(instance) + + def is_css3_color(instance): + if instance.lower() in webcolors.css3_names_to_hex: + return True + return is_css_color_code(instance) + + +try: + import jsonpointer +except ImportError: + pass +else: + @_checks_drafts( + draft6="json-pointer", + draft7="json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_json_pointer(instance): + if not isinstance(instance, str_types): + return True + return jsonpointer.JsonPointer(instance) + + # TODO: I don't want to maintain this, so it + # needs to go either into jsonpointer (pending + # https://github.com/stefankoegl/python-json-pointer/issues/34) or + # into a new external library. + @_checks_drafts( + draft7="relative-json-pointer", + raises=jsonpointer.JsonPointerException, + ) + def is_relative_json_pointer(instance): + # Definition taken from: + # https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01#section-3 + if not isinstance(instance, str_types): + return True + non_negative_integer, rest = [], "" + for i, character in enumerate(instance): + if character.isdigit(): + non_negative_integer.append(character) + continue + + if not non_negative_integer: + return False + + rest = instance[i:] + break + return (rest == "#") or jsonpointer.JsonPointer(rest) + + +try: + import uritemplate.exceptions +except ImportError: + pass +else: + @_checks_drafts( + draft6="uri-template", + draft7="uri-template", + raises=uritemplate.exceptions.InvalidTemplate, + ) + def is_uri_template( + instance, + template_validator=uritemplate.Validator().force_balanced_braces(), + ): + template = uritemplate.URITemplate(instance) + return template_validator.validate(template) diff --git a/jsonschema/_legacy_validators.py b/jsonschema/_legacy_validators.py new file mode 100644 index 0000000..264ff7d --- /dev/null +++ b/jsonschema/_legacy_validators.py @@ -0,0 +1,141 @@ +from jsonschema import _utils +from jsonschema.compat import iteritems +from jsonschema.exceptions import ValidationError + + +def dependencies_draft3(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in iteritems(dependencies): + if property not in instance: + continue + + if validator.is_type(dependency, "object"): + for error in validator.descend( + instance, dependency, schema_path=property, + ): + yield error + elif validator.is_type(dependency, "string"): + if dependency not in instance: + yield ValidationError( + "%r is a dependency of %r" % (dependency, property) + ) + else: + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + + +def disallow_draft3(validator, disallow, instance, schema): + for disallowed in _utils.ensure_list(disallow): + if validator.is_valid(instance, {"type": [disallowed]}): + yield ValidationError( + "%r is disallowed for %r" % (disallowed, instance) + ) + + +def extends_draft3(validator, extends, instance, schema): + if validator.is_type(extends, "object"): + for error in validator.descend(instance, extends): + yield error + return + for index, subschema in enumerate(extends): + for error in validator.descend(instance, subschema, schema_path=index): + yield error + + +def items_draft3_draft4(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "object"): + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + else: + for (index, item), subschema in zip(enumerate(instance), items): + for error in validator.descend( + item, subschema, path=index, schema_path=index, + ): + yield error + + +def minimum_draft3_draft4(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMinimum", False): + failed = instance <= minimum + cmp = "less than or equal to" + else: + failed = instance < minimum + cmp = "less than" + + if failed: + yield ValidationError( + "%r is %s the minimum of %r" % (instance, cmp, minimum) + ) + + +def maximum_draft3_draft4(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if schema.get("exclusiveMaximum", False): + failed = instance >= maximum + cmp = "greater than or equal to" + else: + failed = instance > maximum + cmp = "greater than" + + if failed: + yield ValidationError( + "%r is %s the maximum of %r" % (instance, cmp, maximum) + ) + + +def properties_draft3(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ): + yield error + elif subschema.get("required", False): + error = ValidationError("%r is a required property" % property) + error._set( + validator="required", + validator_value=subschema["required"], + instance=instance, + schema=schema, + ) + error.path.appendleft(property) + error.schema_path.extend([property, "required"]) + yield error + + +def type_draft3(validator, types, instance, schema): + types = _utils.ensure_list(types) + + all_errors = [] + for index, type in enumerate(types): + if validator.is_type(type, "object"): + errors = list(validator.descend(instance, type, schema_path=index)) + if not errors: + return + all_errors.extend(errors) + else: + if validator.is_type(instance, type): + return + else: + yield ValidationError( + _utils.types_msg(instance, types), context=all_errors, + ) diff --git a/jsonschema/_reflect.py b/jsonschema/_reflect.py new file mode 100644 index 0000000..d09e38f --- /dev/null +++ b/jsonschema/_reflect.py @@ -0,0 +1,155 @@ +# -*- test-case-name: twisted.test.test_reflect -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Standardized versions of various cool and/or strange things that you can do +with Python's reflection capabilities. +""" + +import sys + +from jsonschema.compat import PY3 + + +class _NoModuleFound(Exception): + """ + No module was found because none exists. + """ + + + +class InvalidName(ValueError): + """ + The given name is not a dot-separated list of Python objects. + """ + + + +class ModuleNotFound(InvalidName): + """ + The module associated with the given name doesn't exist and it can't be + imported. + """ + + + +class ObjectNotFound(InvalidName): + """ + The object associated with the given name doesn't exist and it can't be + imported. + """ + + + +if PY3: + def reraise(exception, traceback): + raise exception.with_traceback(traceback) +else: + exec("""def reraise(exception, traceback): + raise exception.__class__, exception, traceback""") + +reraise.__doc__ = """ +Re-raise an exception, with an optional traceback, in a way that is compatible +with both Python 2 and Python 3. + +Note that on Python 3, re-raised exceptions will be mutated, with their +C{__traceback__} attribute being set. + +@param exception: The exception instance. +@param traceback: The traceback to use, or C{None} indicating a new traceback. +""" + + +def _importAndCheckStack(importName): + """ + Import the given name as a module, then walk the stack to determine whether + the failure was the module not existing, or some code in the module (for + example a dependent import) failing. This can be helpful to determine + whether any actual application code was run. For example, to distiguish + administrative error (entering the wrong module name), from programmer + error (writing buggy code in a module that fails to import). + + @param importName: The name of the module to import. + @type importName: C{str} + @raise Exception: if something bad happens. This can be any type of + exception, since nobody knows what loading some arbitrary code might + do. + @raise _NoModuleFound: if no module was found. + """ + try: + return __import__(importName) + except ImportError: + excType, excValue, excTraceback = sys.exc_info() + while excTraceback: + execName = excTraceback.tb_frame.f_globals["__name__"] + # in Python 2 execName is None when an ImportError is encountered, + # where in Python 3 execName is equal to the importName. + if execName is None or execName == importName: + reraise(excValue, excTraceback) + excTraceback = excTraceback.tb_next + raise _NoModuleFound() + + + +def namedAny(name): + """ + Retrieve a Python object by its fully qualified name from the global Python + module namespace. The first part of the name, that describes a module, + will be discovered and imported. Each subsequent part of the name is + treated as the name of an attribute of the object specified by all of the + name which came before it. For example, the fully-qualified name of this + object is 'twisted.python.reflect.namedAny'. + + @type name: L{str} + @param name: The name of the object to return. + + @raise InvalidName: If the name is an empty string, starts or ends with + a '.', or is otherwise syntactically incorrect. + + @raise ModuleNotFound: If the name is syntactically correct but the + module it specifies cannot be imported because it does not appear to + exist. + + @raise ObjectNotFound: If the name is syntactically correct, includes at + least one '.', but the module it specifies cannot be imported because + it does not appear to exist. + + @raise AttributeError: If an attribute of an object along the way cannot be + accessed, or a module along the way is not found. + + @return: the Python object identified by 'name'. + """ + if not name: + raise InvalidName('Empty module name') + + names = name.split('.') + + # if the name starts or ends with a '.' or contains '..', the __import__ + # will raise an 'Empty module name' error. This will provide a better error + # message. + if '' in names: + raise InvalidName( + "name must be a string giving a '.'-separated list of Python " + "identifiers, not %r" % (name,)) + + topLevelPackage = None + moduleNames = names[:] + while not topLevelPackage: + if moduleNames: + trialname = '.'.join(moduleNames) + try: + topLevelPackage = _importAndCheckStack(trialname) + except _NoModuleFound: + moduleNames.pop() + else: + if len(names) == 1: + raise ModuleNotFound("No module named %r" % (name,)) + else: + raise ObjectNotFound('%r does not name an object' % (name,)) + + obj = topLevelPackage + for n in names[1:]: + obj = getattr(obj, n) + + return obj diff --git a/jsonschema/_types.py b/jsonschema/_types.py new file mode 100644 index 0000000..f556ded --- /dev/null +++ b/jsonschema/_types.py @@ -0,0 +1,188 @@ +import numbers + +from pyrsistent import pmap +import attr + +from jsonschema.compat import int_types, str_types +from jsonschema.exceptions import UndefinedTypeCheck + + +def is_array(checker, instance): + return isinstance(instance, list) + + +def is_bool(checker, instance): + return isinstance(instance, bool) + + +def is_integer(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, int_types) + + +def is_null(checker, instance): + return instance is None + + +def is_number(checker, instance): + # bool inherits from int, so ensure bools aren't reported as ints + if isinstance(instance, bool): + return False + return isinstance(instance, numbers.Number) + + +def is_object(checker, instance): + return isinstance(instance, dict) + + +def is_string(checker, instance): + return isinstance(instance, str_types) + + +def is_any(checker, instance): + return True + + +@attr.s(frozen=True) +class TypeChecker(object): + """ + A ``type`` property checker. + + A `TypeChecker` performs type checking for an `IValidator`. Type + checks to perform are updated using `TypeChecker.redefine` or + `TypeChecker.redefine_many` and removed via `TypeChecker.remove`. + Each of these return a new `TypeChecker` object. + + Arguments: + + type_checkers (dict): + + The initial mapping of types to their checking functions. + """ + _type_checkers = attr.ib(default=pmap(), converter=pmap) + + def is_type(self, instance, type): + """ + Check if the instance is of the appropriate type. + + Arguments: + + instance (object): + + The instance to check + + type (str): + + The name of the type that is expected. + + Returns: + + bool: Whether it conformed. + + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + if type is unknown to this object. + """ + try: + fn = self._type_checkers[type] + except KeyError: + raise UndefinedTypeCheck(type) + + return fn(self, instance) + + def redefine(self, type, fn): + """ + Produce a new checker with the given type redefined. + + Arguments: + + type (str): + + The name of the type to check. + + fn (callable): + + A function taking exactly two parameters - the type + checker calling the function and the instance to check. + The function should return true if instance is of this + type and false otherwise. + + Returns: + + A new `TypeChecker` instance. + """ + return self.redefine_many({type: fn}) + + def redefine_many(self, definitions=()): + """ + Produce a new checker with the given types redefined. + + Arguments: + + definitions (dict): + + A dictionary mapping types to their checking functions. + + Returns: + + A new `TypeChecker` instance. + """ + return attr.evolve( + self, type_checkers=self._type_checkers.update(definitions), + ) + + def remove(self, *types): + """ + Produce a new checker with the given types forgotten. + + Arguments: + + types (~collections.Iterable): + + the names of the types to remove. + + Returns: + + A new `TypeChecker` instance + + Raises: + + `jsonschema.exceptions.UndefinedTypeCheck`: + + if any given type is unknown to this object + """ + + checkers = self._type_checkers + for each in types: + try: + checkers = checkers.remove(each) + except KeyError: + raise UndefinedTypeCheck(each) + return attr.evolve(self, type_checkers=checkers) + + +draft3_type_checker = TypeChecker( + { + u"any": is_any, + u"array": is_array, + u"boolean": is_bool, + u"integer": is_integer, + u"object": is_object, + u"null": is_null, + u"number": is_number, + u"string": is_string, + }, +) +draft4_type_checker = draft3_type_checker.remove(u"any") +draft6_type_checker = draft4_type_checker.redefine( + u"integer", + lambda checker, instance: ( + is_integer(checker, instance) or + isinstance(instance, float) and instance.is_integer() + ), +) +draft7_type_checker = draft6_type_checker diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py new file mode 100644 index 0000000..f758b39 --- /dev/null +++ b/jsonschema/_utils.py @@ -0,0 +1,217 @@ +import itertools +import json +import pkgutil +import re + +from jsonschema.compat import MutableMapping, str_types, urlsplit + + +class URIDict(MutableMapping): + """ + Dictionary which uses normalized URIs as keys. + + """ + + def normalize(self, uri): + return urlsplit(uri).geturl() + + def __init__(self, *args, **kwargs): + self.store = dict() + self.store.update(*args, **kwargs) + + def __getitem__(self, uri): + return self.store[self.normalize(uri)] + + def __setitem__(self, uri, value): + self.store[self.normalize(uri)] = value + + def __delitem__(self, uri): + del self.store[self.normalize(uri)] + + def __iter__(self): + return iter(self.store) + + def __len__(self): + return len(self.store) + + def __repr__(self): + return repr(self.store) + + +class Unset(object): + """ + An as-of-yet unset attribute or unprovided default parameter. + + """ + + def __repr__(self): + return "<unset>" + + +def load_schema(name): + """ + Load a schema from ./schemas/``name``.json and return it. + + """ + + data = pkgutil.get_data('jsonschema', "schemas/{0}.json".format(name)) + return json.loads(data.decode("utf-8")) + + +def indent(string, times=1): + """ + A dumb version of `textwrap.indent` from Python 3.3. + + """ + + return "\n".join(" " * (4 * times) + line for line in string.splitlines()) + + +def format_as_index(indices): + """ + Construct a single string containing indexing operations for the indices. + + For example, [1, 2, "foo"] -> [1][2]["foo"] + + Arguments: + + indices (sequence): + + The indices to format. + + """ + + if not indices: + return "" + return "[%s]" % "][".join(repr(index) for index in indices) + + +def find_additional_properties(instance, schema): + """ + Return the set of additional properties for the given ``instance``. + + Weeds out properties that should have been validated by ``properties`` and + / or ``patternProperties``. + + Assumes ``instance`` is dict-like already. + + """ + + properties = schema.get("properties", {}) + patterns = "|".join(schema.get("patternProperties", {})) + for property in instance: + if property not in properties: + if patterns and re.search(patterns, property): + continue + yield property + + +def extras_msg(extras): + """ + Create an error message for extra items or properties. + + """ + + if len(extras) == 1: + verb = "was" + else: + verb = "were" + return ", ".join(repr(extra) for extra in extras), verb + + +def types_msg(instance, types): + """ + Create an error message for a failure to match the given types. + + If the ``instance`` is an object and contains a ``name`` property, it will + be considered to be a description of that object and used as its type. + + Otherwise the message is simply the reprs of the given ``types``. + + """ + + reprs = [] + for type in types: + try: + reprs.append(repr(type["name"])) + except Exception: + reprs.append(repr(type)) + return "%r is not of type %s" % (instance, ", ".join(reprs)) + + +def flatten(suitable_for_isinstance): + """ + isinstance() can accept a bunch of really annoying different types: + * a single type + * a tuple of types + * an arbitrary nested tree of tuples + + Return a flattened tuple of the given argument. + + """ + + types = set() + + if not isinstance(suitable_for_isinstance, tuple): + suitable_for_isinstance = (suitable_for_isinstance,) + for thing in suitable_for_isinstance: + if isinstance(thing, tuple): + types.update(flatten(thing)) + else: + types.add(thing) + return tuple(types) + + +def ensure_list(thing): + """ + Wrap ``thing`` in a list if it's a single str. + + Otherwise, return it unchanged. + + """ + + if isinstance(thing, str_types): + return [thing] + return thing + + +def unbool(element, true=object(), false=object()): + """ + A hack to make True and 1 and False and 0 unique for ``uniq``. + + """ + + if element is True: + return true + elif element is False: + return false + return element + + +def uniq(container): + """ + Check if all of a container's elements are unique. + + Successively tries first to rely that the elements are hashable, then + falls back on them being sortable, and finally falls back on brute + force. + + """ + + try: + return len(set(unbool(i) for i in container)) == len(container) + except TypeError: + try: + sort = sorted(unbool(i) for i in container) + sliced = itertools.islice(sort, 1, None) + for i, j in zip(sort, sliced): + if i == j: + return False + except (NotImplementedError, TypeError): + seen = [] + for e in container: + e = unbool(e) + if e in seen: + return False + seen.append(e) + return True diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py new file mode 100644 index 0000000..8837d39 --- /dev/null +++ b/jsonschema/_validators.py @@ -0,0 +1,361 @@ +import re + +from jsonschema import _utils +from jsonschema.exceptions import FormatError, ValidationError +from jsonschema.compat import iteritems + + +def patternProperties(validator, patternProperties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for pattern, subschema in iteritems(patternProperties): + for k, v in iteritems(instance): + if re.search(pattern, k): + for error in validator.descend( + v, subschema, path=k, schema_path=pattern, + ): + yield error + + +def propertyNames(validator, propertyNames, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property in instance: + for error in validator.descend( + instance=property, + schema=propertyNames, + ): + yield error + + +def additionalProperties(validator, aP, instance, schema): + if not validator.is_type(instance, "object"): + return + + extras = set(_utils.find_additional_properties(instance, schema)) + + if validator.is_type(aP, "object"): + for extra in extras: + for error in validator.descend(instance[extra], aP, path=extra): + yield error + elif not aP and extras: + if "patternProperties" in schema: + patterns = sorted(schema["patternProperties"]) + if len(extras) == 1: + verb = "does" + else: + verb = "do" + error = "%s %s not match any of the regexes: %s" % ( + ", ".join(map(repr, sorted(extras))), + verb, + ", ".join(map(repr, patterns)), + ) + yield ValidationError(error) + else: + error = "Additional properties are not allowed (%s %s unexpected)" + yield ValidationError(error % _utils.extras_msg(extras)) + + +def items(validator, items, instance, schema): + if not validator.is_type(instance, "array"): + return + + if validator.is_type(items, "array"): + for (index, item), subschema in zip(enumerate(instance), items): + for error in validator.descend( + item, subschema, path=index, schema_path=index, + ): + yield error + else: + for index, item in enumerate(instance): + for error in validator.descend(item, items, path=index): + yield error + + +def additionalItems(validator, aI, instance, schema): + if ( + not validator.is_type(instance, "array") or + validator.is_type(schema.get("items", {}), "object") + ): + return + + len_items = len(schema.get("items", [])) + if validator.is_type(aI, "object"): + for index, item in enumerate(instance[len_items:], start=len_items): + for error in validator.descend(item, aI, path=index): + yield error + elif not aI and len(instance) > len(schema.get("items", [])): + error = "Additional items are not allowed (%s %s unexpected)" + yield ValidationError( + error % + _utils.extras_msg(instance[len(schema.get("items", [])):]) + ) + + +def const(validator, const, instance, schema): + if instance != const: + yield ValidationError("%r was expected" % (const,)) + + +def contains(validator, contains, instance, schema): + if not validator.is_type(instance, "array"): + return + + if not any(validator.is_valid(element, contains) for element in instance): + yield ValidationError( + "None of %r are valid under the given schema" % (instance,) + ) + + +def exclusiveMinimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance <= minimum: + yield ValidationError( + "%r is less than or equal to the minimum of %r" % ( + instance, minimum, + ), + ) + + +def exclusiveMaximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance >= maximum: + yield ValidationError( + "%r is greater than or equal to the maximum of %r" % ( + instance, maximum, + ), + ) + + +def minimum(validator, minimum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance < minimum: + yield ValidationError( + "%r is less than the minimum of %r" % (instance, minimum) + ) + + +def maximum(validator, maximum, instance, schema): + if not validator.is_type(instance, "number"): + return + + if instance > maximum: + yield ValidationError( + "%r is greater than the maximum of %r" % (instance, maximum) + ) + + +def multipleOf(validator, dB, instance, schema): + if not validator.is_type(instance, "number"): + return + + if isinstance(dB, float): + quotient = instance / dB + failed = int(quotient) != quotient + else: + failed = instance % dB + + if failed: + yield ValidationError("%r is not a multiple of %r" % (instance, dB)) + + +def minItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) < mI: + yield ValidationError("%r is too short" % (instance,)) + + +def maxItems(validator, mI, instance, schema): + if validator.is_type(instance, "array") and len(instance) > mI: + yield ValidationError("%r is too long" % (instance,)) + + +def uniqueItems(validator, uI, instance, schema): + if ( + uI and + validator.is_type(instance, "array") and + not _utils.uniq(instance) + ): + yield ValidationError("%r has non-unique elements" % (instance,)) + + +def pattern(validator, patrn, instance, schema): + if ( + validator.is_type(instance, "string") and + not re.search(patrn, instance) + ): + yield ValidationError("%r does not match %r" % (instance, patrn)) + + +def format(validator, format, instance, schema): + if validator.format_checker is not None: + try: + validator.format_checker.check(instance, format) + except FormatError as error: + yield ValidationError(error.message, cause=error.cause) + + +def minLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) < mL: + yield ValidationError("%r is too short" % (instance,)) + + +def maxLength(validator, mL, instance, schema): + if validator.is_type(instance, "string") and len(instance) > mL: + yield ValidationError("%r is too long" % (instance,)) + + +def dependencies(validator, dependencies, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, dependency in iteritems(dependencies): + if property not in instance: + continue + + if validator.is_type(dependency, "array"): + for each in dependency: + if each not in instance: + message = "%r is a dependency of %r" + yield ValidationError(message % (each, property)) + else: + for error in validator.descend( + instance, dependency, schema_path=property, + ): + yield error + + +def enum(validator, enums, instance, schema): + if instance not in enums: + yield ValidationError("%r is not one of %r" % (instance, enums)) + + +def ref(validator, ref, instance, schema): + resolve = getattr(validator.resolver, "resolve", None) + if resolve is None: + with validator.resolver.resolving(ref) as resolved: + for error in validator.descend(instance, resolved): + yield error + else: + scope, resolved = validator.resolver.resolve(ref) + validator.resolver.push_scope(scope) + + try: + for error in validator.descend(instance, resolved): + yield error + finally: + validator.resolver.pop_scope() + + +def type(validator, types, instance, schema): + types = _utils.ensure_list(types) + + if not any(validator.is_type(instance, type) for type in types): + yield ValidationError(_utils.types_msg(instance, types)) + + +def properties(validator, properties, instance, schema): + if not validator.is_type(instance, "object"): + return + + for property, subschema in iteritems(properties): + if property in instance: + for error in validator.descend( + instance[property], + subschema, + path=property, + schema_path=property, + ): + yield error + + +def required(validator, required, instance, schema): + if not validator.is_type(instance, "object"): + return + for property in required: + if property not in instance: + yield ValidationError("%r is a required property" % property) + + +def minProperties(validator, mP, instance, schema): + if validator.is_type(instance, "object") and len(instance) < mP: + yield ValidationError( + "%r does not have enough properties" % (instance,) + ) + + +def maxProperties(validator, mP, instance, schema): + if not validator.is_type(instance, "object"): + return + if validator.is_type(instance, "object") and len(instance) > mP: + yield ValidationError("%r has too many properties" % (instance,)) + + +def allOf(validator, allOf, instance, schema): + for index, subschema in enumerate(allOf): + for error in validator.descend(instance, subschema, schema_path=index): + yield error + + +def anyOf(validator, anyOf, instance, schema): + all_errors = [] + for index, subschema in enumerate(anyOf): + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + break + all_errors.extend(errs) + else: + yield ValidationError( + "%r is not valid under any of the given schemas" % (instance,), + context=all_errors, + ) + + +def oneOf(validator, oneOf, instance, schema): + subschemas = enumerate(oneOf) + all_errors = [] + for index, subschema in subschemas: + errs = list(validator.descend(instance, subschema, schema_path=index)) + if not errs: + first_valid = subschema + break + all_errors.extend(errs) + else: + yield ValidationError( + "%r is not valid under any of the given schemas" % (instance,), + context=all_errors, + ) + + more_valid = [s for i, s in subschemas if validator.is_valid(instance, s)] + if more_valid: + more_valid.append(first_valid) + reprs = ", ".join(repr(schema) for schema in more_valid) + yield ValidationError( + "%r is valid under each of %s" % (instance, reprs) + ) + + +def not_(validator, not_schema, instance, schema): + if validator.is_valid(instance, not_schema): + yield ValidationError( + "%r is not allowed for %r" % (not_schema, instance) + ) + + +def if_(validator, if_schema, instance, schema): + if validator.is_valid(instance, if_schema): + if u"then" in schema: + then = schema[u"then"] + for error in validator.descend(instance, then, schema_path="then"): + yield error + elif u"else" in schema: + else_ = schema[u"else"] + for error in validator.descend(instance, else_, schema_path="else"): + yield error diff --git a/jsonschema/benchmarks/__init__.py b/jsonschema/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/jsonschema/benchmarks/__init__.py diff --git a/jsonschema/benchmarks/issue232.py b/jsonschema/benchmarks/issue232.py new file mode 100644 index 0000000..460bb06 --- /dev/null +++ b/jsonschema/benchmarks/issue232.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +""" +A performance benchmark using the example from issue #232: + +https://github.com/Julian/jsonschema/pull/232 + +""" +from twisted.python.filepath import FilePath +from perf import Runner +from pyrsistent import m + +from jsonschema.tests._suite import Collection +import jsonschema + + +collection = Collection( + path=FilePath(__file__).sibling("issue232"), + remotes=m(), + name="issue232", + validator=jsonschema.Draft7Validator, +) + + +if __name__ == "__main__": + collection.benchmark(runner=Runner()) diff --git a/jsonschema/benchmarks/issue232/issue.json b/jsonschema/benchmarks/issue232/issue.json new file mode 100644 index 0000000..804c340 --- /dev/null +++ b/jsonschema/benchmarks/issue232/issue.json @@ -0,0 +1,2653 @@ +[ + { + "description": "Petstore", + "schema": { + "title": "A JSON Schema for Swagger 2.0 API.", + "id": "http://swagger.io/v2/schema.json#", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "required": [ + "swagger", + "info", + "paths" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "swagger": { + "type": "string", + "enum": [ + "2.0" + ], + "description": "The Swagger version of this document." + }, + "info": { + "$ref": "#/definitions/info" + }, + "host": { + "type": "string", + "pattern": "^[^{}/ :\\\\]+(?::\\d+)?$", + "description": "The host (name or ip) of the API. Example: 'swagger.io'" + }, + "basePath": { + "type": "string", + "pattern": "^/", + "description": "The base path to the API. Example: '/api'." + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "consumes": { + "description": "A list of MIME types accepted by the API.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "paths": { + "$ref": "#/definitions/paths" + }, + "definitions": { + "$ref": "#/definitions/definitions" + }, + "parameters": { + "$ref": "#/definitions/parameterDefinitions" + }, + "responses": { + "$ref": "#/definitions/responseDefinitions" + }, + "security": { + "$ref": "#/definitions/security" + }, + "securityDefinitions": { + "$ref": "#/definitions/securityDefinitions" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." + }, + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. GitHub Flavored Markdown is allowed." + }, + "termsOfService": { + "type": "string", + "description": "The terms of service for the API." + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "paths": { + "type": "object", + "description": "Relative paths to the individual endpoints. They must be relative to the 'basePath'.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + }, + "^/": { + "$ref": "#/definitions/pathItem" + } + }, + "additionalProperties": false + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "One or more JSON objects describing the schemas being consumed and produced by the API." + }, + "parameterDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "One or more JSON representations for parameters" + }, + "responseDefinitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/response" + }, + "description": "One or more JSON representations for parameters" + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "examples": { + "type": "object", + "additionalProperties": true + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the HTTP message." + }, + "operation": { + "type": "object", + "required": [ + "responses" + ], + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the operation." + }, + "description": { + "type": "string", + "description": "A longer description of the operation, GitHub Flavored Markdown is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string", + "description": "A unique identifier of the operation." + }, + "produces": { + "description": "A list of MIME types the API can produce.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "consumes": { + "description": "A list of MIME types the API can consume.", + "allOf": [ + { + "$ref": "#/definitions/mediaTypeList" + } + ] + }, + "parameters": { + "$ref": "#/definitions/parametersList" + }, + "responses": { + "$ref": "#/definitions/responses" + }, + "schemes": { + "$ref": "#/definitions/schemesList" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "security": { + "$ref": "#/definitions/security" + } + } + }, + "pathItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "get": { + "$ref": "#/definitions/operation" + }, + "put": { + "$ref": "#/definitions/operation" + }, + "post": { + "$ref": "#/definitions/operation" + }, + "delete": { + "$ref": "#/definitions/operation" + }, + "options": { + "$ref": "#/definitions/operation" + }, + "head": { + "$ref": "#/definitions/operation" + }, + "patch": { + "$ref": "#/definitions/operation" + }, + "parameters": { + "$ref": "#/definitions/parametersList" + } + } + }, + "responses": { + "type": "object", + "description": "Response objects names can either be any valid HTTP status code or 'default'.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^([0-9]{3})$|^(default)$": { + "$ref": "#/definitions/responseValue" + }, + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "not": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + } + }, + "responseValue": { + "oneOf": [ + { + "$ref": "#/definitions/response" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "response": { + "type": "object", + "required": [ + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "schema": { + "oneOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "$ref": "#/definitions/fileSchema" + } + ] + }, + "headers": { + "$ref": "#/definitions/headers" + }, + "examples": { + "$ref": "#/definitions/examples" + } + }, + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "headers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/header" + } + }, + "header": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "vendorExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "bodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "schema" + ], + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "body" + ] + }, + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "schema": { + "$ref": "#/definitions/schema" + } + }, + "additionalProperties": false + }, + "headerParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "header" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "queryParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "query" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "formDataParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "required": { + "type": "boolean", + "description": "Determines whether or not this parameter is required or optional.", + "default": false + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "formData" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "allowEmptyValue": { + "type": "boolean", + "default": false, + "description": "allows sending a parameter by name only or with an empty value." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array", + "file" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormatWithMulti" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "pathParameterSubSchema": { + "additionalProperties": false, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean", + "enum": [ + true + ], + "description": "Determines whether or not this parameter is required or optional." + }, + "in": { + "type": "string", + "description": "Determines the location of the parameter.", + "enum": [ + "path" + ] + }, + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "name": { + "type": "string", + "description": "The name of the parameter." + }, + "type": { + "type": "string", + "enum": [ + "string", + "number", + "boolean", + "integer", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + } + }, + "nonBodyParameter": { + "type": "object", + "required": [ + "name", + "in", + "type" + ], + "oneOf": [ + { + "$ref": "#/definitions/headerParameterSubSchema" + }, + { + "$ref": "#/definitions/formDataParameterSubSchema" + }, + { + "$ref": "#/definitions/queryParameterSubSchema" + }, + { + "$ref": "#/definitions/pathParameterSubSchema" + } + ] + }, + "parameter": { + "oneOf": [ + { + "$ref": "#/definitions/bodyParameter" + }, + { + "$ref": "#/definitions/nonBodyParameter" + } + ] + }, + "schema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "properties": { + "$ref": { + "type": "string" + }, + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "maxProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minProperties": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "type": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/type" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "discriminator": { + "type": "string" + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "xml": { + "$ref": "#/definitions/xml" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "fileSchema": { + "type": "object", + "description": "A deterministic version of a JSON Schema object.", + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + }, + "required": [ + "type" + ], + "properties": { + "format": { + "type": "string" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "required": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/stringArray" + }, + "type": { + "type": "string", + "enum": [ + "file" + ] + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "example": {} + }, + "additionalProperties": false + }, + "primitivesItems": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "array" + ] + }, + "format": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/primitivesItems" + }, + "collectionFormat": { + "$ref": "#/definitions/collectionFormat" + }, + "default": { + "$ref": "#/definitions/default" + }, + "maximum": { + "$ref": "#/definitions/maximum" + }, + "exclusiveMaximum": { + "$ref": "#/definitions/exclusiveMaximum" + }, + "minimum": { + "$ref": "#/definitions/minimum" + }, + "exclusiveMinimum": { + "$ref": "#/definitions/exclusiveMinimum" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + }, + "minLength": { + "$ref": "#/definitions/minLength" + }, + "pattern": { + "$ref": "#/definitions/pattern" + }, + "maxItems": { + "$ref": "#/definitions/maxItems" + }, + "minItems": { + "$ref": "#/definitions/minItems" + }, + "uniqueItems": { + "$ref": "#/definitions/uniqueItems" + }, + "enum": { + "$ref": "#/definitions/enum" + }, + "multipleOf": { + "$ref": "#/definitions/multipleOf" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/securityRequirement" + }, + "uniqueItems": true + }, + "securityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "xml": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "attribute": { + "type": "boolean", + "default": false + }, + "wrapped": { + "type": "boolean", + "default": false + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "securityDefinitions": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/definitions/basicAuthenticationSecurity" + }, + { + "$ref": "#/definitions/apiKeySecurity" + }, + { + "$ref": "#/definitions/oauth2ImplicitSecurity" + }, + { + "$ref": "#/definitions/oauth2PasswordSecurity" + }, + { + "$ref": "#/definitions/oauth2ApplicationSecurity" + }, + { + "$ref": "#/definitions/oauth2AccessCodeSecurity" + } + ] + } + }, + "basicAuthenticationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "basic" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "apiKeySecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ImplicitSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "implicit" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2PasswordSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "password" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2ApplicationSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "application" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2AccessCodeSecurity": { + "type": "object", + "additionalProperties": false, + "required": [ + "type", + "flow", + "authorizationUrl", + "tokenUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "flow": { + "type": "string", + "enum": [ + "accessCode" + ] + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + }, + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-": { + "$ref": "#/definitions/vendorExtension" + } + } + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "mediaTypeList": { + "type": "array", + "items": { + "$ref": "#/definitions/mimeType" + }, + "uniqueItems": true + }, + "parametersList": { + "type": "array", + "description": "The parameters needed to send a valid API call.", + "additionalItems": false, + "items": { + "oneOf": [ + { + "$ref": "#/definitions/parameter" + }, + { + "$ref": "#/definitions/jsonReference" + } + ] + }, + "uniqueItems": true + }, + "schemesList": { + "type": "array", + "description": "The transfer protocol of the API.", + "items": { + "type": "string", + "enum": [ + "http", + "https", + "ws", + "wss" + ] + }, + "uniqueItems": true + }, + "collectionFormat": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes" + ], + "default": "csv" + }, + "collectionFormatWithMulti": { + "type": "string", + "enum": [ + "csv", + "ssv", + "tsv", + "pipes", + "multi" + ], + "default": "csv" + }, + "title": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/title" + }, + "description": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/description" + }, + "default": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/default" + }, + "multipleOf": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/multipleOf" + }, + "maximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/maximum" + }, + "exclusiveMaximum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMaximum" + }, + "minimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/minimum" + }, + "exclusiveMinimum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/exclusiveMinimum" + }, + "maxLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minLength": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "pattern": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/pattern" + }, + "maxItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveInteger" + }, + "minItems": { + "$ref": "http://json-schema.org/draft-04/schema#/definitions/positiveIntegerDefault0" + }, + "uniqueItems": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/uniqueItems" + }, + "enum": { + "$ref": "http://json-schema.org/draft-04/schema#/properties/enum" + }, + "jsonReference": { + "type": "object", + "required": [ + "$ref" + ], + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string" + } + } + } + } + }, + "tests": [ + { + "description": "Example petsore", + "data": { + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } + }, + "valid": true + } + ] + } +] diff --git a/jsonschema/benchmarks/json_schema_test_suite.py b/jsonschema/benchmarks/json_schema_test_suite.py new file mode 100644 index 0000000..c4d3ccd --- /dev/null +++ b/jsonschema/benchmarks/json_schema_test_suite.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +""" +A performance benchmark using the official test suite. + +This benchmarks jsonschema using every valid example in the +JSON-Schema-Test-Suite. It will take some time to complete. +""" +from perf import Runner + +from jsonschema.tests._suite import Suite + + +if __name__ == "__main__": + Suite().benchmark(runner=Runner()) diff --git a/jsonschema/cli.py b/jsonschema/cli.py new file mode 100644 index 0000000..fb1b0f5 --- /dev/null +++ b/jsonschema/cli.py @@ -0,0 +1,81 @@ +from __future__ import absolute_import +import argparse +import json +import sys + +from jsonschema._reflect import namedAny +from jsonschema.validators import validator_for + + +def _namedAnyWithDefault(name): + if "." not in name: + name = "jsonschema." + name + return namedAny(name) + + +def _json_file(path): + with open(path) as file: + return json.load(file) + + +parser = argparse.ArgumentParser( + description="JSON Schema Validation CLI", +) +parser.add_argument( + "-i", "--instance", + action="append", + dest="instances", + type=_json_file, + help=( + "a path to a JSON instance (i.e. filename.json)" + "to validate (may be specified multiple times)" + ), +) +parser.add_argument( + "-F", "--error-format", + default="{error.instance}: {error.message}\n", + help=( + "the format to use for each error output message, specified in " + "a form suitable for passing to str.format, which will be called " + "with 'error' for each error" + ), +) +parser.add_argument( + "-V", "--validator", + type=_namedAnyWithDefault, + help=( + "the fully qualified object name of a validator to use, or, for " + "validators that are registered with jsonschema, simply the name " + "of the class." + ), +) +parser.add_argument( + "schema", + help="the JSON Schema to validate with (i.e. filename.schema)", + type=_json_file, +) + + +def parse_args(args): + arguments = vars(parser.parse_args(args=args or ["--help"])) + if arguments["validator"] is None: + arguments["validator"] = validator_for(arguments["schema"]) + return arguments + + +def main(args=sys.argv[1:]): + sys.exit(run(arguments=parse_args(args=args))) + + +def run(arguments, stdout=sys.stdout, stderr=sys.stderr): + error_format = arguments["error_format"] + validator = arguments["validator"](schema=arguments["schema"]) + + validator.check_schema(arguments["schema"]) + + errored = False + for instance in arguments["instances"] or (): + for error in validator.iter_errors(instance): + stderr.write(error_format.format(error=error)) + errored = True + return errored diff --git a/jsonschema/compat.py b/jsonschema/compat.py new file mode 100644 index 0000000..93492f9 --- /dev/null +++ b/jsonschema/compat.py @@ -0,0 +1,65 @@ +""" +Python 2/3 compatibility helpers. + +Note: This module is *not* public API. +""" +import contextlib +import operator +import sys + + +try: + from collections.abc import MutableMapping, Sequence # noqa +except ImportError: + from collections import MutableMapping, Sequence # noqa + +PY3 = sys.version_info[0] >= 3 + +if PY3: + zip = zip + from functools import lru_cache + from io import StringIO as NativeIO + from urllib.parse import ( + unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit + ) + from urllib.request import pathname2url, urlopen + str_types = str, + int_types = int, + iteritems = operator.methodcaller("items") +else: + from itertools import izip as zip # noqa + from io import BytesIO as NativeIO + from urlparse import ( + urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa + ) + from urllib import pathname2url, unquote # noqa + import urllib2 # noqa + def urlopen(*args, **kwargs): + return contextlib.closing(urllib2.urlopen(*args, **kwargs)) + + str_types = basestring + int_types = int, long + iteritems = operator.methodcaller("iteritems") + + from functools32 import lru_cache + + +# On python < 3.3 fragments are not handled properly with unknown schemes +def urlsplit(url): + scheme, netloc, path, query, fragment = _urlsplit(url) + if "#" in path: + path, fragment = path.split("#", 1) + return SplitResult(scheme, netloc, path, query, fragment) + + +def urldefrag(url): + if "#" in url: + s, n, p, q, frag = urlsplit(url) + defrag = urlunsplit((s, n, p, q, '')) + else: + defrag = url + frag = '' + return defrag, frag + + +# flake8: noqa diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py new file mode 100644 index 0000000..9008182 --- /dev/null +++ b/jsonschema/exceptions.py @@ -0,0 +1,300 @@ +from collections import defaultdict, deque +import itertools +import pprint +import textwrap + +import attr + +from jsonschema import _utils +from jsonschema.compat import PY3, iteritems + + +WEAK_MATCHES = frozenset(["anyOf", "oneOf"]) +STRONG_MATCHES = frozenset() + +_unset = _utils.Unset() + + +class _Error(Exception): + def __init__( + self, + message, + validator=_unset, + path=(), + cause=None, + context=(), + validator_value=_unset, + instance=_unset, + schema=_unset, + schema_path=(), + parent=None, + ): + super(_Error, self).__init__( + message, + validator, + path, + cause, + context, + validator_value, + instance, + schema, + schema_path, + parent, + ) + self.message = message + self.path = self.relative_path = deque(path) + self.schema_path = self.relative_schema_path = deque(schema_path) + self.context = list(context) + self.cause = self.__cause__ = cause + self.validator = validator + self.validator_value = validator_value + self.instance = instance + self.schema = schema + self.parent = parent + + for error in context: + error.parent = self + + def __repr__(self): + return "<%s: %r>" % (self.__class__.__name__, self.message) + + def __unicode__(self): + essential_for_verbose = ( + self.validator, self.validator_value, self.instance, self.schema, + ) + if any(m is _unset for m in essential_for_verbose): + return self.message + + pschema = pprint.pformat(self.schema, width=72) + pinstance = pprint.pformat(self.instance, width=72) + return self.message + textwrap.dedent(""" + + Failed validating %r in %s%s: + %s + + On %s%s: + %s + """.rstrip() + ) % ( + self.validator, + self._word_for_schema_in_error_message, + _utils.format_as_index(list(self.relative_schema_path)[:-1]), + _utils.indent(pschema), + self._word_for_instance_in_error_message, + _utils.format_as_index(self.relative_path), + _utils.indent(pinstance), + ) + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") + + @classmethod + def create_from(cls, other): + return cls(**other._contents()) + + @property + def absolute_path(self): + parent = self.parent + if parent is None: + return self.relative_path + + path = deque(self.relative_path) + path.extendleft(reversed(parent.absolute_path)) + return path + + @property + def absolute_schema_path(self): + parent = self.parent + if parent is None: + return self.relative_schema_path + + path = deque(self.relative_schema_path) + path.extendleft(reversed(parent.absolute_schema_path)) + return path + + def _set(self, **kwargs): + for k, v in iteritems(kwargs): + if getattr(self, k) is _unset: + setattr(self, k, v) + + def _contents(self): + attrs = ( + "message", "cause", "context", "validator", "validator_value", + "path", "schema_path", "instance", "schema", "parent", + ) + return dict((attr, getattr(self, attr)) for attr in attrs) + + +class ValidationError(_Error): + _word_for_schema_in_error_message = "schema" + _word_for_instance_in_error_message = "instance" + + +class SchemaError(_Error): + _word_for_schema_in_error_message = "metaschema" + _word_for_instance_in_error_message = "schema" + + +@attr.s(hash=True) +class RefResolutionError(Exception): + + _cause = attr.ib() + + def __str__(self): + return str(self._cause) + + +class UndefinedTypeCheck(Exception): + def __init__(self, type): + self.type = type + + def __unicode__(self): + return "Type %r is unknown to this type checker" % self.type + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") + + +class UnknownType(Exception): + def __init__(self, type, instance, schema): + self.type = type + self.instance = instance + self.schema = schema + + def __unicode__(self): + pschema = pprint.pformat(self.schema, width=72) + pinstance = pprint.pformat(self.instance, width=72) + return textwrap.dedent(""" + Unknown type %r for validator with schema: + %s + + While checking instance: + %s + """.rstrip() + ) % (self.type, _utils.indent(pschema), _utils.indent(pinstance)) + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return unicode(self).encode("utf-8") + + +class FormatError(Exception): + def __init__(self, message, cause=None): + super(FormatError, self).__init__(message, cause) + self.message = message + self.cause = self.__cause__ = cause + + def __unicode__(self): + return self.message + + if PY3: + __str__ = __unicode__ + else: + def __str__(self): + return self.message.encode("utf-8") + + +class ErrorTree(object): + """ + ErrorTrees make it easier to check which validations failed. + + """ + + _instance = _unset + + def __init__(self, errors=()): + self.errors = {} + self._contents = defaultdict(self.__class__) + + for error in errors: + container = self + for element in error.path: + container = container[element] + container.errors[error.validator] = error + + container._instance = error.instance + + def __contains__(self, index): + """ + Check whether ``instance[index]`` has any errors. + + """ + + return index in self._contents + + def __getitem__(self, index): + """ + Retrieve the child tree one level down at the given ``index``. + + If the index is not in the instance that this tree corresponds to and + is not known by this tree, whatever error would be raised by + ``instance.__getitem__`` will be propagated (usually this is some + subclass of `exceptions.LookupError`. + + """ + + if self._instance is not _unset and index not in self: + self._instance[index] + return self._contents[index] + + def __setitem__(self, index, value): + self._contents[index] = value + + def __iter__(self): + """ + Iterate (non-recursively) over the indices in the instance with errors. + + """ + + return iter(self._contents) + + def __len__(self): + """ + Same as `total_errors`. + + """ + + return self.total_errors + + def __repr__(self): + return "<%s (%s total errors)>" % (self.__class__.__name__, len(self)) + + @property + def total_errors(self): + """ + The total number of errors in the entire tree, including children. + + """ + + child_errors = sum(len(tree) for _, tree in iteritems(self._contents)) + return len(self.errors) + child_errors + + +def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): + def relevance(error): + validator = error.validator + return -len(error.path), validator not in weak, validator in strong + return relevance + + +relevance = by_relevance() + + +def best_match(errors, key=relevance): + errors = iter(errors) + best = next(errors, None) + if best is None: + return + best = max(itertools.chain([best], errors), key=key) + + while best.context: + best = min(best.context, key=key) + return best diff --git a/jsonschema/schemas/draft3.json b/jsonschema/schemas/draft3.json new file mode 100644 index 0000000..f8a09c5 --- /dev/null +++ b/jsonschema/schemas/draft3.json @@ -0,0 +1,199 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "dependencies": { + "exclusiveMaximum": "maximum", + "exclusiveMinimum": "minimum" + }, + "id": "http://json-schema.org/draft-03/schema#", + "properties": { + "$ref": { + "format": "uri", + "type": "string" + }, + "$schema": { + "format": "uri", + "type": "string" + }, + "additionalItems": { + "default": {}, + "type": [ + { + "$ref": "#" + }, + "boolean" + ] + }, + "additionalProperties": { + "default": {}, + "type": [ + { + "$ref": "#" + }, + "boolean" + ] + }, + "default": { + "type": "any" + }, + "dependencies": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": [ + "string", + "array", + { + "$ref": "#" + } + ] + }, + "default": {}, + "type": [ + "string", + "array", + "object" + ] + }, + "description": { + "type": "string" + }, + "disallow": { + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "type": [ + "string", + "array" + ], + "uniqueItems": true + }, + "divisibleBy": { + "default": 1, + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "enum": { + "type": "array" + }, + "exclusiveMaximum": { + "default": false, + "type": "boolean" + }, + "exclusiveMinimum": { + "default": false, + "type": "boolean" + }, + "extends": { + "default": {}, + "items": { + "$ref": "#" + }, + "type": [ + { + "$ref": "#" + }, + "array" + ] + }, + "format": { + "type": "string" + }, + "id": { + "format": "uri", + "type": "string" + }, + "items": { + "default": {}, + "items": { + "$ref": "#" + }, + "type": [ + { + "$ref": "#" + }, + "array" + ] + }, + "maxDecimal": { + "minimum": 0, + "type": "number" + }, + "maxItems": { + "minimum": 0, + "type": "integer" + }, + "maxLength": { + "type": "integer" + }, + "maximum": { + "type": "number" + }, + "minItems": { + "default": 0, + "minimum": 0, + "type": "integer" + }, + "minLength": { + "default": 0, + "minimum": 0, + "type": "integer" + }, + "minimum": { + "type": "number" + }, + "pattern": { + "format": "regex", + "type": "string" + }, + "patternProperties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "properties": { + "additionalProperties": { + "$ref": "#", + "type": "object" + }, + "default": {}, + "type": "object" + }, + "required": { + "default": false, + "type": "boolean" + }, + "title": { + "type": "string" + }, + "type": { + "default": "any", + "items": { + "type": [ + "string", + { + "$ref": "#" + } + ] + }, + "type": [ + "string", + "array" + ], + "uniqueItems": true + }, + "uniqueItems": { + "default": false, + "type": "boolean" + } + }, + "type": "object" +} diff --git a/jsonschema/schemas/draft4.json b/jsonschema/schemas/draft4.json new file mode 100644 index 0000000..9b666cf --- /dev/null +++ b/jsonschema/schemas/draft4.json @@ -0,0 +1,222 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "default": {}, + "definitions": { + "positiveInteger": { + "minimum": 0, + "type": "integer" + }, + "positiveIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/positiveInteger" + }, + { + "default": 0 + } + ] + }, + "schemaArray": { + "items": { + "$ref": "#" + }, + "minItems": 1, + "type": "array" + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "items": { + "type": "string" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + }, + "dependencies": { + "exclusiveMaximum": [ + "maximum" + ], + "exclusiveMinimum": [ + "minimum" + ] + }, + "description": "Core schema meta-schema", + "id": "http://json-schema.org/draft-04/schema#", + "properties": { + "$schema": { + "format": "uri", + "type": "string" + }, + "additionalItems": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#" + } + ], + "default": {} + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "default": {}, + "definitions": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "dependencies": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + }, + "type": "object" + }, + "description": { + "type": "string" + }, + "enum": { + "type": "array" + }, + "exclusiveMaximum": { + "default": false, + "type": "boolean" + }, + "exclusiveMinimum": { + "default": false, + "type": "boolean" + }, + "format": { + "type": "string" + }, + "id": { + "format": "uri", + "type": "string" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": {} + }, + "maxItems": { + "$ref": "#/definitions/positiveInteger" + }, + "maxLength": { + "$ref": "#/definitions/positiveInteger" + }, + "maxProperties": { + "$ref": "#/definitions/positiveInteger" + }, + "maximum": { + "type": "number" + }, + "minItems": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minLength": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minProperties": { + "$ref": "#/definitions/positiveIntegerDefault0" + }, + "minimum": { + "type": "number" + }, + "multipleOf": { + "exclusiveMinimum": true, + "minimum": 0, + "type": "number" + }, + "not": { + "$ref": "#" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "pattern": { + "format": "regex", + "type": "string" + }, + "patternProperties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "properties": { + "additionalProperties": { + "$ref": "#" + }, + "default": {}, + "type": "object" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "title": { + "type": "string" + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "type": "array", + "uniqueItems": true + } + ] + }, + "uniqueItems": { + "default": false, + "type": "boolean" + } + }, + "type": "object" +} diff --git a/jsonschema/schemas/draft6.json b/jsonschema/schemas/draft6.json new file mode 100644 index 0000000..a0d2bf7 --- /dev/null +++ b/jsonschema/schemas/draft6.json @@ -0,0 +1,153 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array" + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/jsonschema/schemas/draft7.json b/jsonschema/schemas/draft7.json new file mode 100644 index 0000000..746cde9 --- /dev/null +++ b/jsonschema/schemas/draft7.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/jsonschema/tests/__init__.py b/jsonschema/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/jsonschema/tests/__init__.py diff --git a/jsonschema/tests/_suite.py b/jsonschema/tests/_suite.py new file mode 100644 index 0000000..8db0519 --- /dev/null +++ b/jsonschema/tests/_suite.py @@ -0,0 +1,237 @@ +""" +Python representations of the JSON Schema Test Suite tests. + +""" + +import json +import os +import re +import subprocess +import sys +import unittest + +from twisted.python.filepath import FilePath +import attr + +from jsonschema.compat import PY3 +from jsonschema.validators import validators +import jsonschema + + +def _find_suite(): + root = os.environ.get("JSON_SCHEMA_TEST_SUITE") + if root is not None: + return FilePath(root) + + root = FilePath(jsonschema.__file__).parent().sibling("json") + if not root.isdir(): # pragma: no cover + raise ValueError( + ( + "Can't find the JSON-Schema-Test-Suite directory. " + "Set the 'JSON_SCHEMA_TEST_SUITE' environment " + "variable or run the tests from alongside a checkout " + "of the suite." + ), + ) + return root + + +@attr.s(hash=True) +class Suite(object): + + _root = attr.ib(default=attr.Factory(_find_suite)) + + def _remotes(self): + jsonschema_suite = self._root.descendant(["bin", "jsonschema_suite"]) + remotes = subprocess.check_output( + [sys.executable, jsonschema_suite.path, "remotes"], + ) + return { + "http://localhost:1234/" + name: schema + for name, schema in json.loads(remotes.decode("utf-8")).items() + } + + def benchmark(self, runner): # pragma: no cover + for name in validators: + self.version(name=name).benchmark(runner=runner) + + def version(self, name): + return Version( + name=name, + path=self._root.descendant(["tests", name]), + remotes=self._remotes(), + ) + + +@attr.s(hash=True) +class Version(object): + + _path = attr.ib() + _remotes = attr.ib() + + name = attr.ib() + + def benchmark(self, runner): # pragma: no cover + for suite in self.tests(): + for test in suite: + runner.bench_func( + name=test.fully_qualified_name, + func=test.validate_ignoring_errors, + ) + + def tests(self): + return ( + test + for child in self._path.globChildren("*.json") + for test in self._tests_in( + subject=child.basename()[:-5], + path=child, + ) + ) + + def format_tests(self): + path = self._path.descendant(["optional", "format"]) + return ( + test + for child in path.globChildren("*.json") + for test in self._tests_in( + subject=child.basename()[:-5], + path=child, + ) + ) + + def tests_of(self, name): + return self._tests_in( + subject=name, + path=self._path.child(name + ".json"), + ) + + def optional_tests_of(self, name): + return self._tests_in( + subject=name, + path=self._path.descendant(["optional", name + ".json"]), + ) + + def to_unittest_testcase(self, *suites, **kwargs): + name = kwargs.pop("name", "Test" + self.name.title()) + methods = { + test.method_name: test.to_unittest_method(**kwargs) + for suite in suites + for tests in suite + for test in tests + } + cls = type(name, (unittest.TestCase,), methods) + + try: + cls.__module__ = _someone_save_us_the_module_of_the_caller() + except Exception: # pragma: no cover + # We're doing crazy things, so if they go wrong, like a function + # behaving differently on some other interpreter, just make them + # not happen. + pass + + return cls + + def _tests_in(self, subject, path): + for each in json.loads(path.getContent().decode("utf-8")): + yield ( + _Test( + version=self, + subject=subject, + case_description=each["description"], + schema=each["schema"], + remotes=self._remotes, + **test + ) for test in each["tests"] + ) + + +@attr.s(hash=True, repr=False) +class _Test(object): + + version = attr.ib() + + subject = attr.ib() + case_description = attr.ib() + description = attr.ib() + + data = attr.ib() + schema = attr.ib(repr=False) + + valid = attr.ib() + + _remotes = attr.ib() + + def __repr__(self): # pragma: no cover + return "<Test {}>".format(self.fully_qualified_name) + + @property + def fully_qualified_name(self): # pragma: no cover + return " > ".join( + [ + self.version.name, + self.subject, + self.case_description, + self.description, + ] + ) + + @property + def method_name(self): + delimiters = r"[\W\- ]+" + name = "test_%s_%s_%s" % ( + re.sub(delimiters, "_", self.subject), + re.sub(delimiters, "_", self.case_description), + re.sub(delimiters, "_", self.description), + ) + + if not PY3: # pragma: no cover + name = name.encode("utf-8") + return name + + def to_unittest_method(self, skip=lambda test: None, **kwargs): + if self.valid: + def fn(this): + self.validate(**kwargs) + else: + def fn(this): + with this.assertRaises(jsonschema.ValidationError): + self.validate(**kwargs) + + fn.__name__ = self.method_name + reason = skip(self) + return unittest.skipIf(reason is not None, reason)(fn) + + def validate(self, Validator=None, **kwargs): + resolver = jsonschema.RefResolver.from_schema( + schema=self.schema, store=self._remotes, + ) + jsonschema.validate( + instance=self.data, + schema=self.schema, + cls=Validator, + resolver=resolver, + **kwargs + ) + + def validate_ignoring_errors(self, **kwargs): # pragma: no cover + try: + self.validate(**kwargs) + except jsonschema.ValidationError: + pass + + +def _someone_save_us_the_module_of_the_caller(): + """ + The FQON of the module 2nd stack frames up from here. + + This is intended to allow us to dynamicallly return test case classes that + are indistinguishable from being defined in the module that wants them. + + Otherwise, trial will mis-print the FQON, and copy pasting it won't re-run + the class that really is running. + + Save us all, this is all so so so so so terrible. + """ + + return sys._getframe(2).f_globals["__name__"] diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py new file mode 100644 index 0000000..a80be08 --- /dev/null +++ b/jsonschema/tests/test_cli.py @@ -0,0 +1,141 @@ +from unittest import TestCase +import json + +from jsonschema import Draft4Validator, ValidationError, cli +from jsonschema.compat import NativeIO +from jsonschema.exceptions import SchemaError + + +def fake_validator(*errors): + errors = list(reversed(errors)) + + class FakeValidator(object): + def __init__(self, *args, **kwargs): + pass + + def iter_errors(self, instance): + if errors: + return errors.pop() + return [] + + def check_schema(self, schema): + pass + + return FakeValidator + + +class TestParser(TestCase): + + FakeValidator = fake_validator() + instance_file = "foo.json" + schema_file = "schema.json" + + def setUp(self): + cli.open = self.fake_open + self.addCleanup(delattr, cli, "open") + + def fake_open(self, path): + if path == self.instance_file: + contents = "" + elif path == self.schema_file: + contents = {} + else: # pragma: no cover + self.fail("What is {!r}".format(path)) + return NativeIO(json.dumps(contents)) + + def test_find_validator_by_fully_qualified_object_name(self): + arguments = cli.parse_args( + [ + "--validator", + "jsonschema.tests.test_cli.TestParser.FakeValidator", + "--instance", self.instance_file, + self.schema_file, + ] + ) + self.assertIs(arguments["validator"], self.FakeValidator) + + def test_find_validator_in_jsonschema(self): + arguments = cli.parse_args( + [ + "--validator", "Draft4Validator", + "--instance", self.instance_file, + self.schema_file, + ] + ) + self.assertIs(arguments["validator"], Draft4Validator) + + +class TestCLI(TestCase): + def test_draft3_schema_draft4_validator(self): + stdout, stderr = NativeIO(), NativeIO() + with self.assertRaises(SchemaError): + cli.run( + { + "validator": Draft4Validator, + "schema": { + "anyOf": [ + {"minimum": 20}, + {"type": "string"}, + {"required": True}, + ], + }, + "instances": [1], + "error_format": "{error.message}", + }, + stdout=stdout, + stderr=stderr, + ) + + def test_successful_validation(self): + stdout, stderr = NativeIO(), NativeIO() + exit_code = cli.run( + { + "validator": fake_validator(), + "schema": {}, + "instances": [1], + "error_format": "{error.message}", + }, + stdout=stdout, + stderr=stderr, + ) + self.assertFalse(stdout.getvalue()) + self.assertFalse(stderr.getvalue()) + self.assertEqual(exit_code, 0) + + def test_unsuccessful_validation(self): + error = ValidationError("I am an error!", instance=1) + stdout, stderr = NativeIO(), NativeIO() + exit_code = cli.run( + { + "validator": fake_validator([error]), + "schema": {}, + "instances": [1], + "error_format": "{error.instance} - {error.message}", + }, + stdout=stdout, + stderr=stderr, + ) + self.assertFalse(stdout.getvalue()) + self.assertEqual(stderr.getvalue(), "1 - I am an error!") + self.assertEqual(exit_code, 1) + + def test_unsuccessful_validation_multiple_instances(self): + first_errors = [ + ValidationError("9", instance=1), + ValidationError("8", instance=1), + ] + second_errors = [ValidationError("7", instance=2)] + stdout, stderr = NativeIO(), NativeIO() + exit_code = cli.run( + { + "validator": fake_validator(first_errors, second_errors), + "schema": {}, + "instances": [1, 2], + "error_format": "{error.instance} - {error.message}\t", + }, + stdout=stdout, + stderr=stderr, + ) + self.assertFalse(stdout.getvalue()) + self.assertEqual(stderr.getvalue(), "1 - 9\t1 - 8\t2 - 7\t") + self.assertEqual(exit_code, 1) diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py new file mode 100644 index 0000000..e83801f --- /dev/null +++ b/jsonschema/tests/test_exceptions.py @@ -0,0 +1,468 @@ +from unittest import TestCase +import textwrap + +from jsonschema import Draft4Validator, exceptions +from jsonschema.compat import PY3 + + +class TestBestMatch(TestCase): + def best_match(self, errors): + errors = list(errors) + best = exceptions.best_match(errors) + reversed_best = exceptions.best_match(reversed(errors)) + msg = "Didn't return a consistent best match!\nGot: {0}\n\nThen: {1}" + self.assertEqual( + best._contents(), reversed_best._contents(), + msg=msg.format(best, reversed_best), + ) + return best + + def test_shallower_errors_are_better_matches(self): + validator = Draft4Validator( + { + "properties": { + "foo": { + "minProperties": 2, + "properties": {"bar": {"type": "object"}}, + }, + }, + }, + ) + best = self.best_match(validator.iter_errors({"foo": {"bar": []}})) + self.assertEqual(best.validator, "minProperties") + + def test_oneOf_and_anyOf_are_weak_matches(self): + """ + A property you *must* match is probably better than one you have to + match a part of. + + """ + + validator = Draft4Validator( + { + "minProperties": 2, + "anyOf": [{"type": "string"}, {"type": "number"}], + "oneOf": [{"type": "string"}, {"type": "number"}], + } + ) + best = self.best_match(validator.iter_errors({})) + self.assertEqual(best.validator, "minProperties") + + def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self): + """ + If the most relevant error is an anyOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + + """ + + validator = Draft4Validator( + { + "properties": { + "foo": { + "anyOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + }, + ) + best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) + self.assertEqual(best.validator_value, "array") + + def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self): + """ + If the most relevant error is an oneOf, then we traverse its context + and select the otherwise *least* relevant error, since in this case + that means the most specific, deep, error inside the instance. + + I.e. since only one of the schemas must match, we look for the most + relevant one. + + """ + + validator = Draft4Validator( + { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + }, + ) + best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) + self.assertEqual(best.validator_value, "array") + + def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self): + """ + Now, if the error is allOf, we traverse but select the *most* relevant + error from the context, because all schemas here must match anyways. + + """ + + validator = Draft4Validator( + { + "properties": { + "foo": { + "allOf": [ + {"type": "string"}, + {"properties": {"bar": {"type": "array"}}}, + ], + }, + }, + }, + ) + best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) + self.assertEqual(best.validator_value, "string") + + def test_nested_context_for_oneOf(self): + validator = Draft4Validator( + { + "properties": { + "foo": { + "oneOf": [ + {"type": "string"}, + { + "oneOf": [ + {"type": "string"}, + { + "properties": { + "bar": {"type": "array"}, + }, + }, + ], + }, + ], + }, + }, + }, + ) + best = self.best_match(validator.iter_errors({"foo": {"bar": 12}})) + self.assertEqual(best.validator_value, "array") + + def test_one_error(self): + validator = Draft4Validator({"minProperties": 2}) + error, = validator.iter_errors({}) + self.assertEqual( + exceptions.best_match(validator.iter_errors({})).validator, + "minProperties", + ) + + def test_no_errors(self): + validator = Draft4Validator({}) + self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) + + +class TestByRelevance(TestCase): + def test_short_paths_are_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=["baz"]) + deep = exceptions.ValidationError("Oh yes!", path=["foo", "bar"]) + match = max([shallow, deep], key=exceptions.relevance) + self.assertIs(match, shallow) + + match = max([deep, shallow], key=exceptions.relevance) + self.assertIs(match, shallow) + + def test_global_errors_are_even_better_matches(self): + shallow = exceptions.ValidationError("Oh no!", path=[]) + deep = exceptions.ValidationError("Oh yes!", path=["foo"]) + + errors = sorted([shallow, deep], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + errors = sorted([deep, shallow], key=exceptions.relevance) + self.assertEqual( + [list(error.path) for error in errors], + [["foo"], []], + ) + + def test_weak_validators_are_lower_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + + best_match = exceptions.by_relevance(weak="a") + + match = max([weak, normal], key=best_match) + self.assertIs(match, normal) + + match = max([normal, weak], key=best_match) + self.assertIs(match, normal) + + def test_strong_validators_are_higher_priority(self): + weak = exceptions.ValidationError("Oh no!", path=[], validator="a") + normal = exceptions.ValidationError("Oh yes!", path=[], validator="b") + strong = exceptions.ValidationError("Oh fine!", path=[], validator="c") + + best_match = exceptions.by_relevance(weak="a", strong="c") + + match = max([weak, normal, strong], key=best_match) + self.assertIs(match, strong) + + match = max([strong, normal, weak], key=best_match) + self.assertIs(match, strong) + + +class TestErrorTree(TestCase): + def test_it_knows_how_many_total_errors_it_contains(self): + # FIXME: https://github.com/Julian/jsonschema/issues/442 + errors = [ + exceptions.ValidationError("Something", validator=i) + for i in range(8) + ] + tree = exceptions.ErrorTree(errors) + self.assertEqual(tree.total_errors, 8) + + def test_it_contains_an_item_if_the_item_had_an_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertIn("bar", tree) + + def test_it_does_not_contain_an_item_if_the_item_had_no_error(self): + errors = [exceptions.ValidationError("a message", path=["bar"])] + tree = exceptions.ErrorTree(errors) + self.assertNotIn("foo", tree) + + def test_validators_that_failed_appear_in_errors_dict(self): + error = exceptions.ValidationError("a message", validator="foo") + tree = exceptions.ErrorTree([error]) + self.assertEqual(tree.errors, {"foo": error}) + + def test_it_creates_a_child_tree_for_each_nested_path(self): + errors = [ + exceptions.ValidationError("a bar message", path=["bar"]), + exceptions.ValidationError("a bar -> 0 message", path=["bar", 0]), + ] + tree = exceptions.ErrorTree(errors) + self.assertIn(0, tree["bar"]) + self.assertNotIn(1, tree["bar"]) + + def test_children_have_their_errors_dicts_built(self): + e1, e2 = ( + exceptions.ValidationError("1", validator="foo", path=["bar", 0]), + exceptions.ValidationError("2", validator="quux", path=["bar", 0]), + ) + tree = exceptions.ErrorTree([e1, e2]) + self.assertEqual(tree["bar"][0].errors, {"foo": e1, "quux": e2}) + + def test_multiple_errors_with_instance(self): + e1, e2 = ( + exceptions.ValidationError( + "1", + validator="foo", + path=["bar", "bar2"], + instance="i1"), + exceptions.ValidationError( + "2", + validator="quux", + path=["foobar", 2], + instance="i2"), + ) + exceptions.ErrorTree([e1, e2]) + + def test_it_does_not_contain_subtrees_that_are_not_in_the_instance(self): + error = exceptions.ValidationError("123", validator="foo", instance=[]) + tree = exceptions.ErrorTree([error]) + + with self.assertRaises(IndexError): + tree[0] + + def test_if_its_in_the_tree_anyhow_it_does_not_raise_an_error(self): + """ + If a validator is dumb (like :validator:`required` in draft 3) and + refers to a path that isn't in the instance, the tree still properly + returns a subtree for that path. + + """ + + error = exceptions.ValidationError( + "a message", validator="foo", instance={}, path=["foo"], + ) + tree = exceptions.ErrorTree([error]) + self.assertIsInstance(tree["foo"], exceptions.ErrorTree) + + +class TestErrorInitReprStr(TestCase): + def make_error(self, **kwargs): + defaults = dict( + message=u"hello", + validator=u"type", + validator_value=u"string", + instance=5, + schema={u"type": u"string"}, + ) + defaults.update(kwargs) + return exceptions.ValidationError(**defaults) + + def assertShows(self, expected, **kwargs): + if PY3: # pragma: no cover + expected = expected.replace("u'", "'") + expected = textwrap.dedent(expected).rstrip("\n") + + error = self.make_error(**kwargs) + message_line, _, rest = str(error).partition("\n") + self.assertEqual(message_line, error.message) + self.assertEqual(rest, expected) + + def test_it_calls_super_and_sets_args(self): + error = self.make_error() + self.assertGreater(len(error.args), 1) + + def test_repr(self): + self.assertEqual( + repr(exceptions.ValidationError(message="Hello!")), + "<ValidationError: %r>" % "Hello!", + ) + + def test_unset_error(self): + error = exceptions.ValidationError("message") + self.assertEqual(str(error), "message") + + kwargs = { + "validator": "type", + "validator_value": "string", + "instance": 5, + "schema": {"type": "string"}, + } + # Just the message should show if any of the attributes are unset + for attr in kwargs: + k = dict(kwargs) + del k[attr] + error = exceptions.ValidationError("message", **k) + self.assertEqual(str(error), "message") + + def test_empty_paths(self): + self.assertShows( + """ + Failed validating u'type' in schema: + {u'type': u'string'} + + On instance: + 5 + """, + path=[], + schema_path=[], + ) + + def test_one_item_paths(self): + self.assertShows( + """ + Failed validating u'type' in schema: + {u'type': u'string'} + + On instance[0]: + 5 + """, + path=[0], + schema_path=["items"], + ) + + def test_multiple_item_paths(self): + self.assertShows( + """ + Failed validating u'type' in schema[u'items'][0]: + {u'type': u'string'} + + On instance[0][u'a']: + 5 + """, + path=[0, u"a"], + schema_path=[u"items", 0, 1], + ) + + def test_uses_pprint(self): + self.assertShows( + """ + Failed validating u'maxLength' in schema: + {0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + 10: 10, + 11: 11, + 12: 12, + 13: 13, + 14: 14, + 15: 15, + 16: 16, + 17: 17, + 18: 18, + 19: 19} + + On instance: + [0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24] + """, + instance=list(range(25)), + schema=dict(zip(range(20), range(20))), + validator=u"maxLength", + ) + + def test_str_works_with_instances_having_overriden_eq_operator(self): + """ + Check for https://github.com/Julian/jsonschema/issues/164 which + rendered exceptions unusable when a `ValidationError` involved + instances with an `__eq__` method that returned truthy values. + + """ + + class DontEQMeBro(object): + def __eq__(this, other): # pragma: no cover + self.fail("Don't!") + + def __ne__(this, other): # pragma: no cover + self.fail("Don't!") + + instance = DontEQMeBro() + error = exceptions.ValidationError( + "a message", + validator="foo", + instance=instance, + validator_value="some", + schema="schema", + ) + self.assertIn(repr(instance), str(error)) + + +class TestHashable(TestCase): + def test_hashable(self): + set([exceptions.ValidationError("")]) + set([exceptions.SchemaError("")]) diff --git a/jsonschema/tests/test_format.py b/jsonschema/tests/test_format.py new file mode 100644 index 0000000..e9e5f99 --- /dev/null +++ b/jsonschema/tests/test_format.py @@ -0,0 +1,80 @@ +""" +Tests for the parts of jsonschema related to the :validator:`format` property. + +""" + +from unittest import TestCase + +from jsonschema import FormatError, ValidationError, FormatChecker +from jsonschema.validators import Draft4Validator + + +BOOM = ValueError("Boom!") +BANG = ZeroDivisionError("Bang!") + + +def boom(thing): + if thing == "bang": + raise BANG + raise BOOM + + +class TestFormatChecker(TestCase): + def test_it_can_validate_no_formats(self): + checker = FormatChecker(formats=()) + self.assertFalse(checker.checkers) + + def test_it_raises_a_key_error_for_unknown_formats(self): + with self.assertRaises(KeyError): + FormatChecker(formats=["o noes"]) + + def test_it_can_register_cls_checkers(self): + original = dict(FormatChecker.checkers) + self.addCleanup(FormatChecker.checkers.pop, "boom") + FormatChecker.cls_checks("boom")(boom) + self.assertEqual( + FormatChecker.checkers, + dict(original, boom=(boom, ())), + ) + + def test_it_can_register_checkers(self): + checker = FormatChecker() + checker.checks("boom")(boom) + self.assertEqual( + checker.checkers, + dict(FormatChecker.checkers, boom=(boom, ())) + ) + + def test_it_catches_registered_errors(self): + checker = FormatChecker() + checker.checks("boom", raises=type(BOOM))(boom) + + with self.assertRaises(FormatError) as cm: + checker.check(instance=12, format="boom") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + + # Unregistered errors should not be caught + with self.assertRaises(type(BANG)): + checker.check(instance="bang", format="boom") + + def test_format_error_causes_become_validation_error_causes(self): + checker = FormatChecker() + checker.checks("boom", raises=ValueError)(boom) + validator = Draft4Validator({"format": "boom"}, format_checker=checker) + + with self.assertRaises(ValidationError) as cm: + validator.validate("BOOM") + + self.assertIs(cm.exception.cause, BOOM) + self.assertIs(cm.exception.__cause__, BOOM) + + def test_format_checkers_come_with_defaults(self): + # This is bad :/ but relied upon. + # The docs for quite awhile recommended people do things like + # validate(..., format_checker=FormatChecker()) + # We should change that, but we can't without deprecation... + checker = FormatChecker() + with self.assertRaises(FormatError): + checker.check(instance="not-an-ipv4", format="ipv4") diff --git a/jsonschema/tests/test_jsonschema_test_suite.py b/jsonschema/tests/test_jsonschema_test_suite.py new file mode 100644 index 0000000..cabfc21 --- /dev/null +++ b/jsonschema/tests/test_jsonschema_test_suite.py @@ -0,0 +1,202 @@ +""" +Test runner for the JSON Schema official test suite + +Tests comprehensive correctness of each draft's validator. + +See https://github.com/json-schema-org/JSON-Schema-Test-Suite for details. +""" + +import sys +import warnings + +from jsonschema import ( + Draft3Validator, + Draft4Validator, + Draft6Validator, + Draft7Validator, + draft3_format_checker, + draft4_format_checker, + draft6_format_checker, + draft7_format_checker, +) +from jsonschema.tests._suite import Suite +from jsonschema.validators import _DEPRECATED_DEFAULT_TYPES, create + + +SUITE = Suite() +DRAFT3 = SUITE.version(name="draft3") +DRAFT4 = SUITE.version(name="draft4") +DRAFT6 = SUITE.version(name="draft6") +DRAFT7 = SUITE.version(name="draft7") + + +def skip_tests_containing_descriptions(**kwargs): + def skipper(test): + descriptions_and_reasons = kwargs.get(test.subject, {}) + return next( + ( + reason + for description, reason in descriptions_and_reasons.items() + if description in test.description + ), + None, + ) + return skipper + + +def missing_format(checker): + def missing_format(test): + schema = test.schema + if schema is True or schema is False or "format" not in schema: + return + + if schema["format"] not in checker.checkers: + return "Format checker {0!r} not found.".format(schema["format"]) + return missing_format + + +is_narrow_build = sys.maxunicode == 2 ** 16 - 1 +if is_narrow_build: # pragma: no cover + narrow_unicode_build = skip_tests_containing_descriptions( + maxLength={ + "supplementary Unicode": + "Not running surrogate Unicode case, this Python is narrow.", + }, + minLength={ + "supplementary Unicode": + "Not running surrogate Unicode case, this Python is narrow.", + }, + ) +else: + def narrow_unicode_build(test): # pragma: no cover + return + + +TestDraft3 = DRAFT3.to_unittest_testcase( + DRAFT3.tests(), + DRAFT3.optional_tests_of(name="format"), + DRAFT3.optional_tests_of(name="bignum"), + DRAFT3.optional_tests_of(name="zeroTerminatedFloats"), + Validator=Draft3Validator, + format_checker=draft3_format_checker, + skip=lambda test: ( + narrow_unicode_build(test) or + missing_format(draft3_format_checker)(test) or + skip_tests_containing_descriptions( + format={ + "case-insensitive T and Z": "Upstream bug in strict_rfc3339", + }, + )(test) + ), +) + + +TestDraft4 = DRAFT4.to_unittest_testcase( + DRAFT4.tests(), + DRAFT4.optional_tests_of(name="format"), + DRAFT4.optional_tests_of(name="bignum"), + DRAFT4.optional_tests_of(name="zeroTerminatedFloats"), + Validator=Draft4Validator, + format_checker=draft4_format_checker, + skip=lambda test: ( + narrow_unicode_build(test) or + missing_format(draft4_format_checker)(test) or + skip_tests_containing_descriptions( + format={ + "case-insensitive T and Z": "Upstream bug in strict_rfc3339", + }, + ref={ + "valid tree": "An actual bug, this needs fixing.", + }, + refRemote={ + "number is valid": "An actual bug, this needs fixing.", + "string is invalid": "An actual bug, this needs fixing.", + }, + )(test) + ), +) + + +TestDraft6 = DRAFT6.to_unittest_testcase( + DRAFT6.tests(), + DRAFT6.optional_tests_of(name="format"), + DRAFT6.optional_tests_of(name="bignum"), + DRAFT6.optional_tests_of(name="zeroTerminatedFloats"), + Validator=Draft6Validator, + format_checker=draft6_format_checker, + skip=lambda test: ( + narrow_unicode_build(test) or + missing_format(draft6_format_checker)(test) or + skip_tests_containing_descriptions( + format={ + "case-insensitive T and Z": "Upstream bug in strict_rfc3339", + }, + ref={ + "valid tree": "An actual bug, this needs fixing.", + }, + refRemote={ + "number is valid": "An actual bug, this needs fixing.", + "string is invalid": "An actual bug, this needs fixing.", + }, + )(test) + ), +) + + +TestDraft7 = DRAFT7.to_unittest_testcase( + DRAFT7.tests(), + DRAFT7.format_tests(), + DRAFT7.optional_tests_of(name="bignum"), + DRAFT7.optional_tests_of(name="zeroTerminatedFloats"), + Validator=Draft7Validator, + format_checker=draft7_format_checker, + skip=lambda test: ( + narrow_unicode_build(test) + or missing_format(draft7_format_checker)(test) + or skip_tests_containing_descriptions( + ref={ + "valid tree": "An actual bug, this needs fixing.", + }, + refRemote={ + "number is valid": "An actual bug, this needs fixing.", + "string is invalid": "An actual bug, this needs fixing.", + }, + )(test) + or skip_tests_containing_descriptions( + **{ + "date-time": { + "case-insensitive T and Z": + "Upstream bug in strict_rfc3339", + }, + } + )(test) + ), +) + + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + + TestDraft3LegacyTypeCheck = DRAFT3.to_unittest_testcase( + # Interestingly the any part couldn't really be done w/the old API. + ( + (test for test in each if test.schema != {"type": "any"}) + for each in DRAFT3.tests_of(name="type") + ), + name="TestDraft3LegacyTypeCheck", + Validator=create( + meta_schema=Draft3Validator.META_SCHEMA, + validators=Draft3Validator.VALIDATORS, + default_types=_DEPRECATED_DEFAULT_TYPES, + ), + ) + + TestDraft4LegacyTypeCheck = DRAFT4.to_unittest_testcase( + DRAFT4.tests_of(name="type"), + name="TestDraft4LegacyTypeCheck", + Validator=create( + meta_schema=Draft4Validator.META_SCHEMA, + validators=Draft4Validator.VALIDATORS, + default_types=_DEPRECATED_DEFAULT_TYPES, + ), + ) diff --git a/jsonschema/tests/test_types.py b/jsonschema/tests/test_types.py new file mode 100644 index 0000000..36196c8 --- /dev/null +++ b/jsonschema/tests/test_types.py @@ -0,0 +1,190 @@ +""" +Tests on the new type interface. The actual correctness of the type checking +is handled in test_jsonschema_test_suite; these tests check that TypeChecker +functions correctly and can facilitate extensions to type checking +""" +from collections import namedtuple +from unittest import TestCase + +from jsonschema import ValidationError, _validators +from jsonschema._types import TypeChecker +from jsonschema.exceptions import UndefinedTypeCheck +from jsonschema.validators import Draft4Validator, extend + + +def equals_2(checker, instance): + return instance == 2 + + +def is_namedtuple(instance): + return isinstance(instance, tuple) and getattr(instance, "_fields", None) + + +def is_object_or_named_tuple(checker, instance): + if Draft4Validator.TYPE_CHECKER.is_type(instance, "object"): + return True + return is_namedtuple(instance) + + +def coerce_named_tuple(fn): + def coerced(validator, value, instance, schema): + if is_namedtuple(instance): + instance = instance._asdict() + return fn(validator, value, instance, schema) + return coerced + + +required = coerce_named_tuple(_validators.required) +properties = coerce_named_tuple(_validators.properties) + + +class TestTypeChecker(TestCase): + def test_is_type(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual( + ( + checker.is_type(instance=2, type="two"), + checker.is_type(instance="bar", type="two"), + ), + (True, False), + ) + + def test_is_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as context: + TypeChecker().is_type(4, "foobar") + self.assertIn("foobar", str(context.exception)) + + def test_checks_can_be_added_at_init(self): + checker = TypeChecker({"two": equals_2}) + self.assertEqual(checker, TypeChecker().redefine("two", equals_2)) + + def test_redefine_existing_type(self): + self.assertEqual( + TypeChecker().redefine("two", object()).redefine("two", equals_2), + TypeChecker().redefine("two", equals_2), + ) + + def test_remove(self): + self.assertEqual( + TypeChecker({"two": equals_2}).remove("two"), + TypeChecker(), + ) + + def test_remove_unknown_type(self): + with self.assertRaises(UndefinedTypeCheck) as context: + TypeChecker().remove("foobar") + self.assertIn("foobar", str(context.exception)) + + def test_redefine_many(self): + self.assertEqual( + TypeChecker().redefine_many({"foo": int, "bar": str}), + TypeChecker().redefine("foo", int).redefine("bar", str), + ) + + def test_remove_multiple(self): + self.assertEqual( + TypeChecker({"foo": int, "bar": str}).remove("foo", "bar"), + TypeChecker(), + ) + + def test_type_check_can_raise_key_error(self): + """ + Make sure no one writes: + + try: + self._type_checkers[type](...) + except KeyError: + + ignoring the fact that the function itself can raise that. + """ + + error = KeyError("Stuff") + + def raises_keyerror(checker, instance): + raise error + + with self.assertRaises(KeyError) as context: + TypeChecker({"foo": raises_keyerror}).is_type(4, "foo") + + self.assertIs(context.exception, error) + + +class TestCustomTypes(TestCase): + def test_simple_type_can_be_extended(self): + def int_or_str_int(checker, instance): + if not isinstance(instance, (int, str)): + return False + try: + int(instance) + except ValueError: + return False + return True + + CustomValidator = extend( + Draft4Validator, + type_checker=Draft4Validator.TYPE_CHECKER.redefine( + "integer", int_or_str_int, + ), + ) + validator = CustomValidator({"type": "integer"}) + + validator.validate(4) + validator.validate("4") + + with self.assertRaises(ValidationError): + validator.validate(4.4) + + def test_object_can_be_extended(self): + schema = {'type': 'object'} + + Point = namedtuple('Point', ['x', 'y']) + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple, + ) + + CustomValidator = extend(Draft4Validator, type_checker=type_checker) + validator = CustomValidator(schema) + + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_require_custom_validators(self): + schema = {"type": "object", "required": ["x"]} + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple, + ) + + CustomValidator = extend(Draft4Validator, type_checker=type_checker) + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Cannot handle required + with self.assertRaises(ValidationError): + validator.validate(Point(x=4, y=5)) + + def test_object_extensions_can_handle_custom_validators(self): + schema = { + "type": "object", + "required": ["x"], + "properties": {"x": {"type": "integer"}}, + } + + type_checker = Draft4Validator.TYPE_CHECKER.redefine( + u"object", is_object_or_named_tuple, + ) + + CustomValidator = extend( + Draft4Validator, + type_checker=type_checker, + validators={"required": required, "properties": properties}, + ) + + validator = CustomValidator(schema) + + Point = namedtuple("Point", ["x", "y"]) + # Can now process required and properties + validator.validate(Point(x=4, y=5)) + + with self.assertRaises(ValidationError): + validator.validate(Point(x="not an integer", y=5)) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py new file mode 100644 index 0000000..305852e --- /dev/null +++ b/jsonschema/tests/test_validators.py @@ -0,0 +1,1786 @@ +from collections import deque +from contextlib import contextmanager +from decimal import Decimal +from io import BytesIO +from unittest import TestCase +import json +import os +import sys +import tempfile +import unittest + +from twisted.trial.unittest import SynchronousTestCase +import attr + +from jsonschema import FormatChecker, TypeChecker, exceptions, validators +from jsonschema.compat import PY3, pathname2url +import jsonschema + + +def startswith(validator, startswith, instance, schema): + if not instance.startswith(startswith): + yield exceptions.ValidationError(u"Whoops!") + + +class TestCreateAndExtend(SynchronousTestCase): + def setUp(self): + self.addCleanup( + self.assertEqual, + validators.meta_schemas, + dict(validators.meta_schemas), + ) + + self.meta_schema = {u"$id": "some://meta/schema"} + self.validators = {u"startswith": startswith} + self.type_checker = TypeChecker() + self.Validator = validators.create( + meta_schema=self.meta_schema, + validators=self.validators, + type_checker=self.type_checker, + ) + + def test_attrs(self): + self.assertEqual( + ( + self.Validator.VALIDATORS, + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + ), ( + self.validators, + self.meta_schema, + self.type_checker, + ), + ) + + def test_init(self): + schema = {u"startswith": u"foo"} + self.assertEqual(self.Validator(schema).schema, schema) + + def test_iter_errors(self): + schema = {u"startswith": u"hel"} + iter_errors = self.Validator(schema).iter_errors + + errors = list(iter_errors(u"hello")) + self.assertEqual(errors, []) + + expected_error = exceptions.ValidationError( + u"Whoops!", + instance=u"goodbye", + schema=schema, + validator=u"startswith", + validator_value=u"hel", + schema_path=deque([u"startswith"]), + ) + + errors = list(iter_errors(u"goodbye")) + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0]._contents(), expected_error._contents()) + + def test_if_a_version_is_provided_it_is_registered(self): + Validator = validators.create( + meta_schema={u"$id": "something"}, + version="my version", + ) + self.addCleanup(validators.meta_schemas.pop, "something") + self.assertEqual(Validator.__name__, "MyVersionValidator") + + def test_if_a_version_is_not_provided_it_is_not_registered(self): + original = dict(validators.meta_schemas) + validators.create(meta_schema={u"id": "id"}) + self.assertEqual(validators.meta_schemas, original) + + def test_validates_registers_meta_schema_id(self): + meta_schema_key = "meta schema id" + my_meta_schema = {u"id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + id_of=lambda s: s.get("id", ""), + ) + self.addCleanup(validators.meta_schemas.pop, meta_schema_key) + + self.assertIn(meta_schema_key, validators.meta_schemas) + + def test_validates_registers_meta_schema_draft6_id(self): + meta_schema_key = "meta schema $id" + my_meta_schema = {u"$id": meta_schema_key} + + validators.create( + meta_schema=my_meta_schema, + version="my version", + ) + self.addCleanup(validators.meta_schemas.pop, meta_schema_key) + + self.assertIn(meta_schema_key, validators.meta_schemas) + + def test_create_default_types(self): + Validator = validators.create(meta_schema={}, validators=()) + self.assertTrue( + all( + Validator({}).is_type(instance=instance, type=type) + for type, instance in [ + (u"array", []), + (u"boolean", True), + (u"integer", 12), + (u"null", None), + (u"number", 12.0), + (u"object", {}), + (u"string", u"foo"), + ] + ), + ) + + def test_extend(self): + original = dict(self.Validator.VALIDATORS) + new = object() + + Extended = validators.extend( + self.Validator, + validators={u"new": new}, + ) + self.assertEqual( + ( + Extended.VALIDATORS, + Extended.META_SCHEMA, + Extended.TYPE_CHECKER, + self.Validator.VALIDATORS, + ), ( + dict(original, new=new), + self.Validator.META_SCHEMA, + self.Validator.TYPE_CHECKER, + original, + ), + ) + + def test_extend_idof(self): + """ + Extending a validator preserves its notion of schema IDs. + """ + def id_of(schema): + return schema.get(u"__test__", self.Validator.ID_OF(schema)) + correct_id = "the://correct/id/" + meta_schema = { + u"$id": "the://wrong/id/", + u"__test__": correct_id, + } + Original = validators.create( + meta_schema=meta_schema, + validators=self.validators, + type_checker=self.type_checker, + id_of=id_of, + ) + self.assertEqual(Original.ID_OF(Original.META_SCHEMA), correct_id) + + Derived = validators.extend(Original) + self.assertEqual(Derived.ID_OF(Derived.META_SCHEMA), correct_id) + + +class TestLegacyTypeChecking(SynchronousTestCase): + def test_create_default_types(self): + Validator = validators.create(meta_schema={}, validators=()) + self.assertEqual( + set(Validator.DEFAULT_TYPES), { + u"array", + u"boolean", + u"integer", + u"null", + u"number", + u"object", u"string", + }, + ) + self.flushWarnings() + + def test_extend(self): + Validator = validators.create(meta_schema={}, validators=()) + original = dict(Validator.VALIDATORS) + new = object() + + Extended = validators.extend( + Validator, + validators={u"new": new}, + ) + self.assertEqual( + ( + Extended.VALIDATORS, + Extended.META_SCHEMA, + Extended.TYPE_CHECKER, + Validator.VALIDATORS, + + Extended.DEFAULT_TYPES, + Extended({}).DEFAULT_TYPES, + self.flushWarnings()[0]["message"], + ), ( + dict(original, new=new), + Validator.META_SCHEMA, + Validator.TYPE_CHECKER, + original, + + Validator.DEFAULT_TYPES, + Validator.DEFAULT_TYPES, + self.flushWarnings()[0]["message"], + ), + ) + + def test_types_redefines_the_validators_type_checker(self): + schema = {"type": "string"} + self.assertFalse(validators.Draft7Validator(schema).is_valid(12)) + + validator = validators.Draft7Validator( + schema, + types={"string": (str, int)}, + ) + self.assertTrue(validator.is_valid(12)) + self.flushWarnings() + + def test_providing_default_types_warns(self): + self.assertWarns( + category=DeprecationWarning, + message=( + "The default_types argument is deprecated. " + "Use the type_checker argument instead." + ), + # https://tm.tl/9363 :'( + filename=sys.modules[self.assertWarns.__module__].__file__, + + f=validators.create, + meta_schema={}, + validators={}, + default_types={"foo": object}, + ) + + def test_cannot_ask_for_default_types_with_non_default_type_checker(self): + """ + We raise an error when you ask a validator with non-default + type checker for its DEFAULT_TYPES. + + The type checker argument is new, so no one but this library + itself should be trying to use it, and doing so while then + asking for DEFAULT_TYPES makes no sense (not to mention is + deprecated), since type checkers are not strictly about Python + type. + """ + Validator = validators.create( + meta_schema={}, + validators={}, + type_checker=TypeChecker(), + ) + with self.assertRaises(validators._DontDoThat) as e: + Validator.DEFAULT_TYPES + + self.assertIn( + "DEFAULT_TYPES cannot be used on Validators using TypeCheckers", + str(e.exception), + ) + with self.assertRaises(validators._DontDoThat): + Validator({}).DEFAULT_TYPES + + self.assertFalse(self.flushWarnings()) + + def test_providing_explicit_type_checker_does_not_warn(self): + Validator = validators.create( + meta_schema={}, + validators={}, + type_checker=TypeChecker(), + ) + self.assertFalse(self.flushWarnings()) + + Validator({}) + self.assertFalse(self.flushWarnings()) + + def test_providing_neither_does_not_warn(self): + Validator = validators.create(meta_schema={}, validators={}) + self.assertFalse(self.flushWarnings()) + + Validator({}) + self.assertFalse(self.flushWarnings()) + + def test_providing_default_types_with_type_checker_errors(self): + with self.assertRaises(TypeError) as e: + validators.create( + meta_schema={}, + validators={}, + default_types={"foo": object}, + type_checker=TypeChecker(), + ) + + self.assertIn( + "Do not specify default_types when providing a type checker", + str(e.exception), + ) + self.assertFalse(self.flushWarnings()) + + def test_extending_a_legacy_validator_with_a_type_checker_errors(self): + Validator = validators.create( + meta_schema={}, + validators={}, + default_types={u"array": list} + ) + with self.assertRaises(TypeError) as e: + validators.extend( + Validator, + validators={}, + type_checker=TypeChecker(), + ) + + self.assertIn( + ( + "Cannot extend a validator created with default_types " + "with a type_checker. Update the validator to use a " + "type_checker when created." + ), + str(e.exception), + ) + self.flushWarnings() + + def test_extending_a_legacy_validator_does_not_rewarn(self): + Validator = validators.create(meta_schema={}, default_types={}) + self.assertTrue(self.flushWarnings()) + + validators.extend(Validator) + self.assertFalse(self.flushWarnings()) + + def test_accessing_default_types_warns(self): + Validator = validators.create(meta_schema={}, validators={}) + self.assertFalse(self.flushWarnings()) + + self.assertWarns( + DeprecationWarning, + ( + "The DEFAULT_TYPES attribute is deprecated. " + "See the type checker attached to this validator instead." + ), + # https://tm.tl/9363 :'( + sys.modules[self.assertWarns.__module__].__file__, + + getattr, + Validator, + "DEFAULT_TYPES", + ) + + def test_accessing_default_types_on_the_instance_warns(self): + Validator = validators.create(meta_schema={}, validators={}) + self.assertFalse(self.flushWarnings()) + + self.assertWarns( + DeprecationWarning, + ( + "The DEFAULT_TYPES attribute is deprecated. " + "See the type checker attached to this validator instead." + ), + # https://tm.tl/9363 :'( + sys.modules[self.assertWarns.__module__].__file__, + + getattr, + Validator({}), + "DEFAULT_TYPES", + ) + + def test_providing_types_to_init_warns(self): + Validator = validators.create(meta_schema={}, validators={}) + self.assertFalse(self.flushWarnings()) + + self.assertWarns( + category=DeprecationWarning, + message=( + "The types argument is deprecated. " + "Provide a type_checker to jsonschema.validators.extend " + "instead." + ), + # https://tm.tl/9363 :'( + filename=sys.modules[self.assertWarns.__module__].__file__, + + f=Validator, + schema={}, + types={"bar": object}, + ) + + +class TestIterErrors(TestCase): + def setUp(self): + self.validator = validators.Draft3Validator({}) + + def test_iter_errors(self): + instance = [1, 2] + schema = { + u"disallow": u"array", + u"enum": [["a", "b", "c"], ["d", "e", "f"]], + u"minItems": 3, + } + + got = (e.message for e in self.validator.iter_errors(instance, schema)) + expected = [ + "%r is disallowed for [1, 2]" % (schema["disallow"],), + "[1, 2] is too short", + "[1, 2] is not one of %r" % (schema["enum"],), + ] + self.assertEqual(sorted(got), sorted(expected)) + + def test_iter_errors_multiple_failures_one_validator(self): + instance = {"foo": 2, "bar": [1], "baz": 15, "quux": "spam"} + schema = { + u"properties": { + "foo": {u"type": "string"}, + "bar": {u"minItems": 2}, + "baz": {u"maximum": 10, u"enum": [2, 4, 6, 8]}, + }, + } + + errors = list(self.validator.iter_errors(instance, schema)) + self.assertEqual(len(errors), 4) + + +class TestValidationErrorMessages(TestCase): + def message_for(self, instance, schema, *args, **kwargs): + kwargs.setdefault("cls", validators.Draft3Validator) + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(instance, schema, *args, **kwargs) + return e.exception.message + + def test_single_type_failure(self): + message = self.message_for(instance=1, schema={u"type": u"string"}) + self.assertEqual(message, "1 is not of type %r" % u"string") + + def test_single_type_list_failure(self): + message = self.message_for(instance=1, schema={u"type": [u"string"]}) + self.assertEqual(message, "1 is not of type %r" % u"string") + + def test_multiple_type_failure(self): + types = u"string", u"object" + message = self.message_for(instance=1, schema={u"type": list(types)}) + self.assertEqual(message, "1 is not of type %r, %r" % types) + + def test_object_without_title_type_failure(self): + type = {u"type": [{u"minimum": 3}]} + message = self.message_for(instance=1, schema={u"type": [type]}) + self.assertEqual(message, "1 is less than the minimum of 3") + + def test_object_with_named_type_failure(self): + schema = {u"type": [{u"name": "Foo", u"minimum": 3}]} + message = self.message_for(instance=1, schema=schema) + self.assertEqual(message, "1 is less than the minimum of 3") + + def test_minimum(self): + message = self.message_for(instance=1, schema={"minimum": 2}) + self.assertEqual(message, "1 is less than the minimum of 2") + + def test_maximum(self): + message = self.message_for(instance=1, schema={"maximum": 0}) + self.assertEqual(message, "1 is greater than the maximum of 0") + + def test_dependencies_single_element(self): + depend, on = "bar", "foo" + schema = {u"dependencies": {depend: on}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "%r is a dependency of %r" % (on, depend)) + + def test_dependencies_list_draft3(self): + depend, on = "bar", "foo" + schema = {u"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft3Validator, + ) + self.assertEqual(message, "%r is a dependency of %r" % (on, depend)) + + def test_dependencies_list_draft7(self): + depend, on = "bar", "foo" + schema = {u"dependencies": {depend: [on]}} + message = self.message_for( + instance={"bar": 2}, + schema=schema, + cls=validators.Draft7Validator, + ) + self.assertEqual(message, "%r is a dependency of %r" % (on, depend)) + + def test_additionalItems_single_failure(self): + message = self.message_for( + instance=[2], + schema={u"items": [], u"additionalItems": False}, + ) + self.assertIn("(2 was unexpected)", message) + + def test_additionalItems_multiple_failures(self): + message = self.message_for( + instance=[1, 2, 3], + schema={u"items": [], u"additionalItems": False} + ) + self.assertIn("(1, 2, 3 were unexpected)", message) + + def test_additionalProperties_single_failure(self): + additional = "foo" + schema = {u"additionalProperties": False} + message = self.message_for(instance={additional: 2}, schema=schema) + self.assertIn("(%r was unexpected)" % (additional,), message) + + def test_additionalProperties_multiple_failures(self): + schema = {u"additionalProperties": False} + message = self.message_for( + instance=dict.fromkeys(["foo", "bar"]), + schema=schema, + ) + + self.assertIn(repr("foo"), message) + self.assertIn(repr("bar"), message) + self.assertIn("were unexpected)", message) + + def test_const(self): + schema = {u"const": 12} + message = self.message_for( + instance={"foo": "bar"}, + schema=schema, + cls=validators.Draft6Validator, + ) + self.assertIn("12 was expected", message) + + def test_contains(self): + schema = {u"contains": {u"const": 12}} + message = self.message_for( + instance=[2, {}, []], + schema=schema, + cls=validators.Draft6Validator, + ) + self.assertIn( + "None of [2, {}, []] are valid under the given schema", + message, + ) + + def test_invalid_format_default_message(self): + checker = FormatChecker(formats=()) + checker.checks(u"thing")(lambda value: False) + + schema = {u"format": u"thing"} + message = self.message_for( + instance="bla", + schema=schema, + format_checker=checker, + ) + + self.assertIn(repr("bla"), message) + self.assertIn(repr("thing"), message) + self.assertIn("is not a", message) + + def test_additionalProperties_false_patternProperties(self): + schema = {u"type": u"object", + u"additionalProperties": False, + u"patternProperties": { + u"^abc$": {u"type": u"string"}, + u"^def$": {u"type": u"string"}, + }} + message = self.message_for( + instance={u"zebra": 123}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{} does not match any of the regexes: {}, {}".format( + repr(u"zebra"), repr(u"^abc$"), repr(u"^def$"), + ), + ) + message = self.message_for( + instance={u"zebra": 123, u"fish": 456}, + schema=schema, + cls=validators.Draft4Validator, + ) + self.assertEqual( + message, + "{}, {} do not match any of the regexes: {}, {}".format( + repr(u"fish"), repr(u"zebra"), repr(u"^abc$"), repr(u"^def$") + ), + ) + + def test_False_schema(self): + message = self.message_for( + instance="something", + schema=False, + cls=validators.Draft7Validator, + ) + self.assertIn("False schema does not allow 'something'", message) + + +class TestValidationErrorDetails(TestCase): + # TODO: These really need unit tests for each individual validator, rather + # than just these higher level tests. + def test_anyOf(self): + instance = 5 + schema = { + "anyOf": [ + {"minimum": 20}, + {"type": "string"}, + ], + } + + validator = validators.Draft4Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "anyOf") + self.assertEqual(e.validator_value, schema["anyOf"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + + self.assertEqual(e.schema_path, deque(["anyOf"])) + self.assertEqual(e.relative_schema_path, deque(["anyOf"])) + self.assertEqual(e.absolute_schema_path, deque(["anyOf"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "minimum") + self.assertEqual(e1.validator_value, schema["anyOf"][0]["minimum"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["anyOf"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + + self.assertEqual(e1.schema_path, deque([0, "minimum"])) + self.assertEqual(e1.relative_schema_path, deque([0, "minimum"])) + self.assertEqual( + e1.absolute_schema_path, deque(["anyOf", 0, "minimum"]), + ) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "type") + self.assertEqual(e2.validator_value, schema["anyOf"][1]["type"]) + self.assertEqual(e2.instance, instance) + self.assertEqual(e2.schema, schema["anyOf"][1]) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque([])) + self.assertEqual(e2.relative_path, deque([])) + self.assertEqual(e2.absolute_path, deque([])) + + self.assertEqual(e2.schema_path, deque([1, "type"])) + self.assertEqual(e2.relative_schema_path, deque([1, "type"])) + self.assertEqual(e2.absolute_schema_path, deque(["anyOf", 1, "type"])) + + self.assertEqual(len(e2.context), 0) + + def test_type(self): + instance = {"foo": 1} + schema = { + "type": [ + {"type": "integer"}, + { + "type": "object", + "properties": {"foo": {"enum": [2]}}, + }, + ], + } + + validator = validators.Draft3Validator(schema) + errors = list(validator.iter_errors(instance)) + self.assertEqual(len(errors), 1) + e = errors[0] + + self.assertEqual(e.validator, "type") + self.assertEqual(e.validator_value, schema["type"]) + self.assertEqual(e.instance, instance) + self.assertEqual(e.schema, schema) + self.assertIsNone(e.parent) + + self.assertEqual(e.path, deque([])) + self.assertEqual(e.relative_path, deque([])) + self.assertEqual(e.absolute_path, deque([])) + + self.assertEqual(e.schema_path, deque(["type"])) + self.assertEqual(e.relative_schema_path, deque(["type"])) + self.assertEqual(e.absolute_schema_path, deque(["type"])) + + self.assertEqual(len(e.context), 2) + + e1, e2 = sorted_errors(e.context) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e1.validator_value, schema["type"][0]["type"]) + self.assertEqual(e1.instance, instance) + self.assertEqual(e1.schema, schema["type"][0]) + self.assertIs(e1.parent, e) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e1.relative_path, deque([])) + self.assertEqual(e1.absolute_path, deque([])) + + self.assertEqual(e1.schema_path, deque([0, "type"])) + self.assertEqual(e1.relative_schema_path, deque([0, "type"])) + self.assertEqual(e1.absolute_schema_path, deque(["type", 0, "type"])) + + self.assertFalse(e1.context) + + self.assertEqual(e2.validator, "enum") + self.assertEqual(e2.validator_value, [2]) + self.assertEqual(e2.instance, 1) + self.assertEqual(e2.schema, {u"enum": [2]}) + self.assertIs(e2.parent, e) + + self.assertEqual(e2.path, deque(["foo"])) + self.assertEqual(e2.relative_path, deque(["foo"])) + self.assertEqual(e2.absolute_path, deque(["foo"])) + + self.assertEqual( + e2.schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.relative_schema_path, deque([1, "properties", "foo", "enum"]), + ) + self.assertEqual( + e2.absolute_schema_path, + deque(["type", 1, "properties", "foo", "enum"]), + ) + + self.assertFalse(e2.context) + + def test_single_nesting(self): + instance = {"foo": 2, "bar": [1], "baz": 15, "quux": "spam"} + schema = { + "properties": { + "foo": {"type": "string"}, + "bar": {"minItems": 2}, + "baz": {"maximum": 10, "enum": [2, 4, 6, 8]}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["baz"])) + self.assertEqual(e3.path, deque(["baz"])) + self.assertEqual(e4.path, deque(["foo"])) + + self.assertEqual(e1.relative_path, deque(["bar"])) + self.assertEqual(e2.relative_path, deque(["baz"])) + self.assertEqual(e3.relative_path, deque(["baz"])) + self.assertEqual(e4.relative_path, deque(["foo"])) + + self.assertEqual(e1.absolute_path, deque(["bar"])) + self.assertEqual(e2.absolute_path, deque(["baz"])) + self.assertEqual(e3.absolute_path, deque(["baz"])) + self.assertEqual(e4.absolute_path, deque(["foo"])) + + self.assertEqual(e1.validator, "minItems") + self.assertEqual(e2.validator, "enum") + self.assertEqual(e3.validator, "maximum") + self.assertEqual(e4.validator, "type") + + def test_multiple_nesting(self): + instance = [1, {"foo": 2, "bar": {"baz": [1]}}, "quux"] + schema = { + "type": "string", + "items": { + "type": ["string", "object"], + "properties": { + "foo": {"enum": [1, 3]}, + "bar": { + "type": "array", + "properties": { + "bar": {"required": True}, + "baz": {"minItems": 2}, + }, + }, + }, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2, e3, e4, e5, e6 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([])) + self.assertEqual(e2.path, deque([0])) + self.assertEqual(e3.path, deque([1, "bar"])) + self.assertEqual(e4.path, deque([1, "bar", "bar"])) + self.assertEqual(e5.path, deque([1, "bar", "baz"])) + self.assertEqual(e6.path, deque([1, "foo"])) + + self.assertEqual(e1.schema_path, deque(["type"])) + self.assertEqual(e2.schema_path, deque(["items", "type"])) + self.assertEqual( + list(e3.schema_path), ["items", "properties", "bar", "type"], + ) + self.assertEqual( + list(e4.schema_path), + ["items", "properties", "bar", "properties", "bar", "required"], + ) + self.assertEqual( + list(e5.schema_path), + ["items", "properties", "bar", "properties", "baz", "minItems"] + ) + self.assertEqual( + list(e6.schema_path), ["items", "properties", "foo", "enum"], + ) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "type") + self.assertEqual(e3.validator, "type") + self.assertEqual(e4.validator, "required") + self.assertEqual(e5.validator, "minItems") + self.assertEqual(e6.validator, "enum") + + def test_recursive(self): + schema = { + "definitions": { + "node": { + "anyOf": [{ + "type": "object", + "required": ["name", "children"], + "properties": { + "name": { + "type": "string", + }, + "children": { + "type": "object", + "patternProperties": { + "^.*$": { + "$ref": "#/definitions/node", + }, + }, + }, + }, + }], + }, + }, + "type": "object", + "required": ["root"], + "properties": {"root": {"$ref": "#/definitions/node"}}, + } + + instance = { + "root": { + "name": "root", + "children": { + "a": { + "name": "a", + "children": { + "ab": { + "name": "ab", + # missing "children" + }, + }, + }, + }, + }, + } + validator = validators.Draft4Validator(schema) + + e, = validator.iter_errors(instance) + self.assertEqual(e.absolute_path, deque(["root"])) + self.assertEqual( + e.absolute_schema_path, deque(["properties", "root", "anyOf"]), + ) + + e1, = e.context + self.assertEqual(e1.absolute_path, deque(["root", "children", "a"])) + self.assertEqual( + e1.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + + e2, = e1.context + self.assertEqual( + e2.absolute_path, deque( + ["root", "children", "a", "children", "ab"], + ), + ) + self.assertEqual( + e2.absolute_schema_path, deque( + [ + "properties", + "root", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + 0, + "properties", + "children", + "patternProperties", + "^.*$", + "anyOf", + ], + ), + ) + + def test_additionalProperties(self): + instance = {"bar": "bar", "foo": 2} + schema = {"additionalProperties": {"type": "integer", "minimum": 5}} + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_patternProperties(self): + instance = {"bar": 1, "foo": 2} + schema = { + "patternProperties": { + "bar": {"type": "string"}, + "foo": {"minimum": 5}, + }, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque(["bar"])) + self.assertEqual(e2.path, deque(["foo"])) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems(self): + instance = ["foo", 1] + schema = { + "items": [], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([0])) + self.assertEqual(e2.path, deque([1])) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_additionalItems_with_items(self): + instance = ["foo", "bar", 1] + schema = { + "items": [{}], + "additionalItems": {"type": "integer", "minimum": 5}, + } + + validator = validators.Draft3Validator(schema) + errors = validator.iter_errors(instance) + e1, e2 = sorted_errors(errors) + + self.assertEqual(e1.path, deque([1])) + self.assertEqual(e2.path, deque([2])) + + self.assertEqual(e1.validator, "type") + self.assertEqual(e2.validator, "minimum") + + def test_propertyNames(self): + instance = {"foo": 12} + schema = {"propertyNames": {"not": {"const": "foo"}}} + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(instance) + + self.assertEqual(error.validator, "not") + self.assertEqual( + error.message, + "%r is not allowed for %r" % ({"const": "foo"}, "foo"), + ) + self.assertEqual(error.path, deque([])) + self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) + + def test_if_then(self): + schema = { + "if": {"const": 12}, + "then": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(12) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.schema_path, deque(["if", "then", "const"])) + + def test_if_else(self): + schema = { + "if": {"const": 12}, + "else": {"const": 13}, + } + + validator = validators.Draft7Validator(schema) + error, = validator.iter_errors(15) + + self.assertEqual(error.validator, "const") + self.assertEqual(error.message, "13 was expected") + self.assertEqual(error.path, deque([])) + self.assertEqual(error.schema_path, deque(["if", "else", "const"])) + + def test_boolean_schema_False(self): + validator = validators.Draft7Validator(False) + error, = validator.iter_errors(12) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.schema, + error.schema_path, + ), + ( + "False schema does not allow 12", + None, + None, + 12, + False, + deque([]), + ), + ) + + def test_ref(self): + ref, schema = "someRef", {"additionalProperties": {"type": "integer"}} + validator = validators.Draft7Validator( + {"$ref": ref}, + resolver=validators.RefResolver("", {}, store={ref: schema}), + ) + error, = validator.iter_errors({"foo": "notAnInteger"}) + + self.assertEqual( + ( + error.message, + error.validator, + error.validator_value, + error.instance, + error.absolute_path, + error.schema, + error.schema_path, + ), + ( + "'notAnInteger' is not of type 'integer'", + "type", + "integer", + "notAnInteger", + deque(["foo"]), + {"type": "integer"}, + deque(["additionalProperties", "type"]), + ), + ) + + +class MetaSchemaTestsMixin(object): + # TODO: These all belong upstream + def test_invalid_properties(self): + with self.assertRaises(exceptions.SchemaError): + self.Validator.check_schema({"properties": {"test": object()}}) + + def test_minItems_invalid_string(self): + with self.assertRaises(exceptions.SchemaError): + # needs to be an integer + self.Validator.check_schema({"minItems": "1"}) + + def test_enum_allows_empty_arrays(self): + """ + Technically, all the spec says is they SHOULD have elements, not MUST. + + See https://github.com/Julian/jsonschema/issues/529. + """ + self.Validator.check_schema({"enum": []}) + + def test_enum_allows_non_unique_items(self): + """ + Technically, all the spec says is they SHOULD be unique, not MUST. + + See https://github.com/Julian/jsonschema/issues/529. + """ + self.Validator.check_schema({"enum": [12, 12]}) + + +class ValidatorTestMixin(MetaSchemaTestsMixin, object): + def test_valid_instances_are_valid(self): + schema, instance = self.valid + self.assertTrue(self.Validator(schema).is_valid(instance)) + + def test_invalid_instances_are_not_valid(self): + schema, instance = self.invalid + self.assertFalse(self.Validator(schema).is_valid(instance)) + + def test_non_existent_properties_are_ignored(self): + self.Validator({object(): object()}).validate(instance=object()) + + def test_it_creates_a_ref_resolver_if_not_provided(self): + self.assertIsInstance( + self.Validator({}).resolver, + validators.RefResolver, + ) + + def test_it_delegates_to_a_ref_resolver(self): + ref, schema = "someCoolRef", {"type": "integer"} + resolver = validators.RefResolver("", {}, store={ref: schema}) + validator = self.Validator({"$ref": ref}, resolver=resolver) + + with self.assertRaises(exceptions.ValidationError): + validator.validate(None) + + def test_it_delegates_to_a_legacy_ref_resolver(self): + """ + Legacy RefResolvers support only the context manager form of + resolution. + + """ + + class LegacyRefResolver(object): + @contextmanager + def resolving(this, ref): + self.assertEqual(ref, "the ref") + yield {"type": "integer"} + + resolver = LegacyRefResolver() + schema = {"$ref": "the ref"} + + with self.assertRaises(exceptions.ValidationError): + self.Validator(schema, resolver=resolver).validate(None) + + def test_is_type_is_true_for_valid_type(self): + self.assertTrue(self.Validator({}).is_type("foo", "string")) + + def test_is_type_is_false_for_invalid_type(self): + self.assertFalse(self.Validator({}).is_type("foo", "array")) + + def test_is_type_evades_bool_inheriting_from_int(self): + self.assertFalse(self.Validator({}).is_type(True, "integer")) + self.assertFalse(self.Validator({}).is_type(True, "number")) + + @unittest.skipIf(PY3, "In Python 3 json.load always produces unicode") + def test_string_a_bytestring_is_a_string(self): + self.Validator({"type": "string"}).validate(b"foo") + + def test_it_can_validate_with_decimals(self): + schema = {"items": {"type": "number"}} + Validator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "number", + lambda checker, thing: isinstance( + thing, (int, float, Decimal), + ) and not isinstance(thing, bool), + ) + ) + + validator = Validator(schema) + validator.validate([1, 1.1, Decimal(1) / Decimal(8)]) + + invalid = ["foo", {}, [], True, None] + self.assertEqual( + [error.instance for error in validator.iter_errors(invalid)], + invalid, + ) + + def test_it_returns_true_for_formats_it_does_not_know_about(self): + validator = self.Validator( + {"format": "carrot"}, format_checker=FormatChecker(), + ) + validator.validate("bugs") + + def test_it_does_not_validate_formats_by_default(self): + validator = self.Validator({}) + self.assertIsNone(validator.format_checker) + + def test_it_validates_formats_if_a_checker_is_provided(self): + checker = FormatChecker() + bad = ValueError("Bad!") + + @checker.checks("foo", raises=ValueError) + def check(value): + if value == "good": + return True + elif value == "bad": + raise bad + else: # pragma: no cover + self.fail("What is {}? [Baby Don't Hurt Me]".format(value)) + + validator = self.Validator( + {"format": "foo"}, format_checker=checker, + ) + + validator.validate("good") + with self.assertRaises(exceptions.ValidationError) as cm: + validator.validate("bad") + + # Make sure original cause is attached + self.assertIs(cm.exception.cause, bad) + + def test_non_string_custom_type(self): + non_string_type = object() + schema = {"type": [non_string_type]} + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + non_string_type, + lambda checker, thing: isinstance(thing, int), + ) + ) + Crazy(schema).validate(15) + + def test_it_properly_formats_tuples_in_errors(self): + """ + A tuple instance properly formats validation errors for uniqueItems. + + See https://github.com/Julian/jsonschema/pull/224 + """ + TupleValidator = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "array", + lambda checker, thing: isinstance(thing, tuple), + ) + ) + with self.assertRaises(exceptions.ValidationError) as e: + TupleValidator({"uniqueItems": True}).validate((1, 1)) + self.assertIn("(1, 1) has non-unique elements", str(e.exception)) + + +class AntiDraft6LeakMixin(object): + """ + Make sure functionality from draft 6 doesn't leak backwards in time. + """ + + def test_True_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(True) + self.assertIn("True is not of type", str(e.exception)) + + def test_False_is_not_a_schema(self): + with self.assertRaises(exceptions.SchemaError) as e: + self.Validator.check_schema(False) + self.assertIn("False is not of type", str(e.exception)) + + @unittest.skip("This test fails, but it shouldn't.") + def test_True_is_not_a_schema_even_if_you_forget_to_check(self): + resolver = validators.RefResolver("", {}) + with self.assertRaises(Exception) as e: + self.Validator(True, resolver=resolver).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + @unittest.skip("This test fails, but it shouldn't.") + def test_False_is_not_a_schema_even_if_you_forget_to_check(self): + resolver = validators.RefResolver("", {}) + with self.assertRaises(Exception) as e: + self.Validator(False, resolver=resolver).validate(12) + self.assertNotIsInstance(e.exception, exceptions.ValidationError) + + +class TestDraft3Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft3Validator + valid = {}, {} + invalid = {"type": "integer"}, "foo" + + def test_any_type_is_valid_for_type_any(self): + validator = self.Validator({"type": "any"}) + validator.validate(object()) + + def test_any_type_is_redefinable(self): + """ + Sigh, because why not. + """ + Crazy = validators.extend( + self.Validator, + type_checker=self.Validator.TYPE_CHECKER.redefine( + "any", lambda checker, thing: isinstance(thing, int), + ) + ) + validator = Crazy({"type": "any"}) + validator.validate(12) + with self.assertRaises(exceptions.ValidationError): + validator.validate("foo") + + def test_is_type_is_true_for_any_type(self): + self.assertTrue(self.Validator({}).is_valid(object(), {"type": "any"})) + + def test_is_type_does_not_evade_bool_if_it_is_being_tested(self): + self.assertTrue(self.Validator({}).is_type(True, "boolean")) + self.assertTrue(self.Validator({}).is_valid(True, {"type": "any"})) + + +class TestDraft4Validator(AntiDraft6LeakMixin, ValidatorTestMixin, TestCase): + Validator = validators.Draft4Validator + valid = {}, {} + invalid = {"type": "integer"}, "foo" + + +class TestDraft6Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft6Validator + valid = {}, {} + invalid = {"type": "integer"}, "foo" + + +class TestDraft7Validator(ValidatorTestMixin, TestCase): + Validator = validators.Draft7Validator + valid = {}, {} + invalid = {"type": "integer"}, "foo" + + +class TestBuiltinFormats(TestCase): + """ + The built-in (specification-defined) formats do not raise type errors. + + If an instance or value is not a string, it should be ignored. + """ + + # These tests belong upstream. + # See https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/246 + + +for Validator, checker in ( + (validators.Draft3Validator, jsonschema.draft3_format_checker), + (validators.Draft4Validator, jsonschema.draft4_format_checker), + (validators.Draft6Validator, jsonschema.draft6_format_checker), + (validators.Draft7Validator, jsonschema.draft7_format_checker), +): + for format in checker.checkers: + def test(self, checker=checker, format=format): + validator = Validator({"format": format}, format_checker=checker) + validator.validate(123) + + name = "test_{}_{}_ignores_non_strings".format( + Validator.__name__, format, + ) + test.__name__ = name + setattr(TestBuiltinFormats, name, test) + + +class TestValidatorFor(SynchronousTestCase): + def test_draft_3(self): + schema = {"$schema": "http://json-schema.org/draft-03/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-03/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft3Validator, + ) + + def test_draft_4(self): + schema = {"$schema": "http://json-schema.org/draft-04/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-04/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft4Validator, + ) + + def test_draft_6(self): + schema = {"$schema": "http://json-schema.org/draft-06/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-06/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft6Validator, + ) + + def test_draft_7(self): + schema = {"$schema": "http://json-schema.org/draft-07/schema"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + schema = {"$schema": "http://json-schema.org/draft-07/schema#"} + self.assertIs( + validators.validator_for(schema), + validators.Draft7Validator, + ) + + def test_True(self): + self.assertIs( + validators.validator_for(True), + validators._LATEST_VERSION, + ) + + def test_False(self): + self.assertIs( + validators.validator_for(False), + validators._LATEST_VERSION, + ) + + def test_custom_validator(self): + Validator = validators.create( + meta_schema={"id": "meta schema id"}, + version="12", + id_of=lambda s: s.get("id", ""), + ) + schema = {"$schema": "meta schema id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_custom_validator_draft6(self): + Validator = validators.create( + meta_schema={"$id": "meta schema $id"}, + version="13", + ) + schema = {"$schema": "meta schema $id"} + self.assertIs( + validators.validator_for(schema), + Validator, + ) + + def test_validator_for_jsonschema_default(self): + self.assertIs(validators.validator_for({}), validators._LATEST_VERSION) + + def test_validator_for_custom_default(self): + self.assertIs(validators.validator_for({}, default=None), None) + + def test_warns_if_meta_schema_specified_was_not_found(self): + self.assertWarns( + category=DeprecationWarning, + message=( + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future." + ), + # https://tm.tl/9363 :'( + filename=sys.modules[self.assertWarns.__module__].__file__, + + f=validators.validator_for, + schema={u"$schema": "unknownSchema"}, + default={}, + ) + + def test_does_not_warn_if_meta_schema_is_unspecified(self): + validators.validator_for(schema={}, default={}), + self.assertFalse(self.flushWarnings()) + + +class TestValidate(SynchronousTestCase): + def assertUses(self, schema, Validator): + result = [] + self.patch(Validator, "check_schema", result.append) + validators.validate({}, schema) + self.assertEqual(result, [schema]) + + def test_draft3_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema#"}, + Validator=validators.Draft3Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-03/schema"}, + Validator=validators.Draft3Validator, + ) + + def test_draft4_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema#"}, + Validator=validators.Draft4Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-04/schema"}, + Validator=validators.Draft4Validator, + ) + + def test_draft6_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema#"}, + Validator=validators.Draft6Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-06/schema"}, + Validator=validators.Draft6Validator, + ) + + def test_draft7_validator_is_chosen(self): + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema#"}, + Validator=validators.Draft7Validator, + ) + # Make sure it works without the empty fragment + self.assertUses( + schema={"$schema": "http://json-schema.org/draft-07/schema"}, + Validator=validators.Draft7Validator, + ) + + def test_draft7_validator_is_the_default(self): + self.assertUses(schema={}, Validator=validators.Draft7Validator) + + def test_validation_error_message(self): + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, {"type": "string"}) + self.assertRegexpMatches( + str(e.exception), + "(?s)Failed validating u?'.*' in schema.*On instance", + ) + + def test_schema_error_message(self): + with self.assertRaises(exceptions.SchemaError) as e: + validators.validate(12, {"type": 12}) + self.assertRegexpMatches( + str(e.exception), + "(?s)Failed validating u?'.*' in metaschema.*On schema", + ) + + def test_it_uses_best_match(self): + # This is a schema that best_match will recurse into + schema = {"oneOf": [{"type": "string"}, {"type": "array"}]} + with self.assertRaises(exceptions.ValidationError) as e: + validators.validate(12, schema) + self.assertIn("12 is not of type", str(e.exception)) + + +class TestRefResolver(SynchronousTestCase): + + base_uri = "" + stored_uri = "foo://stored" + stored_schema = {"stored": "schema"} + + def setUp(self): + self.referrer = {} + self.store = {self.stored_uri: self.stored_schema} + self.resolver = validators.RefResolver( + self.base_uri, self.referrer, self.store, + ) + + def test_it_does_not_retrieve_schema_urls_from_the_network(self): + ref = validators.Draft3Validator.META_SCHEMA["id"] + self.patch( + self.resolver, + "resolve_remote", + lambda *args, **kwargs: self.fail("Should not have been called!"), + ) + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, validators.Draft3Validator.META_SCHEMA) + + def test_it_resolves_local_refs(self): + ref = "#/properties/foo" + self.referrer["properties"] = {"foo": object()} + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, self.referrer["properties"]["foo"]) + + def test_it_resolves_local_refs_with_id(self): + schema = {"id": "http://bar/schema#", "a": {"foo": "bar"}} + resolver = validators.RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get(u"id", u""), + ) + with resolver.resolving("#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + with resolver.resolving("http://bar/schema#/a") as resolved: + self.assertEqual(resolved, schema["a"]) + + def test_it_retrieves_stored_refs(self): + with self.resolver.resolving(self.stored_uri) as resolved: + self.assertIs(resolved, self.stored_schema) + + self.resolver.store["cached_ref"] = {"foo": 12} + with self.resolver.resolving("cached_ref#/foo") as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_requests(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = ReallyFakeRequests({"http://bar": schema}) + + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 12) + + def test_it_retrieves_unstored_refs_via_urlopen(self): + ref = "http://bar#baz" + schema = {"baz": 12} + + if "requests" in sys.modules: + self.addCleanup( + sys.modules.__setitem__, "requests", sys.modules["requests"], + ) + sys.modules["requests"] = None + + @contextmanager + def fake_urlopen(url): + self.assertEqual(url, "http://bar") + yield BytesIO(json.dumps(schema).encode("utf8")) + + self.addCleanup(setattr, validators, "urlopen", validators.urlopen) + validators.urlopen = fake_urlopen + + with self.resolver.resolving(ref) as resolved: + pass + self.assertEqual(resolved, 12) + + def test_it_retrieves_local_refs_via_urlopen(self): + with tempfile.NamedTemporaryFile(delete=False, mode='wt') as tempf: + self.addCleanup(os.remove, tempf.name) + json.dump({'foo': 'bar'}, tempf) + + ref = "file://{}#foo".format(pathname2url(tempf.name)) + with self.resolver.resolving(ref) as resolved: + self.assertEqual(resolved, 'bar') + + def test_it_can_construct_a_base_uri_from_a_schema(self): + schema = {"id": "foo"} + resolver = validators.RefResolver.from_schema( + schema, + id_of=lambda schema: schema.get(u"id", u""), + ) + self.assertEqual(resolver.base_uri, "foo") + self.assertEqual(resolver.resolution_scope, "foo") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("foo#") as resolved: + self.assertEqual(resolved, schema) + + def test_it_can_construct_a_base_uri_from_a_schema_without_id(self): + schema = {} + resolver = validators.RefResolver.from_schema(schema) + self.assertEqual(resolver.base_uri, "") + self.assertEqual(resolver.resolution_scope, "") + with resolver.resolving("") as resolved: + self.assertEqual(resolved, schema) + with resolver.resolving("#") as resolved: + self.assertEqual(resolved, schema) + + def test_custom_uri_scheme_handlers(self): + def handler(url): + self.assertEqual(url, ref) + return schema + + schema = {"foo": "bar"} + ref = "foo://bar" + resolver = validators.RefResolver("", {}, handlers={"foo": handler}) + with resolver.resolving(ref) as resolved: + self.assertEqual(resolved, schema) + + def test_cache_remote_on(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Response must not have been cached!") + + ref = "foo://bar" + resolver = validators.RefResolver( + "", {}, cache_remote=True, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + with resolver.resolving(ref): + pass + + def test_cache_remote_off(self): + response = [object()] + + def handler(url): + try: + return response.pop() + except IndexError: # pragma: no cover + self.fail("Handler called twice!") + + ref = "foo://bar" + resolver = validators.RefResolver( + "", {}, cache_remote=False, handlers={"foo": handler}, + ) + with resolver.resolving(ref): + pass + + def test_if_you_give_it_junk_you_get_a_resolution_error(self): + error = ValueError("Oh no! What's this?") + + def handler(url): + raise error + + ref = "foo://bar" + resolver = validators.RefResolver("", {}, handlers={"foo": handler}) + with self.assertRaises(exceptions.RefResolutionError) as err: + with resolver.resolving(ref): + self.fail("Shouldn't get this far!") # pragma: no cover + self.assertEqual(err.exception, exceptions.RefResolutionError(error)) + + def test_helpful_error_message_on_failed_pop_scope(self): + resolver = validators.RefResolver("", {}) + resolver.pop_scope() + with self.assertRaises(exceptions.RefResolutionError) as exc: + resolver.pop_scope() + self.assertIn("Failed to pop the scope", str(exc.exception)) + + +def sorted_errors(errors): + def key(error): + return ( + [str(e) for e in error.path], + [str(e) for e in error.schema_path], + ) + return sorted(errors, key=key) + + +@attr.s +class ReallyFakeRequests(object): + + _responses = attr.ib() + + def get(self, url): + response = self._responses.get(url) + if url is None: # pragma: no cover + raise ValueError("Unknown URL: " + repr(url)) + return _ReallyFakeJSONResponse(json.dumps(response)) + + +@attr.s +class _ReallyFakeJSONResponse(object): + + _response = attr.ib() + + def json(self): + return json.loads(self._response) diff --git a/jsonschema/validators.py b/jsonschema/validators.py new file mode 100644 index 0000000..7b7d76d --- /dev/null +++ b/jsonschema/validators.py @@ -0,0 +1,935 @@ +from __future__ import division + +from warnings import warn +import contextlib +import json +import numbers + +from six import add_metaclass + +from jsonschema import ( + _legacy_validators, + _types, + _utils, + _validators, + exceptions, +) +from jsonschema.compat import ( + Sequence, + int_types, + iteritems, + lru_cache, + str_types, + unquote, + urldefrag, + urljoin, + urlopen, + urlsplit, +) + +# Sigh. https://gitlab.com/pycqa/flake8/issues/280 +# https://github.com/pyga/ebb-lint/issues/7 +# Imported for backwards compatibility. +from jsonschema.exceptions import ErrorTree +ErrorTree + + +class _DontDoThat(Exception): + """ + Raised when a Validators with non-default type checker is misused. + + Asking one for DEFAULT_TYPES doesn't make sense, since type checkers exist + for the unrepresentable cases where DEFAULT_TYPES can't represent the type + relationship. + """ + + def __str__(self): + return "DEFAULT_TYPES cannot be used on Validators using TypeCheckers" + + +validators = {} +meta_schemas = _utils.URIDict() + + +def _generate_legacy_type_checks(types=()): + """ + Generate newer-style type checks out of JSON-type-name-to-type mappings. + + Arguments: + + types (dict): + + A mapping of type names to their Python types + + Returns: + + A dictionary of definitions to pass to `TypeChecker` + """ + types = dict(types) + + def gen_type_check(pytypes): + pytypes = _utils.flatten(pytypes) + + def type_check(checker, instance): + if isinstance(instance, bool): + if bool not in pytypes: + return False + return isinstance(instance, pytypes) + + return type_check + + definitions = {} + for typename, pytypes in iteritems(types): + definitions[typename] = gen_type_check(pytypes) + + return definitions + + +_DEPRECATED_DEFAULT_TYPES = { + u"array": list, + u"boolean": bool, + u"integer": int_types, + u"null": type(None), + u"number": numbers.Number, + u"object": dict, + u"string": str_types, +} +_TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES = _types.TypeChecker( + type_checkers=_generate_legacy_type_checks(_DEPRECATED_DEFAULT_TYPES), +) + + +def validates(version): + """ + Register the decorated validator for a ``version`` of the specification. + + Registered validators and their meta schemas will be considered when + parsing ``$schema`` properties' URIs. + + Arguments: + + version (str): + + An identifier to use as the version's name + + Returns: + + callable: a class decorator to decorate the validator with the version + """ + + def _validates(cls): + validators[version] = cls + meta_schema_id = cls.ID_OF(cls.META_SCHEMA) + if meta_schema_id: + meta_schemas[meta_schema_id] = cls + return cls + return _validates + + +def _DEFAULT_TYPES(self): + if self._CREATED_WITH_DEFAULT_TYPES is None: + raise _DontDoThat() + + warn( + ( + "The DEFAULT_TYPES attribute is deprecated. " + "See the type checker attached to this validator instead." + ), + DeprecationWarning, + stacklevel=2, + ) + return self._DEFAULT_TYPES + + +class _DefaultTypesDeprecatingMetaClass(type): + DEFAULT_TYPES = property(_DEFAULT_TYPES) + + +def _id_of(schema): + if schema is True or schema is False: + return u"" + return schema.get(u"$id", u"") + + +def create( + meta_schema, + validators=(), + version=None, + default_types=None, + type_checker=None, + id_of=_id_of, +): + """ + Create a new validator class. + + Arguments: + + meta_schema (collections.Mapping): + + the meta schema for the new validator class + + validators (collections.Mapping): + + a mapping from names to callables, where each callable will + validate the schema property with the given name. + + Each callable should take 4 arguments: + + 1. a validator instance, + 2. the value of the property being validated within the + instance + 3. the instance + 4. the schema + + version (str): + + an identifier for the version that this validator class will + validate. If provided, the returned validator class will have its + ``__name__`` set to include the version, and also will have + `jsonschema.validators.validates` automatically called for the + given version. + + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :validator:`type` validator. + + If unprovided, a `jsonschema.TypeChecker` will be created with + a set of default types typical of JSON Schema drafts. + + default_types (collections.Mapping): + + .. deprecated:: 3.0.0 + + Please use the type_checker argument instead. + + If set, it provides mappings of JSON types to Python types that + will be converted to functions and redefined in this object's + `jsonschema.TypeChecker`. + + id_of (callable): + + A function that given a schema, returns its ID. + + Returns: + + a new `jsonschema.IValidator` class + """ + + if default_types is not None: + if type_checker is not None: + raise TypeError( + "Do not specify default_types when providing a type checker.", + ) + _created_with_default_types = True + warn( + ( + "The default_types argument is deprecated. " + "Use the type_checker argument instead." + ), + DeprecationWarning, + stacklevel=2, + ) + type_checker = _types.TypeChecker( + type_checkers=_generate_legacy_type_checks(default_types), + ) + else: + default_types = _DEPRECATED_DEFAULT_TYPES + if type_checker is None: + _created_with_default_types = False + type_checker = _TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES + elif type_checker is _TYPE_CHECKER_FOR_DEPRECATED_DEFAULT_TYPES: + _created_with_default_types = False + else: + _created_with_default_types = None + + @add_metaclass(_DefaultTypesDeprecatingMetaClass) + class Validator(object): + + VALIDATORS = dict(validators) + META_SCHEMA = dict(meta_schema) + TYPE_CHECKER = type_checker + ID_OF = staticmethod(id_of) + + DEFAULT_TYPES = property(_DEFAULT_TYPES) + _DEFAULT_TYPES = dict(default_types) + _CREATED_WITH_DEFAULT_TYPES = _created_with_default_types + + def __init__( + self, + schema, + types=(), + resolver=None, + format_checker=None, + ): + if types: + warn( + ( + "The types argument is deprecated. Provide " + "a type_checker to jsonschema.validators.extend " + "instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + self.TYPE_CHECKER = self.TYPE_CHECKER.redefine_many( + _generate_legacy_type_checks(types), + ) + + if resolver is None: + resolver = RefResolver.from_schema(schema, id_of=id_of) + + self.resolver = resolver + self.format_checker = format_checker + self.schema = schema + + @classmethod + def check_schema(cls, schema): + for error in cls(cls.META_SCHEMA).iter_errors(schema): + raise exceptions.SchemaError.create_from(error) + + def iter_errors(self, instance, _schema=None): + if _schema is None: + _schema = self.schema + + if _schema is True: + return + elif _schema is False: + yield exceptions.ValidationError( + "False schema does not allow %r" % (instance,), + validator=None, + validator_value=None, + instance=instance, + schema=_schema, + ) + return + + scope = id_of(_schema) + if scope: + self.resolver.push_scope(scope) + try: + ref = _schema.get(u"$ref") + if ref is not None: + validators = [(u"$ref", ref)] + else: + validators = iteritems(_schema) + + for k, v in validators: + validator = self.VALIDATORS.get(k) + if validator is None: + continue + + errors = validator(self, v, instance, _schema) or () + for error in errors: + # set details if not already set by the called fn + error._set( + validator=k, + validator_value=v, + instance=instance, + schema=_schema, + ) + if k != u"$ref": + error.schema_path.appendleft(k) + yield error + finally: + if scope: + self.resolver.pop_scope() + + def descend(self, instance, schema, path=None, schema_path=None): + for error in self.iter_errors(instance, schema): + if path is not None: + error.path.appendleft(path) + if schema_path is not None: + error.schema_path.appendleft(schema_path) + yield error + + def validate(self, *args, **kwargs): + for error in self.iter_errors(*args, **kwargs): + raise error + + def is_type(self, instance, type): + try: + return self.TYPE_CHECKER.is_type(instance, type) + except exceptions.UndefinedTypeCheck: + raise exceptions.UnknownType(type, instance, self.schema) + + def is_valid(self, instance, _schema=None): + error = next(self.iter_errors(instance, _schema), None) + return error is None + + if version is not None: + Validator = validates(version)(Validator) + Validator.__name__ = version.title().replace(" ", "") + "Validator" + + return Validator + + +def extend(validator, validators=(), version=None, type_checker=None): + """ + Create a new validator class by extending an existing one. + + Arguments: + + validator (jsonschema.IValidator): + + an existing validator class + + validators (collections.Mapping): + + a mapping of new validator callables to extend with, whose + structure is as in `create`. + + .. note:: + + Any validator callables with the same name as an existing one + will (silently) replace the old validator callable entirely, + effectively overriding any validation done in the "parent" + validator class. + + If you wish to instead extend the behavior of a parent's + validator callable, delegate and call it directly in the new + validator function by retrieving it using + ``OldValidator.VALIDATORS["validator_name"]``. + + version (str): + + a version for the new validator class + + type_checker (jsonschema.TypeChecker): + + a type checker, used when applying the :validator:`type` validator. + + If unprovided, the type checker of the extended + `jsonschema.IValidator` will be carried along.` + + Returns: + + a new `jsonschema.IValidator` class extending the one provided + + .. note:: Meta Schemas + + The new validator class will have its parent's meta schema. + + If you wish to change or extend the meta schema in the new + validator class, modify ``META_SCHEMA`` directly on the returned + class. Note that no implicit copying is done, so a copy should + likely be made before modifying it, in order to not affect the + old validator. + """ + + all_validators = dict(validator.VALIDATORS) + all_validators.update(validators) + + if type_checker is None: + type_checker = validator.TYPE_CHECKER + elif validator._CREATED_WITH_DEFAULT_TYPES: + raise TypeError( + "Cannot extend a validator created with default_types " + "with a type_checker. Update the validator to use a " + "type_checker when created." + ) + return create( + meta_schema=validator.META_SCHEMA, + validators=all_validators, + version=version, + type_checker=type_checker, + id_of=validator.ID_OF, + ) + + +Draft3Validator = create( + meta_schema=_utils.load_schema("draft3"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"dependencies": _legacy_validators.dependencies_draft3, + u"disallow": _legacy_validators.disallow_draft3, + u"divisibleBy": _validators.multipleOf, + u"enum": _validators.enum, + u"extends": _legacy_validators.extends_draft3, + u"format": _validators.format, + u"items": _legacy_validators.items_draft3_draft4, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maximum": _legacy_validators.maximum_draft3_draft4, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minimum": _legacy_validators.minimum_draft3_draft4, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _legacy_validators.properties_draft3, + u"type": _legacy_validators.type_draft3, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft3_type_checker, + version="draft3", + id_of=lambda schema: schema.get(u"id", ""), +) + +Draft4Validator = create( + meta_schema=_utils.load_schema("draft4"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"format": _validators.format, + u"items": _legacy_validators.items_draft3_draft4, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _legacy_validators.maximum_draft3_draft4, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _legacy_validators.minimum_draft3_draft4, + u"multipleOf": _validators.multipleOf, + u"not": _validators.not_, + u"oneOf": _validators.oneOf, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft4_type_checker, + version="draft4", + id_of=lambda schema: schema.get(u"id", ""), +) + +Draft6Validator = create( + meta_schema=_utils.load_schema("draft6"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"const": _validators.const, + u"contains": _validators.contains, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"format": _validators.format, + u"items": _validators.items, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _validators.maximum, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _validators.minimum, + u"multipleOf": _validators.multipleOf, + u"not": _validators.not_, + u"oneOf": _validators.oneOf, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"propertyNames": _validators.propertyNames, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft6_type_checker, + version="draft6", +) + +Draft7Validator = create( + meta_schema=_utils.load_schema("draft7"), + validators={ + u"$ref": _validators.ref, + u"additionalItems": _validators.additionalItems, + u"additionalProperties": _validators.additionalProperties, + u"allOf": _validators.allOf, + u"anyOf": _validators.anyOf, + u"const": _validators.const, + u"contains": _validators.contains, + u"dependencies": _validators.dependencies, + u"enum": _validators.enum, + u"exclusiveMaximum": _validators.exclusiveMaximum, + u"exclusiveMinimum": _validators.exclusiveMinimum, + u"format": _validators.format, + u"if": _validators.if_, + u"items": _validators.items, + u"maxItems": _validators.maxItems, + u"maxLength": _validators.maxLength, + u"maxProperties": _validators.maxProperties, + u"maximum": _validators.maximum, + u"minItems": _validators.minItems, + u"minLength": _validators.minLength, + u"minProperties": _validators.minProperties, + u"minimum": _validators.minimum, + u"multipleOf": _validators.multipleOf, + u"oneOf": _validators.oneOf, + u"not": _validators.not_, + u"pattern": _validators.pattern, + u"patternProperties": _validators.patternProperties, + u"properties": _validators.properties, + u"propertyNames": _validators.propertyNames, + u"required": _validators.required, + u"type": _validators.type, + u"uniqueItems": _validators.uniqueItems, + }, + type_checker=_types.draft7_type_checker, + version="draft7", +) + +_LATEST_VERSION = Draft7Validator + + +class RefResolver(object): + """ + Resolve JSON References. + + Arguments: + + base_uri (str): + + The URI of the referring document + + referrer: + + The actual referring document + + store (dict): + + A mapping from URIs to documents to cache + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + + handlers (dict): + + A mapping from URI schemes to functions that should be used + to retrieve them + + urljoin_cache (functools.lru_cache): + + A cache that will be used for caching the results of joining + the resolution scope to subscopes. + + remote_cache (functools.lru_cache): + + A cache that will be used for caching the results of + resolved remote URLs. + + Attributes: + + cache_remote (bool): + + Whether remote refs should be cached after first resolution + """ + + def __init__( + self, + base_uri, + referrer, + store=(), + cache_remote=True, + handlers=(), + urljoin_cache=None, + remote_cache=None, + ): + if urljoin_cache is None: + urljoin_cache = lru_cache(1024)(urljoin) + if remote_cache is None: + remote_cache = lru_cache(1024)(self.resolve_from_url) + + self.referrer = referrer + self.cache_remote = cache_remote + self.handlers = dict(handlers) + + self._scopes_stack = [base_uri] + self.store = _utils.URIDict( + (id, validator.META_SCHEMA) + for id, validator in iteritems(meta_schemas) + ) + self.store.update(store) + self.store[base_uri] = referrer + + self._urljoin_cache = urljoin_cache + self._remote_cache = remote_cache + + @classmethod + def from_schema(cls, schema, id_of=_id_of, *args, **kwargs): + """ + Construct a resolver from a JSON schema object. + + Arguments: + + schema: + + the referring schema + + Returns: + + `RefResolver` + """ + + return cls(base_uri=id_of(schema), referrer=schema, *args, **kwargs) + + def push_scope(self, scope): + self._scopes_stack.append( + self._urljoin_cache(self.resolution_scope, scope), + ) + + def pop_scope(self): + try: + self._scopes_stack.pop() + except IndexError: + raise exceptions.RefResolutionError( + "Failed to pop the scope from an empty stack. " + "`pop_scope()` should only be called once for every " + "`push_scope()`" + ) + + @property + def resolution_scope(self): + return self._scopes_stack[-1] + + @property + def base_uri(self): + uri, _ = urldefrag(self.resolution_scope) + return uri + + @contextlib.contextmanager + def in_scope(self, scope): + self.push_scope(scope) + try: + yield + finally: + self.pop_scope() + + @contextlib.contextmanager + def resolving(self, ref): + """ + Resolve the given ``ref`` and enter its resolution scope. + + Exits the scope on exit of this context manager. + + Arguments: + + ref (str): + + The reference to resolve + """ + + url, resolved = self.resolve(ref) + self.push_scope(url) + try: + yield resolved + finally: + self.pop_scope() + + def resolve(self, ref): + url = self._urljoin_cache(self.resolution_scope, ref) + return url, self._remote_cache(url) + + def resolve_from_url(self, url): + url, fragment = urldefrag(url) + try: + document = self.store[url] + except KeyError: + try: + document = self.resolve_remote(url) + except Exception as exc: + raise exceptions.RefResolutionError(exc) + + return self.resolve_fragment(document, fragment) + + def resolve_fragment(self, document, fragment): + """ + Resolve a ``fragment`` within the referenced ``document``. + + Arguments: + + document: + + The referent document + + fragment (str): + + a URI fragment to resolve within it + """ + + fragment = fragment.lstrip(u"/") + parts = unquote(fragment).split(u"/") if fragment else [] + + for part in parts: + part = part.replace(u"~1", u"/").replace(u"~0", u"~") + + if isinstance(document, Sequence): + # Array indexes should be turned into integers + try: + part = int(part) + except ValueError: + pass + try: + document = document[part] + except (TypeError, LookupError): + raise exceptions.RefResolutionError( + "Unresolvable JSON pointer: %r" % fragment + ) + + return document + + def resolve_remote(self, uri): + """ + Resolve a remote ``uri``. + + If called directly, does not check the store first, but after + retrieving the document at the specified URI it will be saved in + the store if :attr:`cache_remote` is True. + + .. note:: + + If the requests_ library is present, ``jsonschema`` will use it to + request the remote ``uri``, so that the correct encoding is + detected and used. + + If it isn't, or if the scheme of the ``uri`` is not ``http`` or + ``https``, UTF-8 is assumed. + + Arguments: + + uri (str): + + The URI to resolve + + Returns: + + The retrieved document + + .. _requests: https://pypi.org/project/requests/ + """ + try: + import requests + except ImportError: + requests = None + + scheme = urlsplit(uri).scheme + + if scheme in self.handlers: + result = self.handlers[scheme](uri) + elif scheme in [u"http", u"https"] and requests: + # Requests has support for detecting the correct encoding of + # json over http + result = requests.get(uri).json() + else: + # Otherwise, pass off to urllib and assume utf-8 + with urlopen(uri) as url: + result = json.loads(url.read().decode("utf-8")) + + if self.cache_remote: + self.store[uri] = result + return result + + +def validate(instance, schema, cls=None, *args, **kwargs): + """ + Validate an instance under the given schema. + + >>> validate([2, 3, 4], {"maxItems": 2}) + Traceback (most recent call last): + ... + ValidationError: [2, 3, 4] is too long + + :func:`validate` will first verify that the provided schema is itself + valid, since not doing so can lead to less obvious error messages and fail + in less obvious or consistent ways. + + If you know you have a valid schema already, especially if you + intend to validate multiple instances with the same schema, you + likely would prefer using the `IValidator.validate` method directly + on a specific validator (e.g. ``Draft7Validator.validate``). + + + Arguments: + + instance: + + The instance to validate + + schema: + + The schema to validate with + + cls (IValidator): + + The class that will be used to validate the instance. + + If the ``cls`` argument is not provided, two things will happen in + accordance with the specification. First, if the schema has a + :validator:`$schema` property containing a known meta-schema [#]_ then the + proper validator will be used. The specification recommends that all + schemas contain :validator:`$schema` properties for this reason. If no + :validator:`$schema` property is found, the default validator class is + the latest released draft. + + Any other provided positional and keyword arguments will be passed on when + instantiating the ``cls``. + + Raises: + + `jsonschema.exceptions.ValidationError` if the instance + is invalid + + `jsonschema.exceptions.SchemaError` if the schema itself + is invalid + + .. rubric:: Footnotes + .. [#] known by a validator registered with + `jsonschema.validators.validates` + """ + if cls is None: + cls = validator_for(schema) + + cls.check_schema(schema) + validator = cls(schema, *args, **kwargs) + error = exceptions.best_match(validator.iter_errors(instance)) + if error is not None: + raise error + + +def validator_for(schema, default=_LATEST_VERSION): + """ + Retrieve the validator class appropriate for validating the given schema. + + Uses the :validator:`$schema` property that should be present in the given + schema to look up the appropriate validator class. + + Arguments: + + schema (collections.Mapping or bool): + + the schema to look at + + default: + + the default to return if the appropriate validator class cannot be + determined. + + If unprovided, the default is to return + the latest supported draft. + """ + if schema is True or schema is False or u"$schema" not in schema: + return default + if schema[u"$schema"] not in meta_schemas: + warn( + ( + "The metaschema specified by $schema was not found. " + "Using the latest draft to validate, but this will raise " + "an error in the future." + ), + DeprecationWarning, + stacklevel=2, + ) + return meta_schemas.get(schema[u"$schema"], _LATEST_VERSION) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7d56e5f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,56 @@ +[metadata] +name = jsonschema +url = https://github.com/Julian/jsonschema +project_urls = + Docs = https://python-jsonschema.readthedocs.io/en/latest/ +description = An implementation of JSON Schema validation for Python +long_description = file: README.rst +author = Julian Berman +author_email = Julian@GrayVines.com +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +setup_requires = setuptools_scm +install_requires = + attrs>=17.4.0 + pyrsistent>=0.14.0 + setuptools + six>=1.11.0 + functools32;python_version<'3' + +[options.extras_require] +format = + idna + jsonpointer>1.13 + rfc3987 + strict-rfc3339 + webcolors + +[options.entry_points] +console_scripts = + jsonschema = jsonschema.cli:main + +[options.package_data] +jsonschema = schemas/*.json + +[flake8] +exclude = + jsonschema/__init__.py + jsonschema/_reflect.py + +[wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..460aabe --- /dev/null +++ b/setup.py @@ -0,0 +1,2 @@ +from setuptools import setup +setup(use_scm_version=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..082785c --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +Twisted @@ -1,8 +1,141 @@ [tox] -minversion = 1.6 -envlist = py27 skipsdist = True +envlist = + py{27,35,36,37,py,py3}-{build,tests}, + docs-{html,doctest,linkcheck,spelling,style}, + readme, + safety, + style, [testenv] -deps = jsonschema -commands = {envpython} bin/jsonschema_suite check +changedir = + !build: {envtmpdir} +setenv = + JSON_SCHEMA_TEST_SUITE = {toxinidir}/json +whitelist_externals = + python2.7 + sh + virtualenv +commands = + perf,tests: {envbindir}/python -m pip install '{toxinidir}[format]' + + tests: {envbindir}/trial {posargs:jsonschema} + py{py,27,37}-tests: {envpython} -m doctest {toxinidir}/README.rst + + perf: {envpython} {toxinidir}/jsonschema/benchmarks/json_schema_test_suite.py --inherit-environ JSON_SCHEMA_TEST_SUITE + perf: {envpython} {toxinidir}/jsonschema/benchmarks/issue232.py --inherit-environ JSON_SCHEMA_TEST_SUITE + + # Check to make sure that releases build and install properly + build: virtualenv --quiet --python=python2.7 {envtmpdir}/venv + build: {envtmpdir}/venv/bin/pip install --quiet wheel + + build: {envtmpdir}/venv/bin/python {toxinidir}/setup.py --quiet bdist_wheel --dist-dir={envtmpdir}/wheel + build: sh -c '{envbindir}/pip install --quiet --upgrade --force-reinstall {envtmpdir}/wheel/jsonschema*.whl' + + build: python2.7 {toxinidir}/setup.py --quiet sdist --dist-dir={envtmpdir}/sdist --format=gztar,zip + build: sh -c '{envbindir}/pip install --quiet --upgrade --force-reinstall {envtmpdir}/sdist/jsonschema*.tar.gz' + build: sh -c '{envbindir}/pip install --quiet --upgrade --force-reinstall {envtmpdir}/sdist/jsonschema*.zip' +deps = + -r{toxinidir}/test-requirements.txt + + tests,coverage,codecov: twisted + tests: lxml + tests: sphinx + + coverage,codecov: coverage + codecov: codecov + + perf: perf + safety: safety + +[testenv:readme] +changedir = {toxinidir} +deps = readme_renderer +commands = + {envbindir}/python setup.py check --restructuredtext --strict + +[testenv:safety] +deps = safety +commands = + {envbindir}/pip install '{toxinidir}[format]' + {envbindir}/safety check + +[testenv:style] +basepython = pypy +deps = ebb-lint>=0.19.1.0 +commands = {envbindir}/flake8 {posargs} {toxinidir}/jsonschema {toxinidir}/docs {toxinidir}/setup.py + +[testenv:coverage] +setenv = + {[testenv]setenv} + COVERAGE_DEBUG_FILE={envtmpdir}/coverage-debug + COVERAGE_FILE={envtmpdir}/coverage-data +commands = + {envbindir}/python -m pip install '{toxinidir}[format]' + {envbindir}/coverage run --rcfile={toxinidir}/.coveragerc {envbindir}/trial jsonschema + {envbindir}/coverage report --rcfile={toxinidir}/.coveragerc --show-missing + {envbindir}/coverage html --directory={envtmpdir}/htmlcov --rcfile={toxinidir}/.coveragerc {posargs} + +[testenv:codecov] +passenv = CODECOV* CI TRAVIS TRAVIS_* +setenv = {[testenv:coverage]setenv} +commands = + {envbindir}/python -m pip install '{toxinidir}[format]' + {envbindir}/coverage run --rcfile={toxinidir}/.coveragerc {envbindir}/trial jsonschema + {envbindir}/coverage xml -o {envtmpdir}/coverage.xml + codecov --required --disable gcov --file {envtmpdir}/coverage.xml + +[testenv:docs-html] +basepython = pypy +changedir = docs +whitelist_externals = make +commands = make -f {toxinidir}/docs/Makefile BUILDDIR={envtmpdir}/build SPHINXOPTS='-a -c {toxinidir}/docs/ -n -T -W {posargs}' html +deps = + -r{toxinidir}/docs/requirements.txt + -e{toxinidir} + +[testenv:docs-doctest] +basepython = pypy +changedir = docs +whitelist_externals = make +commands = make -f {toxinidir}/docs/Makefile BUILDDIR={envtmpdir}/build SPHINXOPTS='-a -c {toxinidir}/docs/ -n -T -W {posargs}' doctest +deps = + -r{toxinidir}/docs/requirements.txt + -e{toxinidir} + +[testenv:docs-linkcheck] +basepython = pypy +changedir = docs +whitelist_externals = make +commands = make -f {toxinidir}/docs/Makefile BUILDDIR={envtmpdir}/build SPHINXOPTS='-a -c {toxinidir}/docs/ -n -T -W {posargs}' linkcheck +deps = + -r{toxinidir}/docs/requirements.txt + -e{toxinidir} + +[testenv:docs-spelling] +basepython = pypy +changedir = docs +whitelist_externals = make +commands = make -f {toxinidir}/docs/Makefile BUILDDIR={envtmpdir}/build SPHINXOPTS='-a -c {toxinidir}/docs/ -n -T -W {posargs}' spelling +deps = + -r{toxinidir}/docs/requirements.txt + -e{toxinidir} + +[testenv:docs-style] +basepython = pypy +changedir = docs +commands = doc8 {posargs} {toxinidir}/docs +deps = + doc8 + pygments + pygments-github-lexers + +[doc8] +ignore-path = + version.txt, + .*/, + _*/ + +[travis] +python = + pypy: pypy, docs, readme, style |