././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714820172.370068 schema-0.7.7/0000775000175000017500000000000014615412114013012 5ustar00stavrosstavros././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714819023.0 schema-0.7.7/LICENSE-MIT0000664000175000017500000000207614615407717014470 0ustar00stavrosstavrosCopyright (c) 2012 Vladimir Keleshev, 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1592394732.0 schema-0.7.7/MANIFEST.in0000664000175000017500000000006513672401754014563 0ustar00stavrosstavrosinclude README.rst requirements.txt LICENSE-MIT *.py ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714820172.370068 schema-0.7.7/PKG-INFO0000664000175000017500000010234114615412114014110 0ustar00stavrosstavrosMetadata-Version: 2.1 Name: schema Version: 0.7.7 Summary: Simple data validation library Home-page: https://github.com/keleshev/schema Author: Vladimir Keleshev Author-email: vladimir@keleshev.com License: MIT Keywords: schema json validation Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: MIT License Description-Content-Type: text/x-rst License-File: LICENSE-MIT Schema validation just got Pythonic =============================================================================== **schema** is a library for validating Python data structures, such as those obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types. .. image:: https://secure.travis-ci.org/keleshev/schema.svg?branch=master :target: https://travis-ci.org/keleshev/schema .. image:: https://img.shields.io/codecov/c/github/keleshev/schema.svg :target: http://codecov.io/github/keleshev/schema Example ---------------------------------------------------------------------------- Here is a quick example to get a feeling of **schema**, validating a list of entries with personal information: .. code:: python from schema import Schema, And, Use, Optional, SchemaError schema = Schema( [ { "name": And(str, len), "age": And(Use(int), lambda n: 18 <= n <= 99), Optional("gender"): And( str, Use(str.lower), lambda s: s in ("squid", "kid"), ), } ] ) data = [ {"name": "Sue", "age": "28", "gender": "Squid"}, {"name": "Sam", "age": "42"}, {"name": "Sacha", "age": "20", "gender": "KID"}, ] validated = schema.validate(data) assert validated == [ {"name": "Sue", "age": 28, "gender": "squid"}, {"name": "Sam", "age": 42}, {"name": "Sacha", "age": 20, "gender": "kid"}, ] If data is valid, ``Schema.validate`` will return the validated data (optionally converted with `Use` calls, see below). If data is invalid, ``Schema`` will raise ``SchemaError`` exception. If you just want to check that the data is valid, ``schema.is_valid(data)`` will return ``True`` or ``False``. Installation ------------------------------------------------------------------------------- Use `pip `_ or easy_install:: pip install schema Alternatively, you can just drop ``schema.py`` file into your project—it is self-contained. - **schema** is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 and PyPy. - **schema** follows `semantic versioning `_. How ``Schema`` validates data ------------------------------------------------------------------------------- Types ~~~~~ If ``Schema(...)`` encounters a type (such as ``int``, ``str``, ``object``, etc.), it will check if the corresponding piece of data is an instance of that type, otherwise it will raise ``SchemaError``. .. code:: python >>> from schema import Schema >>> Schema(int).validate(123) 123 >>> Schema(int).validate('123') Traceback (most recent call last): ... schema.SchemaUnexpectedTypeError: '123' should be instance of 'int' >>> Schema(object).validate('hai') 'hai' Callables ~~~~~~~~~ If ``Schema(...)`` encounters a callable (function, class, or object with ``__call__`` method) it will call it, and if its return value evaluates to ``True`` it will continue validating, else—it will raise ``SchemaError``. .. code:: python >>> import os >>> Schema(os.path.exists).validate('./') './' >>> Schema(os.path.exists).validate('./non-existent/') Traceback (most recent call last): ... schema.SchemaError: exists('./non-existent/') should evaluate to True >>> Schema(lambda n: n > 0).validate(123) 123 >>> Schema(lambda n: n > 0).validate(-12) Traceback (most recent call last): ... schema.SchemaError: (-12) should evaluate to True "Validatables" ~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an object with method ``validate`` it will run this method on corresponding data as ``data = obj.validate(data)``. This method may raise ``SchemaError`` exception, which will tell ``Schema`` that that piece of data is invalid, otherwise—it will continue validating. An example of "validatable" is ``Regex``, that tries to match a string or a buffer with the given regular expression (itself as a string, buffer or compiled regex ``SRE_Pattern``): .. code:: python >>> from schema import Regex >>> import re >>> Regex(r'^foo').validate('foobar') 'foobar' >>> Regex(r'^[A-Z]+$', flags=re.I).validate('those-dashes-dont-match') Traceback (most recent call last): ... schema.SchemaError: Regex('^[A-Z]+$', flags=re.IGNORECASE) does not match 'those-dashes-dont-match' For a more general case, you can use ``Use`` for creating such objects. ``Use`` helps to use a function or type to convert a value while validating it: .. code:: python >>> from schema import Use >>> Schema(Use(int)).validate('123') 123 >>> Schema(Use(lambda f: open(f, 'a'))).validate('LICENSE-MIT') <_io.TextIOWrapper name='LICENSE-MIT' mode='a' encoding='UTF-8'> Dropping the details, ``Use`` is basically: .. code:: python class Use(object): def __init__(self, callable_): self._callable = callable_ def validate(self, data): try: return self._callable(data) except Exception as e: raise SchemaError('%r raised %r' % (self._callable.__name__, e)) Sometimes you need to transform and validate part of data, but keep original data unchanged. ``Const`` helps to keep your data safe: .. code:: python >> from schema import Use, Const, And, Schema >> from datetime import datetime >> is_future = lambda date: datetime.now() > date >> to_json = lambda v: {"timestamp": v} >> Schema(And(Const(And(Use(datetime.fromtimestamp), is_future)), Use(to_json))).validate(1234567890) {"timestamp": 1234567890} Now you can write your own validation-aware classes and data types. Lists, similar containers ~~~~~~~~~~~~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``list``, ``tuple``, ``set`` or ``frozenset``, it will validate contents of corresponding data container against all schemas listed inside that container and aggregate all errors: .. code:: python >>> Schema([1, 0]).validate([1, 1, 0, 1]) [1, 1, 0, 1] >>> Schema((int, float)).validate((5, 7, 8, 'not int or float here')) Traceback (most recent call last): ... schema.SchemaError: Or(, ) did not validate 'not int or float here' 'not int or float here' should be instance of 'int' 'not int or float here' should be instance of 'float' Dictionaries ~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``dict``, it will validate data key-value pairs: .. code:: python >>> d = Schema( ... {"name": str, "age": lambda n: 18 <= n <= 99} ... ).validate( ... {"name": "Sue", "age": 28} ... ) >>> assert d == {'name': 'Sue', 'age': 28} You can specify keys as schemas too: .. code:: python >>> schema = Schema({ ... str: int, # string keys should have integer values ... int: None, # int keys should be always None ... }) >>> data = schema.validate({ ... "key1": 1, ... "key2": 2, ... 10: None, ... 20: None, ... }) >>> schema.validate({ ... "key1": 1, ... 10: "not None here", ... }) Traceback (most recent call last): ... schema.SchemaError: Key '10' error: None does not match 'not None here' This is useful if you want to check certain key-values, but don't care about others: .. code:: python >>> schema = Schema({ ... "": int, ... "": Use(open), ... str: object, # don't care about other str keys ... }) >>> data = schema.validate({ ... "": 10, ... "": "README.rst", ... "--verbose": True, ... }) You can mark a key as optional as follows: .. code:: python >>> Schema({ ... "name": str, ... Optional("occupation"): str, ... }).validate({"name": "Sam"}) {'name': 'Sam'} ``Optional`` keys can also carry a ``default``, to be used when no key in the data matches: .. code:: python >>> Schema({ ... Optional("color", default="blue"): str, ... str: str, ... }).validate({"texture": "furry"}) == { ... "color": "blue", ... "texture": "furry", ... } True Defaults are used verbatim, not passed through any validators specified in the value. default can also be a callable: .. code:: python >>> from schema import Schema, Optional >>> Schema({Optional('data', default=dict): {}}).validate({}) == {'data': {}} True Also, a caveat: If you specify types, **schema** won't validate the empty dict: .. code:: python >>> Schema({int:int}).is_valid({}) False To do that, you need ``Schema(Or({int:int}, {}))``. This is unlike what happens with lists, where ``Schema([int]).is_valid([])`` will return True. **schema** has classes ``And`` and ``Or`` that help validating several schemas for the same data: .. code:: python >>> from schema import And, Or >>> Schema({'age': And(int, lambda n: 0 < n < 99)}).validate({'age': 7}) {'age': 7} >>> Schema({'password': And(str, lambda s: len(s) > 6)}).validate({'password': 'hai'}) Traceback (most recent call last): ... schema.SchemaError: Key 'password' error: ('hai') should evaluate to True >>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415) 3.1415 In a dictionary, you can also combine two keys in a "one or the other" manner. To do so, use the `Or` class as a key: .. code:: python >>> from schema import Or, Schema >>> schema = Schema({ ... Or("key1", "key2", only_one=True): str ... }) >>> schema.validate({"key1": "test"}) # Ok {'key1': 'test'} >>> schema.validate({"key1": "test", "key2": "test"}) # SchemaError Traceback (most recent call last): ... schema.SchemaOnlyOneAllowedError: There are multiple keys present from the Or('key1', 'key2') condition Hooks ~~~~~~~~~~ You can define hooks which are functions that are executed whenever a valid key:value is found. The `Forbidden` class is an example of this. You can mark a key as forbidden as follows: .. code:: python >>> from schema import Forbidden >>> Schema({Forbidden('age'): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} A few things are worth noting. First, the value paired with the forbidden key determines whether it will be rejected: .. code:: python >>> Schema({Forbidden('age'): str, 'age': int}).validate({'age': 50}) {'age': 50} Note: if we hadn't supplied the 'age' key here, the call would have failed too, but with SchemaWrongKeyError, not SchemaForbiddenKeyError. Second, Forbidden has a higher priority than standard keys, and consequently than Optional. This means we can do that: .. code:: python >>> Schema({Forbidden('age'): object, Optional(str): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} You can also define your own hooks. The following hook will call `_my_function` if `key` is encountered. .. code:: python from schema import Hook def _my_function(key, scope, error): print(key, scope, error) Hook("key", handler=_my_function) Here's an example where a `Deprecated` class is added to log warnings whenever a key is encountered: .. code:: python from schema import Hook, Schema class Deprecated(Hook): def __init__(self, *args, **kwargs): kwargs["handler"] = lambda key, *args: logging.warn(f"`{key}` is deprecated. " + (self._error or "")) super(Deprecated, self).__init__(*args, **kwargs) Schema({Deprecated("test", "custom error message."): object}, ignore_extra_keys=True).validate({"test": "value"}) ... WARNING: `test` is deprecated. custom error message. Extra Keys ~~~~~~~~~~ The ``Schema(...)`` parameter ``ignore_extra_keys`` causes validation to ignore extra keys in a dictionary, and also to not return them after validating. .. code:: python >>> schema = Schema({'name': str}, ignore_extra_keys=True) >>> schema.validate({'name': 'Sam', 'age': '42'}) {'name': 'Sam'} If you would like any extra keys returned, use ``object: object`` as one of the key/value pairs, which will match any key and any value. Otherwise, extra keys will raise a ``SchemaError``. Customized Validation ~~~~~~~~~~~~~~~~~~~~~~~ The ``Schema.validate`` method accepts additional keyword arguments. The keyword arguments will be propagated to the ``validate`` method of any child validatables (including any ad-hoc ``Schema`` objects), or the default value callable (if a callable is specified) for ``Optional`` keys. This feature can be used together with inheritance of the ``Schema`` class for customized validation. Here is an example where a "post-validation" hook that runs after validation against a sub-schema in a larger schema: .. code:: python class EventSchema(schema.Schema): def validate(self, data, _is_event_schema=True): data = super(EventSchema, self).validate(data, _is_event_schema=False) if _is_event_schema and data.get("minimum", None) is None: data["minimum"] = data["capacity"] return data events_schema = schema.Schema( { str: EventSchema({ "capacity": int, schema.Optional("minimum"): int, # default to capacity }) } ) data = {'event1': {'capacity': 1}, 'event2': {'capacity': 2, 'minimum': 3}} events = events_schema.validate(data) assert events['event1']['minimum'] == 1 # == capacity assert events['event2']['minimum'] == 3 Note that the additional keyword argument ``_is_event_schema`` is necessary to limit the customized behavior to the ``EventSchema`` object itself so that it won't affect any recursive invoke of the ``self.__class__.validate`` for the child schemas (e.g., the call to ``Schema("capacity").validate("capacity")``). User-friendly error reporting ------------------------------------------------------------------------------- You can pass a keyword argument ``error`` to any of validatable classes (such as ``Schema``, ``And``, ``Or``, ``Regex``, ``Use``) to report this error instead of a built-in one. .. code:: python >>> Schema(Use(int, error='Invalid year')).validate('XVII') Traceback (most recent call last): ... schema.SchemaError: Invalid year You can see all errors that occurred by accessing exception's ``exc.autos`` for auto-generated error messages, and ``exc.errors`` for errors which had ``error`` text passed to them. You can exit with ``sys.exit(exc.code)`` if you want to show the messages to the user without traceback. ``error`` messages are given precedence in that case. A JSON API example ------------------------------------------------------------------------------- Here is a quick example: validation of `create a gist `_ request from github API. .. code:: python >>> gist = '''{"description": "the description for this gist", ... "public": true, ... "files": { ... "file1.txt": {"content": "String file contents"}, ... "other.txt": {"content": "Another file contents"}}}''' >>> from schema import Schema, And, Use, Optional >>> import json >>> gist_schema = Schema( ... And( ... Use(json.loads), # first convert from JSON ... # use str since json returns unicode ... { ... Optional("description"): str, ... "public": bool, ... "files": {str: {"content": str}}, ... }, ... ) ... ) >>> gist = gist_schema.validate(gist) # gist: {u'description': u'the description for this gist', u'files': {u'file1.txt': {u'content': u'String file contents'}, u'other.txt': {u'content': u'Another file contents'}}, u'public': True} Using **schema** with `docopt `_ ------------------------------------------------------------------------------- Assume you are using **docopt** with the following usage-pattern: Usage: my_program.py [--count=N] ... and you would like to validate that ```` are readable, and that ```` exists, and that ``--count`` is either integer from 0 to 5, or ``None``. Assuming **docopt** returns the following dict: .. code:: python >>> args = { ... "": ["LICENSE-MIT", "setup.py"], ... "": "../", ... "--count": "3", ... } this is how you validate it using ``schema``: .. code:: python >>> from schema import Schema, And, Or, Use >>> import os >>> s = Schema({ ... "": [Use(open)], ... "": os.path.exists, ... "--count": Or(None, And(Use(int), lambda n: 0 < n < 5)), ... }) >>> args = s.validate(args) >>> args[''] [<_io.TextIOWrapper name='LICENSE-MIT' ...>, <_io.TextIOWrapper name='setup.py' ...] >>> args[''] '../' >>> args['--count'] 3 As you can see, **schema** validated data successfully, opened files and converted ``'3'`` to ``int``. JSON schema ----------- You can also generate standard `draft-07 JSON schema `_ from a dict ``Schema``. This can be used to add word completion, validation, and documentation directly in code editors. The output schema can also be used with JSON schema compatible libraries. JSON: Generating ~~~~~~~~~~~~~~~~ Just define your schema normally and call ``.json_schema()`` on it. The output is a Python dict, you need to dump it to JSON. .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": {Optional("other"): str}, ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json")) # json_schema { "type":"object", "properties": { "test": {"type": "string"}, "nested": { "type":"object", "properties": { "other": {"type": "string"} }, "required": [], "additionalProperties": false } }, "required":[ "test", "nested" ], "additionalProperties":false, "$id":"https://example.com/my-schema.json", "$schema":"http://json-schema.org/draft-07/schema#" } You can add descriptions for the schema elements using the ``Literal`` object instead of a string. The main schema can also have a description. These will appear in IDEs to help your users write a configuration. .. code:: python >>> from schema import Literal, Schema >>> import json >>> s = Schema( ... {Literal("project_name", description="Names must be unique"): str}, ... description="Project schema", ... ) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "project_name": { "description": "Names must be unique", "type": "string" } }, "required": [ "project_name" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "description": "Project schema" } JSON: Supported validations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The resulting JSON schema is not guaranteed to accept the same objects as the library would accept, since some validations are not implemented or have no JSON schema equivalent. This is the case of the ``Use`` and ``Hook`` objects for example. Implemented ''''''''''' `Object properties `_ Use a dict literal. The dict keys are the JSON schema properties. Example: ``Schema({"test": str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': ['test'], 'additionalProperties': False}``. Please note that attributes are required by default. To create optional attributes use ``Optional``, like so: ``Schema({Optional("test"): str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': [], 'additionalProperties': False}`` additionalProperties is set to true when at least one of the conditions is met: - ignore_extra_keys is True - at least one key is `str` or `object` For example: ``Schema({str: str})`` and ``Schema({}, ignore_extra_keys=True)`` both becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': True}`` and ``Schema({})`` becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': False}`` Types Use the Python type name directly. It will be converted to the JSON name: - ``str`` -> `string `_ - ``int`` -> `integer `_ - ``float`` -> `number `_ - ``bool`` -> `boolean `_ - ``list`` -> `array `_ - ``dict`` -> `object `_ Example: ``Schema(float)`` becomes ``{"type": "number"}`` `Array items `_ Surround a schema with ``[]``. Example: ``Schema([str])`` means an array of string and becomes: ``{'type': 'array', 'items': {'type': 'string'}}`` `Enumerated values `_ Use `Or`. Example: ``Schema(Or(1, 2, 3))`` becomes ``{"enum": [1, 2, 3]}`` `Constant values `_ Use the value itself. Example: ``Schema("name")`` becomes ``{"const": "name"}`` `Regular expressions `_ Use ``Regex``. Example: ``Schema(Regex("^v\d+"))`` becomes ``{'type': 'string', 'pattern': '^v\\d+'}`` `Annotations (title and description) `_ You can use the ``name`` and ``description`` parameters of the ``Schema`` object init method. To add description to keys, replace a str with a ``Literal`` object. Example: ``Schema({Literal("test", description="A description"): str})`` is equivalent to ``Schema({"test": str})`` with the description added to the resulting JSON schema. `Combining schemas with allOf `_ Use ``And`` Example: ``Schema(And(str, "value"))`` becomes ``{"allOf": [{"type": "string"}, {"const": "value"}]}`` Note that this example is not really useful in the real world, since ``const`` already implies the type. `Combining schemas with anyOf `_ Use ``Or`` Example: ``Schema(Or(str, int))`` becomes ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` Not implemented ''''''''''''''' The following JSON schema validations cannot be generated from this library. - `String length `_ However, those can be implemented using ``Regex`` - `String format `_ However, those can be implemented using ``Regex`` - `Object dependencies `_ - `Array length `_ - `Array uniqueness `_ - `Numeric multiples `_ - `Numeric ranges `_ - `Property Names `_ Not implemented. We suggest listing the possible keys instead. As a tip, you can use ``Or`` as a dict key. Example: ``Schema({Or("name1", "name2"): str})`` - `Annotations (default and examples) `_ - `Combining schemas with oneOf `_ - `Not `_ - `Object size `_ - `additionalProperties having a different schema (true and false is supported)` JSON: Minimizing output size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Explicit Reuse '''''''''''''' If your JSON schema is big and has a lot of repetition, it can be made simpler and smaller by defining Schema objects as reference. These references will be placed in a "definitions" section in the main schema. `You can look at the JSON schema documentation for more information `_ .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": Schema({Optional("other"): str}, name="nested", as_reference=True) ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "test": { "type": "string" }, "nested": { "$ref": "#/definitions/nested" } }, "required": [ "test", "nested" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "nested": { "type": "object", "properties": { "other": { "type": "string" } }, "required": [], "additionalProperties": false } } } This becomes really useful when using the same object several times .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema( ... {"autocomplete": bool, "stop_words": [str]}, ... name="language", ... as_reference=True, ... ) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "ar": { "$ref": "#/definitions/language" }, "cs": { "$ref": "#/definitions/language" }, "de": { "$ref": "#/definitions/language" }, "el": { "$ref": "#/definitions/language" }, "eu": { "$ref": "#/definitions/language" }, "en": { "$ref": "#/definitions/language" }, "es": { "$ref": "#/definitions/language" }, "fr": { "$ref": "#/definitions/language" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "language": { "type": "object", "properties": { "autocomplete": { "type": "boolean" }, "stop_words": { "type": "array", "items": { "type": "string" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false } } } Automatic reuse ''''''''''''''' If you want to minimize the output size without using names explicitly, you can have the library generate hashes of parts of the output JSON schema and use them as references throughout. Enable this behaviour by providing the parameter ``use_refs`` to the json_schema method. Be aware that this method is less often compatible with IDEs and JSON schema libraries. It produces a JSON schema that is more difficult to read by humans. .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema({"autocomplete": bool, "stop_words": [str]}) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json", use_refs=True), indent=4) # json_schema { "type": "object", "properties": { "ar": { "type": "object", "properties": { "autocomplete": { "type": "boolean", "$id": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "type": "string", "$id": "#1856069563381977338" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "cs": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "$ref": "#1856069563381977338" }, "$id": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "de": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "$ref": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false, "$id": "#-8142886105174600858" }, "el": { "$ref": "#-8142886105174600858" }, "eu": { "$ref": "#-8142886105174600858" }, "en": { "$ref": "#-8142886105174600858" }, "es": { "$ref": "#-8142886105174600858" }, "fr": { "$ref": "#-8142886105174600858" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714819023.0 schema-0.7.7/README.rst0000664000175000017500000010065014615407717014520 0ustar00stavrosstavrosSchema validation just got Pythonic =============================================================================== **schema** is a library for validating Python data structures, such as those obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types. .. image:: https://secure.travis-ci.org/keleshev/schema.svg?branch=master :target: https://travis-ci.org/keleshev/schema .. image:: https://img.shields.io/codecov/c/github/keleshev/schema.svg :target: http://codecov.io/github/keleshev/schema Example ---------------------------------------------------------------------------- Here is a quick example to get a feeling of **schema**, validating a list of entries with personal information: .. code:: python from schema import Schema, And, Use, Optional, SchemaError schema = Schema( [ { "name": And(str, len), "age": And(Use(int), lambda n: 18 <= n <= 99), Optional("gender"): And( str, Use(str.lower), lambda s: s in ("squid", "kid"), ), } ] ) data = [ {"name": "Sue", "age": "28", "gender": "Squid"}, {"name": "Sam", "age": "42"}, {"name": "Sacha", "age": "20", "gender": "KID"}, ] validated = schema.validate(data) assert validated == [ {"name": "Sue", "age": 28, "gender": "squid"}, {"name": "Sam", "age": 42}, {"name": "Sacha", "age": 20, "gender": "kid"}, ] If data is valid, ``Schema.validate`` will return the validated data (optionally converted with `Use` calls, see below). If data is invalid, ``Schema`` will raise ``SchemaError`` exception. If you just want to check that the data is valid, ``schema.is_valid(data)`` will return ``True`` or ``False``. Installation ------------------------------------------------------------------------------- Use `pip `_ or easy_install:: pip install schema Alternatively, you can just drop ``schema.py`` file into your project—it is self-contained. - **schema** is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 and PyPy. - **schema** follows `semantic versioning `_. How ``Schema`` validates data ------------------------------------------------------------------------------- Types ~~~~~ If ``Schema(...)`` encounters a type (such as ``int``, ``str``, ``object``, etc.), it will check if the corresponding piece of data is an instance of that type, otherwise it will raise ``SchemaError``. .. code:: python >>> from schema import Schema >>> Schema(int).validate(123) 123 >>> Schema(int).validate('123') Traceback (most recent call last): ... schema.SchemaUnexpectedTypeError: '123' should be instance of 'int' >>> Schema(object).validate('hai') 'hai' Callables ~~~~~~~~~ If ``Schema(...)`` encounters a callable (function, class, or object with ``__call__`` method) it will call it, and if its return value evaluates to ``True`` it will continue validating, else—it will raise ``SchemaError``. .. code:: python >>> import os >>> Schema(os.path.exists).validate('./') './' >>> Schema(os.path.exists).validate('./non-existent/') Traceback (most recent call last): ... schema.SchemaError: exists('./non-existent/') should evaluate to True >>> Schema(lambda n: n > 0).validate(123) 123 >>> Schema(lambda n: n > 0).validate(-12) Traceback (most recent call last): ... schema.SchemaError: (-12) should evaluate to True "Validatables" ~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an object with method ``validate`` it will run this method on corresponding data as ``data = obj.validate(data)``. This method may raise ``SchemaError`` exception, which will tell ``Schema`` that that piece of data is invalid, otherwise—it will continue validating. An example of "validatable" is ``Regex``, that tries to match a string or a buffer with the given regular expression (itself as a string, buffer or compiled regex ``SRE_Pattern``): .. code:: python >>> from schema import Regex >>> import re >>> Regex(r'^foo').validate('foobar') 'foobar' >>> Regex(r'^[A-Z]+$', flags=re.I).validate('those-dashes-dont-match') Traceback (most recent call last): ... schema.SchemaError: Regex('^[A-Z]+$', flags=re.IGNORECASE) does not match 'those-dashes-dont-match' For a more general case, you can use ``Use`` for creating such objects. ``Use`` helps to use a function or type to convert a value while validating it: .. code:: python >>> from schema import Use >>> Schema(Use(int)).validate('123') 123 >>> Schema(Use(lambda f: open(f, 'a'))).validate('LICENSE-MIT') <_io.TextIOWrapper name='LICENSE-MIT' mode='a' encoding='UTF-8'> Dropping the details, ``Use`` is basically: .. code:: python class Use(object): def __init__(self, callable_): self._callable = callable_ def validate(self, data): try: return self._callable(data) except Exception as e: raise SchemaError('%r raised %r' % (self._callable.__name__, e)) Sometimes you need to transform and validate part of data, but keep original data unchanged. ``Const`` helps to keep your data safe: .. code:: python >> from schema import Use, Const, And, Schema >> from datetime import datetime >> is_future = lambda date: datetime.now() > date >> to_json = lambda v: {"timestamp": v} >> Schema(And(Const(And(Use(datetime.fromtimestamp), is_future)), Use(to_json))).validate(1234567890) {"timestamp": 1234567890} Now you can write your own validation-aware classes and data types. Lists, similar containers ~~~~~~~~~~~~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``list``, ``tuple``, ``set`` or ``frozenset``, it will validate contents of corresponding data container against all schemas listed inside that container and aggregate all errors: .. code:: python >>> Schema([1, 0]).validate([1, 1, 0, 1]) [1, 1, 0, 1] >>> Schema((int, float)).validate((5, 7, 8, 'not int or float here')) Traceback (most recent call last): ... schema.SchemaError: Or(, ) did not validate 'not int or float here' 'not int or float here' should be instance of 'int' 'not int or float here' should be instance of 'float' Dictionaries ~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``dict``, it will validate data key-value pairs: .. code:: python >>> d = Schema( ... {"name": str, "age": lambda n: 18 <= n <= 99} ... ).validate( ... {"name": "Sue", "age": 28} ... ) >>> assert d == {'name': 'Sue', 'age': 28} You can specify keys as schemas too: .. code:: python >>> schema = Schema({ ... str: int, # string keys should have integer values ... int: None, # int keys should be always None ... }) >>> data = schema.validate({ ... "key1": 1, ... "key2": 2, ... 10: None, ... 20: None, ... }) >>> schema.validate({ ... "key1": 1, ... 10: "not None here", ... }) Traceback (most recent call last): ... schema.SchemaError: Key '10' error: None does not match 'not None here' This is useful if you want to check certain key-values, but don't care about others: .. code:: python >>> schema = Schema({ ... "": int, ... "": Use(open), ... str: object, # don't care about other str keys ... }) >>> data = schema.validate({ ... "": 10, ... "": "README.rst", ... "--verbose": True, ... }) You can mark a key as optional as follows: .. code:: python >>> Schema({ ... "name": str, ... Optional("occupation"): str, ... }).validate({"name": "Sam"}) {'name': 'Sam'} ``Optional`` keys can also carry a ``default``, to be used when no key in the data matches: .. code:: python >>> Schema({ ... Optional("color", default="blue"): str, ... str: str, ... }).validate({"texture": "furry"}) == { ... "color": "blue", ... "texture": "furry", ... } True Defaults are used verbatim, not passed through any validators specified in the value. default can also be a callable: .. code:: python >>> from schema import Schema, Optional >>> Schema({Optional('data', default=dict): {}}).validate({}) == {'data': {}} True Also, a caveat: If you specify types, **schema** won't validate the empty dict: .. code:: python >>> Schema({int:int}).is_valid({}) False To do that, you need ``Schema(Or({int:int}, {}))``. This is unlike what happens with lists, where ``Schema([int]).is_valid([])`` will return True. **schema** has classes ``And`` and ``Or`` that help validating several schemas for the same data: .. code:: python >>> from schema import And, Or >>> Schema({'age': And(int, lambda n: 0 < n < 99)}).validate({'age': 7}) {'age': 7} >>> Schema({'password': And(str, lambda s: len(s) > 6)}).validate({'password': 'hai'}) Traceback (most recent call last): ... schema.SchemaError: Key 'password' error: ('hai') should evaluate to True >>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415) 3.1415 In a dictionary, you can also combine two keys in a "one or the other" manner. To do so, use the `Or` class as a key: .. code:: python >>> from schema import Or, Schema >>> schema = Schema({ ... Or("key1", "key2", only_one=True): str ... }) >>> schema.validate({"key1": "test"}) # Ok {'key1': 'test'} >>> schema.validate({"key1": "test", "key2": "test"}) # SchemaError Traceback (most recent call last): ... schema.SchemaOnlyOneAllowedError: There are multiple keys present from the Or('key1', 'key2') condition Hooks ~~~~~~~~~~ You can define hooks which are functions that are executed whenever a valid key:value is found. The `Forbidden` class is an example of this. You can mark a key as forbidden as follows: .. code:: python >>> from schema import Forbidden >>> Schema({Forbidden('age'): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} A few things are worth noting. First, the value paired with the forbidden key determines whether it will be rejected: .. code:: python >>> Schema({Forbidden('age'): str, 'age': int}).validate({'age': 50}) {'age': 50} Note: if we hadn't supplied the 'age' key here, the call would have failed too, but with SchemaWrongKeyError, not SchemaForbiddenKeyError. Second, Forbidden has a higher priority than standard keys, and consequently than Optional. This means we can do that: .. code:: python >>> Schema({Forbidden('age'): object, Optional(str): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} You can also define your own hooks. The following hook will call `_my_function` if `key` is encountered. .. code:: python from schema import Hook def _my_function(key, scope, error): print(key, scope, error) Hook("key", handler=_my_function) Here's an example where a `Deprecated` class is added to log warnings whenever a key is encountered: .. code:: python from schema import Hook, Schema class Deprecated(Hook): def __init__(self, *args, **kwargs): kwargs["handler"] = lambda key, *args: logging.warn(f"`{key}` is deprecated. " + (self._error or "")) super(Deprecated, self).__init__(*args, **kwargs) Schema({Deprecated("test", "custom error message."): object}, ignore_extra_keys=True).validate({"test": "value"}) ... WARNING: `test` is deprecated. custom error message. Extra Keys ~~~~~~~~~~ The ``Schema(...)`` parameter ``ignore_extra_keys`` causes validation to ignore extra keys in a dictionary, and also to not return them after validating. .. code:: python >>> schema = Schema({'name': str}, ignore_extra_keys=True) >>> schema.validate({'name': 'Sam', 'age': '42'}) {'name': 'Sam'} If you would like any extra keys returned, use ``object: object`` as one of the key/value pairs, which will match any key and any value. Otherwise, extra keys will raise a ``SchemaError``. Customized Validation ~~~~~~~~~~~~~~~~~~~~~~~ The ``Schema.validate`` method accepts additional keyword arguments. The keyword arguments will be propagated to the ``validate`` method of any child validatables (including any ad-hoc ``Schema`` objects), or the default value callable (if a callable is specified) for ``Optional`` keys. This feature can be used together with inheritance of the ``Schema`` class for customized validation. Here is an example where a "post-validation" hook that runs after validation against a sub-schema in a larger schema: .. code:: python class EventSchema(schema.Schema): def validate(self, data, _is_event_schema=True): data = super(EventSchema, self).validate(data, _is_event_schema=False) if _is_event_schema and data.get("minimum", None) is None: data["minimum"] = data["capacity"] return data events_schema = schema.Schema( { str: EventSchema({ "capacity": int, schema.Optional("minimum"): int, # default to capacity }) } ) data = {'event1': {'capacity': 1}, 'event2': {'capacity': 2, 'minimum': 3}} events = events_schema.validate(data) assert events['event1']['minimum'] == 1 # == capacity assert events['event2']['minimum'] == 3 Note that the additional keyword argument ``_is_event_schema`` is necessary to limit the customized behavior to the ``EventSchema`` object itself so that it won't affect any recursive invoke of the ``self.__class__.validate`` for the child schemas (e.g., the call to ``Schema("capacity").validate("capacity")``). User-friendly error reporting ------------------------------------------------------------------------------- You can pass a keyword argument ``error`` to any of validatable classes (such as ``Schema``, ``And``, ``Or``, ``Regex``, ``Use``) to report this error instead of a built-in one. .. code:: python >>> Schema(Use(int, error='Invalid year')).validate('XVII') Traceback (most recent call last): ... schema.SchemaError: Invalid year You can see all errors that occurred by accessing exception's ``exc.autos`` for auto-generated error messages, and ``exc.errors`` for errors which had ``error`` text passed to them. You can exit with ``sys.exit(exc.code)`` if you want to show the messages to the user without traceback. ``error`` messages are given precedence in that case. A JSON API example ------------------------------------------------------------------------------- Here is a quick example: validation of `create a gist `_ request from github API. .. code:: python >>> gist = '''{"description": "the description for this gist", ... "public": true, ... "files": { ... "file1.txt": {"content": "String file contents"}, ... "other.txt": {"content": "Another file contents"}}}''' >>> from schema import Schema, And, Use, Optional >>> import json >>> gist_schema = Schema( ... And( ... Use(json.loads), # first convert from JSON ... # use str since json returns unicode ... { ... Optional("description"): str, ... "public": bool, ... "files": {str: {"content": str}}, ... }, ... ) ... ) >>> gist = gist_schema.validate(gist) # gist: {u'description': u'the description for this gist', u'files': {u'file1.txt': {u'content': u'String file contents'}, u'other.txt': {u'content': u'Another file contents'}}, u'public': True} Using **schema** with `docopt `_ ------------------------------------------------------------------------------- Assume you are using **docopt** with the following usage-pattern: Usage: my_program.py [--count=N] ... and you would like to validate that ```` are readable, and that ```` exists, and that ``--count`` is either integer from 0 to 5, or ``None``. Assuming **docopt** returns the following dict: .. code:: python >>> args = { ... "": ["LICENSE-MIT", "setup.py"], ... "": "../", ... "--count": "3", ... } this is how you validate it using ``schema``: .. code:: python >>> from schema import Schema, And, Or, Use >>> import os >>> s = Schema({ ... "": [Use(open)], ... "": os.path.exists, ... "--count": Or(None, And(Use(int), lambda n: 0 < n < 5)), ... }) >>> args = s.validate(args) >>> args[''] [<_io.TextIOWrapper name='LICENSE-MIT' ...>, <_io.TextIOWrapper name='setup.py' ...] >>> args[''] '../' >>> args['--count'] 3 As you can see, **schema** validated data successfully, opened files and converted ``'3'`` to ``int``. JSON schema ----------- You can also generate standard `draft-07 JSON schema `_ from a dict ``Schema``. This can be used to add word completion, validation, and documentation directly in code editors. The output schema can also be used with JSON schema compatible libraries. JSON: Generating ~~~~~~~~~~~~~~~~ Just define your schema normally and call ``.json_schema()`` on it. The output is a Python dict, you need to dump it to JSON. .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": {Optional("other"): str}, ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json")) # json_schema { "type":"object", "properties": { "test": {"type": "string"}, "nested": { "type":"object", "properties": { "other": {"type": "string"} }, "required": [], "additionalProperties": false } }, "required":[ "test", "nested" ], "additionalProperties":false, "$id":"https://example.com/my-schema.json", "$schema":"http://json-schema.org/draft-07/schema#" } You can add descriptions for the schema elements using the ``Literal`` object instead of a string. The main schema can also have a description. These will appear in IDEs to help your users write a configuration. .. code:: python >>> from schema import Literal, Schema >>> import json >>> s = Schema( ... {Literal("project_name", description="Names must be unique"): str}, ... description="Project schema", ... ) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "project_name": { "description": "Names must be unique", "type": "string" } }, "required": [ "project_name" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "description": "Project schema" } JSON: Supported validations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The resulting JSON schema is not guaranteed to accept the same objects as the library would accept, since some validations are not implemented or have no JSON schema equivalent. This is the case of the ``Use`` and ``Hook`` objects for example. Implemented ''''''''''' `Object properties `_ Use a dict literal. The dict keys are the JSON schema properties. Example: ``Schema({"test": str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': ['test'], 'additionalProperties': False}``. Please note that attributes are required by default. To create optional attributes use ``Optional``, like so: ``Schema({Optional("test"): str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': [], 'additionalProperties': False}`` additionalProperties is set to true when at least one of the conditions is met: - ignore_extra_keys is True - at least one key is `str` or `object` For example: ``Schema({str: str})`` and ``Schema({}, ignore_extra_keys=True)`` both becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': True}`` and ``Schema({})`` becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': False}`` Types Use the Python type name directly. It will be converted to the JSON name: - ``str`` -> `string `_ - ``int`` -> `integer `_ - ``float`` -> `number `_ - ``bool`` -> `boolean `_ - ``list`` -> `array `_ - ``dict`` -> `object `_ Example: ``Schema(float)`` becomes ``{"type": "number"}`` `Array items `_ Surround a schema with ``[]``. Example: ``Schema([str])`` means an array of string and becomes: ``{'type': 'array', 'items': {'type': 'string'}}`` `Enumerated values `_ Use `Or`. Example: ``Schema(Or(1, 2, 3))`` becomes ``{"enum": [1, 2, 3]}`` `Constant values `_ Use the value itself. Example: ``Schema("name")`` becomes ``{"const": "name"}`` `Regular expressions `_ Use ``Regex``. Example: ``Schema(Regex("^v\d+"))`` becomes ``{'type': 'string', 'pattern': '^v\\d+'}`` `Annotations (title and description) `_ You can use the ``name`` and ``description`` parameters of the ``Schema`` object init method. To add description to keys, replace a str with a ``Literal`` object. Example: ``Schema({Literal("test", description="A description"): str})`` is equivalent to ``Schema({"test": str})`` with the description added to the resulting JSON schema. `Combining schemas with allOf `_ Use ``And`` Example: ``Schema(And(str, "value"))`` becomes ``{"allOf": [{"type": "string"}, {"const": "value"}]}`` Note that this example is not really useful in the real world, since ``const`` already implies the type. `Combining schemas with anyOf `_ Use ``Or`` Example: ``Schema(Or(str, int))`` becomes ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` Not implemented ''''''''''''''' The following JSON schema validations cannot be generated from this library. - `String length `_ However, those can be implemented using ``Regex`` - `String format `_ However, those can be implemented using ``Regex`` - `Object dependencies `_ - `Array length `_ - `Array uniqueness `_ - `Numeric multiples `_ - `Numeric ranges `_ - `Property Names `_ Not implemented. We suggest listing the possible keys instead. As a tip, you can use ``Or`` as a dict key. Example: ``Schema({Or("name1", "name2"): str})`` - `Annotations (default and examples) `_ - `Combining schemas with oneOf `_ - `Not `_ - `Object size `_ - `additionalProperties having a different schema (true and false is supported)` JSON: Minimizing output size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Explicit Reuse '''''''''''''' If your JSON schema is big and has a lot of repetition, it can be made simpler and smaller by defining Schema objects as reference. These references will be placed in a "definitions" section in the main schema. `You can look at the JSON schema documentation for more information `_ .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": Schema({Optional("other"): str}, name="nested", as_reference=True) ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "test": { "type": "string" }, "nested": { "$ref": "#/definitions/nested" } }, "required": [ "test", "nested" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "nested": { "type": "object", "properties": { "other": { "type": "string" } }, "required": [], "additionalProperties": false } } } This becomes really useful when using the same object several times .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema( ... {"autocomplete": bool, "stop_words": [str]}, ... name="language", ... as_reference=True, ... ) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "ar": { "$ref": "#/definitions/language" }, "cs": { "$ref": "#/definitions/language" }, "de": { "$ref": "#/definitions/language" }, "el": { "$ref": "#/definitions/language" }, "eu": { "$ref": "#/definitions/language" }, "en": { "$ref": "#/definitions/language" }, "es": { "$ref": "#/definitions/language" }, "fr": { "$ref": "#/definitions/language" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "language": { "type": "object", "properties": { "autocomplete": { "type": "boolean" }, "stop_words": { "type": "array", "items": { "type": "string" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false } } } Automatic reuse ''''''''''''''' If you want to minimize the output size without using names explicitly, you can have the library generate hashes of parts of the output JSON schema and use them as references throughout. Enable this behaviour by providing the parameter ``use_refs`` to the json_schema method. Be aware that this method is less often compatible with IDEs and JSON schema libraries. It produces a JSON schema that is more difficult to read by humans. .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema({"autocomplete": bool, "stop_words": [str]}) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json", use_refs=True), indent=4) # json_schema { "type": "object", "properties": { "ar": { "type": "object", "properties": { "autocomplete": { "type": "boolean", "$id": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "type": "string", "$id": "#1856069563381977338" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "cs": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "$ref": "#1856069563381977338" }, "$id": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "de": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "$ref": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false, "$id": "#-8142886105174600858" }, "el": { "$ref": "#-8142886105174600858" }, "eu": { "$ref": "#-8142886105174600858" }, "en": { "$ref": "#-8142886105174600858" }, "es": { "$ref": "#-8142886105174600858" }, "fr": { "$ref": "#-8142886105174600858" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1592394732.0 schema-0.7.7/pyproject.toml0000664000175000017500000000050113672401754015734 0ustar00stavrosstavros[tool.black] line-length = 120 target-version = ['py37'] include = '\.pyi?$' exclude = ''' ( /( \.eggs # exclude a few common directories in the | \.git # root of the project | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ) ''' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714819023.0 schema-0.7.7/requirements.txt0000664000175000017500000000005314615407717016311 0ustar00stavrosstavroscontextlib2>=0.5.5; python_version < "3.3" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714820172.370068 schema-0.7.7/schema.egg-info/0000775000175000017500000000000014615412114015744 5ustar00stavrosstavros././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820172.0 schema-0.7.7/schema.egg-info/PKG-INFO0000644000175000017500000010234114615412114017040 0ustar00stavrosstavrosMetadata-Version: 2.1 Name: schema Version: 0.7.7 Summary: Simple data validation library Home-page: https://github.com/keleshev/schema Author: Vladimir Keleshev Author-email: vladimir@keleshev.com License: MIT Keywords: schema json validation Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Topic :: Utilities Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: MIT License Description-Content-Type: text/x-rst License-File: LICENSE-MIT Schema validation just got Pythonic =============================================================================== **schema** is a library for validating Python data structures, such as those obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types. .. image:: https://secure.travis-ci.org/keleshev/schema.svg?branch=master :target: https://travis-ci.org/keleshev/schema .. image:: https://img.shields.io/codecov/c/github/keleshev/schema.svg :target: http://codecov.io/github/keleshev/schema Example ---------------------------------------------------------------------------- Here is a quick example to get a feeling of **schema**, validating a list of entries with personal information: .. code:: python from schema import Schema, And, Use, Optional, SchemaError schema = Schema( [ { "name": And(str, len), "age": And(Use(int), lambda n: 18 <= n <= 99), Optional("gender"): And( str, Use(str.lower), lambda s: s in ("squid", "kid"), ), } ] ) data = [ {"name": "Sue", "age": "28", "gender": "Squid"}, {"name": "Sam", "age": "42"}, {"name": "Sacha", "age": "20", "gender": "KID"}, ] validated = schema.validate(data) assert validated == [ {"name": "Sue", "age": 28, "gender": "squid"}, {"name": "Sam", "age": 42}, {"name": "Sacha", "age": 20, "gender": "kid"}, ] If data is valid, ``Schema.validate`` will return the validated data (optionally converted with `Use` calls, see below). If data is invalid, ``Schema`` will raise ``SchemaError`` exception. If you just want to check that the data is valid, ``schema.is_valid(data)`` will return ``True`` or ``False``. Installation ------------------------------------------------------------------------------- Use `pip `_ or easy_install:: pip install schema Alternatively, you can just drop ``schema.py`` file into your project—it is self-contained. - **schema** is tested with Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9 and PyPy. - **schema** follows `semantic versioning `_. How ``Schema`` validates data ------------------------------------------------------------------------------- Types ~~~~~ If ``Schema(...)`` encounters a type (such as ``int``, ``str``, ``object``, etc.), it will check if the corresponding piece of data is an instance of that type, otherwise it will raise ``SchemaError``. .. code:: python >>> from schema import Schema >>> Schema(int).validate(123) 123 >>> Schema(int).validate('123') Traceback (most recent call last): ... schema.SchemaUnexpectedTypeError: '123' should be instance of 'int' >>> Schema(object).validate('hai') 'hai' Callables ~~~~~~~~~ If ``Schema(...)`` encounters a callable (function, class, or object with ``__call__`` method) it will call it, and if its return value evaluates to ``True`` it will continue validating, else—it will raise ``SchemaError``. .. code:: python >>> import os >>> Schema(os.path.exists).validate('./') './' >>> Schema(os.path.exists).validate('./non-existent/') Traceback (most recent call last): ... schema.SchemaError: exists('./non-existent/') should evaluate to True >>> Schema(lambda n: n > 0).validate(123) 123 >>> Schema(lambda n: n > 0).validate(-12) Traceback (most recent call last): ... schema.SchemaError: (-12) should evaluate to True "Validatables" ~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an object with method ``validate`` it will run this method on corresponding data as ``data = obj.validate(data)``. This method may raise ``SchemaError`` exception, which will tell ``Schema`` that that piece of data is invalid, otherwise—it will continue validating. An example of "validatable" is ``Regex``, that tries to match a string or a buffer with the given regular expression (itself as a string, buffer or compiled regex ``SRE_Pattern``): .. code:: python >>> from schema import Regex >>> import re >>> Regex(r'^foo').validate('foobar') 'foobar' >>> Regex(r'^[A-Z]+$', flags=re.I).validate('those-dashes-dont-match') Traceback (most recent call last): ... schema.SchemaError: Regex('^[A-Z]+$', flags=re.IGNORECASE) does not match 'those-dashes-dont-match' For a more general case, you can use ``Use`` for creating such objects. ``Use`` helps to use a function or type to convert a value while validating it: .. code:: python >>> from schema import Use >>> Schema(Use(int)).validate('123') 123 >>> Schema(Use(lambda f: open(f, 'a'))).validate('LICENSE-MIT') <_io.TextIOWrapper name='LICENSE-MIT' mode='a' encoding='UTF-8'> Dropping the details, ``Use`` is basically: .. code:: python class Use(object): def __init__(self, callable_): self._callable = callable_ def validate(self, data): try: return self._callable(data) except Exception as e: raise SchemaError('%r raised %r' % (self._callable.__name__, e)) Sometimes you need to transform and validate part of data, but keep original data unchanged. ``Const`` helps to keep your data safe: .. code:: python >> from schema import Use, Const, And, Schema >> from datetime import datetime >> is_future = lambda date: datetime.now() > date >> to_json = lambda v: {"timestamp": v} >> Schema(And(Const(And(Use(datetime.fromtimestamp), is_future)), Use(to_json))).validate(1234567890) {"timestamp": 1234567890} Now you can write your own validation-aware classes and data types. Lists, similar containers ~~~~~~~~~~~~~~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``list``, ``tuple``, ``set`` or ``frozenset``, it will validate contents of corresponding data container against all schemas listed inside that container and aggregate all errors: .. code:: python >>> Schema([1, 0]).validate([1, 1, 0, 1]) [1, 1, 0, 1] >>> Schema((int, float)).validate((5, 7, 8, 'not int or float here')) Traceback (most recent call last): ... schema.SchemaError: Or(, ) did not validate 'not int or float here' 'not int or float here' should be instance of 'int' 'not int or float here' should be instance of 'float' Dictionaries ~~~~~~~~~~~~ If ``Schema(...)`` encounters an instance of ``dict``, it will validate data key-value pairs: .. code:: python >>> d = Schema( ... {"name": str, "age": lambda n: 18 <= n <= 99} ... ).validate( ... {"name": "Sue", "age": 28} ... ) >>> assert d == {'name': 'Sue', 'age': 28} You can specify keys as schemas too: .. code:: python >>> schema = Schema({ ... str: int, # string keys should have integer values ... int: None, # int keys should be always None ... }) >>> data = schema.validate({ ... "key1": 1, ... "key2": 2, ... 10: None, ... 20: None, ... }) >>> schema.validate({ ... "key1": 1, ... 10: "not None here", ... }) Traceback (most recent call last): ... schema.SchemaError: Key '10' error: None does not match 'not None here' This is useful if you want to check certain key-values, but don't care about others: .. code:: python >>> schema = Schema({ ... "": int, ... "": Use(open), ... str: object, # don't care about other str keys ... }) >>> data = schema.validate({ ... "": 10, ... "": "README.rst", ... "--verbose": True, ... }) You can mark a key as optional as follows: .. code:: python >>> Schema({ ... "name": str, ... Optional("occupation"): str, ... }).validate({"name": "Sam"}) {'name': 'Sam'} ``Optional`` keys can also carry a ``default``, to be used when no key in the data matches: .. code:: python >>> Schema({ ... Optional("color", default="blue"): str, ... str: str, ... }).validate({"texture": "furry"}) == { ... "color": "blue", ... "texture": "furry", ... } True Defaults are used verbatim, not passed through any validators specified in the value. default can also be a callable: .. code:: python >>> from schema import Schema, Optional >>> Schema({Optional('data', default=dict): {}}).validate({}) == {'data': {}} True Also, a caveat: If you specify types, **schema** won't validate the empty dict: .. code:: python >>> Schema({int:int}).is_valid({}) False To do that, you need ``Schema(Or({int:int}, {}))``. This is unlike what happens with lists, where ``Schema([int]).is_valid([])`` will return True. **schema** has classes ``And`` and ``Or`` that help validating several schemas for the same data: .. code:: python >>> from schema import And, Or >>> Schema({'age': And(int, lambda n: 0 < n < 99)}).validate({'age': 7}) {'age': 7} >>> Schema({'password': And(str, lambda s: len(s) > 6)}).validate({'password': 'hai'}) Traceback (most recent call last): ... schema.SchemaError: Key 'password' error: ('hai') should evaluate to True >>> Schema(And(Or(int, float), lambda x: x > 0)).validate(3.1415) 3.1415 In a dictionary, you can also combine two keys in a "one or the other" manner. To do so, use the `Or` class as a key: .. code:: python >>> from schema import Or, Schema >>> schema = Schema({ ... Or("key1", "key2", only_one=True): str ... }) >>> schema.validate({"key1": "test"}) # Ok {'key1': 'test'} >>> schema.validate({"key1": "test", "key2": "test"}) # SchemaError Traceback (most recent call last): ... schema.SchemaOnlyOneAllowedError: There are multiple keys present from the Or('key1', 'key2') condition Hooks ~~~~~~~~~~ You can define hooks which are functions that are executed whenever a valid key:value is found. The `Forbidden` class is an example of this. You can mark a key as forbidden as follows: .. code:: python >>> from schema import Forbidden >>> Schema({Forbidden('age'): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} A few things are worth noting. First, the value paired with the forbidden key determines whether it will be rejected: .. code:: python >>> Schema({Forbidden('age'): str, 'age': int}).validate({'age': 50}) {'age': 50} Note: if we hadn't supplied the 'age' key here, the call would have failed too, but with SchemaWrongKeyError, not SchemaForbiddenKeyError. Second, Forbidden has a higher priority than standard keys, and consequently than Optional. This means we can do that: .. code:: python >>> Schema({Forbidden('age'): object, Optional(str): object}).validate({'age': 50}) Traceback (most recent call last): ... schema.SchemaForbiddenKeyError: Forbidden key encountered: 'age' in {'age': 50} You can also define your own hooks. The following hook will call `_my_function` if `key` is encountered. .. code:: python from schema import Hook def _my_function(key, scope, error): print(key, scope, error) Hook("key", handler=_my_function) Here's an example where a `Deprecated` class is added to log warnings whenever a key is encountered: .. code:: python from schema import Hook, Schema class Deprecated(Hook): def __init__(self, *args, **kwargs): kwargs["handler"] = lambda key, *args: logging.warn(f"`{key}` is deprecated. " + (self._error or "")) super(Deprecated, self).__init__(*args, **kwargs) Schema({Deprecated("test", "custom error message."): object}, ignore_extra_keys=True).validate({"test": "value"}) ... WARNING: `test` is deprecated. custom error message. Extra Keys ~~~~~~~~~~ The ``Schema(...)`` parameter ``ignore_extra_keys`` causes validation to ignore extra keys in a dictionary, and also to not return them after validating. .. code:: python >>> schema = Schema({'name': str}, ignore_extra_keys=True) >>> schema.validate({'name': 'Sam', 'age': '42'}) {'name': 'Sam'} If you would like any extra keys returned, use ``object: object`` as one of the key/value pairs, which will match any key and any value. Otherwise, extra keys will raise a ``SchemaError``. Customized Validation ~~~~~~~~~~~~~~~~~~~~~~~ The ``Schema.validate`` method accepts additional keyword arguments. The keyword arguments will be propagated to the ``validate`` method of any child validatables (including any ad-hoc ``Schema`` objects), or the default value callable (if a callable is specified) for ``Optional`` keys. This feature can be used together with inheritance of the ``Schema`` class for customized validation. Here is an example where a "post-validation" hook that runs after validation against a sub-schema in a larger schema: .. code:: python class EventSchema(schema.Schema): def validate(self, data, _is_event_schema=True): data = super(EventSchema, self).validate(data, _is_event_schema=False) if _is_event_schema and data.get("minimum", None) is None: data["minimum"] = data["capacity"] return data events_schema = schema.Schema( { str: EventSchema({ "capacity": int, schema.Optional("minimum"): int, # default to capacity }) } ) data = {'event1': {'capacity': 1}, 'event2': {'capacity': 2, 'minimum': 3}} events = events_schema.validate(data) assert events['event1']['minimum'] == 1 # == capacity assert events['event2']['minimum'] == 3 Note that the additional keyword argument ``_is_event_schema`` is necessary to limit the customized behavior to the ``EventSchema`` object itself so that it won't affect any recursive invoke of the ``self.__class__.validate`` for the child schemas (e.g., the call to ``Schema("capacity").validate("capacity")``). User-friendly error reporting ------------------------------------------------------------------------------- You can pass a keyword argument ``error`` to any of validatable classes (such as ``Schema``, ``And``, ``Or``, ``Regex``, ``Use``) to report this error instead of a built-in one. .. code:: python >>> Schema(Use(int, error='Invalid year')).validate('XVII') Traceback (most recent call last): ... schema.SchemaError: Invalid year You can see all errors that occurred by accessing exception's ``exc.autos`` for auto-generated error messages, and ``exc.errors`` for errors which had ``error`` text passed to them. You can exit with ``sys.exit(exc.code)`` if you want to show the messages to the user without traceback. ``error`` messages are given precedence in that case. A JSON API example ------------------------------------------------------------------------------- Here is a quick example: validation of `create a gist `_ request from github API. .. code:: python >>> gist = '''{"description": "the description for this gist", ... "public": true, ... "files": { ... "file1.txt": {"content": "String file contents"}, ... "other.txt": {"content": "Another file contents"}}}''' >>> from schema import Schema, And, Use, Optional >>> import json >>> gist_schema = Schema( ... And( ... Use(json.loads), # first convert from JSON ... # use str since json returns unicode ... { ... Optional("description"): str, ... "public": bool, ... "files": {str: {"content": str}}, ... }, ... ) ... ) >>> gist = gist_schema.validate(gist) # gist: {u'description': u'the description for this gist', u'files': {u'file1.txt': {u'content': u'String file contents'}, u'other.txt': {u'content': u'Another file contents'}}, u'public': True} Using **schema** with `docopt `_ ------------------------------------------------------------------------------- Assume you are using **docopt** with the following usage-pattern: Usage: my_program.py [--count=N] ... and you would like to validate that ```` are readable, and that ```` exists, and that ``--count`` is either integer from 0 to 5, or ``None``. Assuming **docopt** returns the following dict: .. code:: python >>> args = { ... "": ["LICENSE-MIT", "setup.py"], ... "": "../", ... "--count": "3", ... } this is how you validate it using ``schema``: .. code:: python >>> from schema import Schema, And, Or, Use >>> import os >>> s = Schema({ ... "": [Use(open)], ... "": os.path.exists, ... "--count": Or(None, And(Use(int), lambda n: 0 < n < 5)), ... }) >>> args = s.validate(args) >>> args[''] [<_io.TextIOWrapper name='LICENSE-MIT' ...>, <_io.TextIOWrapper name='setup.py' ...] >>> args[''] '../' >>> args['--count'] 3 As you can see, **schema** validated data successfully, opened files and converted ``'3'`` to ``int``. JSON schema ----------- You can also generate standard `draft-07 JSON schema `_ from a dict ``Schema``. This can be used to add word completion, validation, and documentation directly in code editors. The output schema can also be used with JSON schema compatible libraries. JSON: Generating ~~~~~~~~~~~~~~~~ Just define your schema normally and call ``.json_schema()`` on it. The output is a Python dict, you need to dump it to JSON. .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": {Optional("other"): str}, ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json")) # json_schema { "type":"object", "properties": { "test": {"type": "string"}, "nested": { "type":"object", "properties": { "other": {"type": "string"} }, "required": [], "additionalProperties": false } }, "required":[ "test", "nested" ], "additionalProperties":false, "$id":"https://example.com/my-schema.json", "$schema":"http://json-schema.org/draft-07/schema#" } You can add descriptions for the schema elements using the ``Literal`` object instead of a string. The main schema can also have a description. These will appear in IDEs to help your users write a configuration. .. code:: python >>> from schema import Literal, Schema >>> import json >>> s = Schema( ... {Literal("project_name", description="Names must be unique"): str}, ... description="Project schema", ... ) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "project_name": { "description": "Names must be unique", "type": "string" } }, "required": [ "project_name" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "description": "Project schema" } JSON: Supported validations ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The resulting JSON schema is not guaranteed to accept the same objects as the library would accept, since some validations are not implemented or have no JSON schema equivalent. This is the case of the ``Use`` and ``Hook`` objects for example. Implemented ''''''''''' `Object properties `_ Use a dict literal. The dict keys are the JSON schema properties. Example: ``Schema({"test": str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': ['test'], 'additionalProperties': False}``. Please note that attributes are required by default. To create optional attributes use ``Optional``, like so: ``Schema({Optional("test"): str})`` becomes ``{'type': 'object', 'properties': {'test': {'type': 'string'}}, 'required': [], 'additionalProperties': False}`` additionalProperties is set to true when at least one of the conditions is met: - ignore_extra_keys is True - at least one key is `str` or `object` For example: ``Schema({str: str})`` and ``Schema({}, ignore_extra_keys=True)`` both becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': True}`` and ``Schema({})`` becomes ``{'type': 'object', 'properties' : {}, 'required': [], 'additionalProperties': False}`` Types Use the Python type name directly. It will be converted to the JSON name: - ``str`` -> `string `_ - ``int`` -> `integer `_ - ``float`` -> `number `_ - ``bool`` -> `boolean `_ - ``list`` -> `array `_ - ``dict`` -> `object `_ Example: ``Schema(float)`` becomes ``{"type": "number"}`` `Array items `_ Surround a schema with ``[]``. Example: ``Schema([str])`` means an array of string and becomes: ``{'type': 'array', 'items': {'type': 'string'}}`` `Enumerated values `_ Use `Or`. Example: ``Schema(Or(1, 2, 3))`` becomes ``{"enum": [1, 2, 3]}`` `Constant values `_ Use the value itself. Example: ``Schema("name")`` becomes ``{"const": "name"}`` `Regular expressions `_ Use ``Regex``. Example: ``Schema(Regex("^v\d+"))`` becomes ``{'type': 'string', 'pattern': '^v\\d+'}`` `Annotations (title and description) `_ You can use the ``name`` and ``description`` parameters of the ``Schema`` object init method. To add description to keys, replace a str with a ``Literal`` object. Example: ``Schema({Literal("test", description="A description"): str})`` is equivalent to ``Schema({"test": str})`` with the description added to the resulting JSON schema. `Combining schemas with allOf `_ Use ``And`` Example: ``Schema(And(str, "value"))`` becomes ``{"allOf": [{"type": "string"}, {"const": "value"}]}`` Note that this example is not really useful in the real world, since ``const`` already implies the type. `Combining schemas with anyOf `_ Use ``Or`` Example: ``Schema(Or(str, int))`` becomes ``{"anyOf": [{"type": "string"}, {"type": "integer"}]}`` Not implemented ''''''''''''''' The following JSON schema validations cannot be generated from this library. - `String length `_ However, those can be implemented using ``Regex`` - `String format `_ However, those can be implemented using ``Regex`` - `Object dependencies `_ - `Array length `_ - `Array uniqueness `_ - `Numeric multiples `_ - `Numeric ranges `_ - `Property Names `_ Not implemented. We suggest listing the possible keys instead. As a tip, you can use ``Or`` as a dict key. Example: ``Schema({Or("name1", "name2"): str})`` - `Annotations (default and examples) `_ - `Combining schemas with oneOf `_ - `Not `_ - `Object size `_ - `additionalProperties having a different schema (true and false is supported)` JSON: Minimizing output size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Explicit Reuse '''''''''''''' If your JSON schema is big and has a lot of repetition, it can be made simpler and smaller by defining Schema objects as reference. These references will be placed in a "definitions" section in the main schema. `You can look at the JSON schema documentation for more information `_ .. code:: python >>> from schema import Optional, Schema >>> import json >>> s = Schema({ ... "test": str, ... "nested": Schema({Optional("other"): str}, name="nested", as_reference=True) ... }) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "test": { "type": "string" }, "nested": { "$ref": "#/definitions/nested" } }, "required": [ "test", "nested" ], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "nested": { "type": "object", "properties": { "other": { "type": "string" } }, "required": [], "additionalProperties": false } } } This becomes really useful when using the same object several times .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema( ... {"autocomplete": bool, "stop_words": [str]}, ... name="language", ... as_reference=True, ... ) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json"), indent=4) # json_schema { "type": "object", "properties": { "ar": { "$ref": "#/definitions/language" }, "cs": { "$ref": "#/definitions/language" }, "de": { "$ref": "#/definitions/language" }, "el": { "$ref": "#/definitions/language" }, "eu": { "$ref": "#/definitions/language" }, "en": { "$ref": "#/definitions/language" }, "es": { "$ref": "#/definitions/language" }, "fr": { "$ref": "#/definitions/language" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "language": { "type": "object", "properties": { "autocomplete": { "type": "boolean" }, "stop_words": { "type": "array", "items": { "type": "string" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false } } } Automatic reuse ''''''''''''''' If you want to minimize the output size without using names explicitly, you can have the library generate hashes of parts of the output JSON schema and use them as references throughout. Enable this behaviour by providing the parameter ``use_refs`` to the json_schema method. Be aware that this method is less often compatible with IDEs and JSON schema libraries. It produces a JSON schema that is more difficult to read by humans. .. code:: python >>> from schema import Optional, Or, Schema >>> import json >>> language_configuration = Schema({"autocomplete": bool, "stop_words": [str]}) >>> s = Schema({Or("ar", "cs", "de", "el", "eu", "en", "es", "fr"): language_configuration}) >>> json_schema = json.dumps(s.json_schema("https://example.com/my-schema.json", use_refs=True), indent=4) # json_schema { "type": "object", "properties": { "ar": { "type": "object", "properties": { "autocomplete": { "type": "boolean", "$id": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "type": "string", "$id": "#1856069563381977338" } } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "cs": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "type": "array", "items": { "$ref": "#1856069563381977338" }, "$id": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false }, "de": { "type": "object", "properties": { "autocomplete": { "$ref": "#6456104181059880193" }, "stop_words": { "$ref": "#-5377945144312515805" } }, "required": [ "autocomplete", "stop_words" ], "additionalProperties": false, "$id": "#-8142886105174600858" }, "el": { "$ref": "#-8142886105174600858" }, "eu": { "$ref": "#-8142886105174600858" }, "en": { "$ref": "#-8142886105174600858" }, "es": { "$ref": "#-8142886105174600858" }, "fr": { "$ref": "#-8142886105174600858" } }, "required": [], "additionalProperties": false, "$id": "https://example.com/my-schema.json", "$schema": "http://json-schema.org/draft-07/schema#" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820172.0 schema-0.7.7/schema.egg-info/SOURCES.txt0000664000175000017500000000040314615412114017625 0ustar00stavrosstavrosLICENSE-MIT MANIFEST.in README.rst pyproject.toml requirements.txt schema.py setup.cfg setup.py test_schema.py schema.egg-info/PKG-INFO schema.egg-info/SOURCES.txt schema.egg-info/dependency_links.txt schema.egg-info/requires.txt schema.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820172.0 schema-0.7.7/schema.egg-info/dependency_links.txt0000664000175000017500000000000114615412114022012 0ustar00stavrosstavros ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820172.0 schema-0.7.7/schema.egg-info/requires.txt0000664000175000017500000000005614615412114020345 0ustar00stavrosstavros [:python_version < "3.3"] contextlib2>=0.5.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820172.0 schema-0.7.7/schema.egg-info/top_level.txt0000664000175000017500000000000714615412114020473 0ustar00stavrosstavrosschema ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714820087.0 schema-0.7.7/schema.py0000664000175000017500000010445614615411767014653 0ustar00stavrosstavros"""schema is a library for validating Python data structures, such as those obtained from config-files, forms, external services or command-line parsing, converted from JSON/YAML (or something else) to Python data-types.""" import inspect import re from typing import Any from typing import Callable from typing import cast from typing import Dict from typing import Generic from typing import Iterable from typing import List from typing import NoReturn from typing import Sequence from typing import Set from typing import Sized from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union # Use TYPE_CHECKING to determine the correct type hint but avoid runtime import errors if TYPE_CHECKING: # Only for type checking purposes, we import the standard ExitStack from contextlib import ExitStack else: try: from contextlib import ExitStack # Python 3.3 and later except ImportError: from contextlib2 import ExitStack # Python 2.x/3.0-3.2 fallback __version__ = "0.7.7" __all__ = [ "Schema", "And", "Or", "Regex", "Optional", "Use", "Forbidden", "Const", "Literal", "SchemaError", "SchemaWrongKeyError", "SchemaMissingKeyError", "SchemaForbiddenKeyError", "SchemaUnexpectedTypeError", "SchemaOnlyOneAllowedError", ] class SchemaError(Exception): """Error during Schema validation.""" def __init__( self, autos: Union[Sequence[Union[str, None]], None], errors: Union[List, str, None] = None, ): self.autos = autos if isinstance(autos, List) else [autos] self.errors = errors if isinstance(errors, List) else [errors] Exception.__init__(self, self.code) @property def code(self) -> str: """Remove duplicates in autos and errors list and combine them into a single message.""" def uniq(seq: Iterable[Union[str, None]]) -> List[str]: """Utility function to remove duplicates while preserving the order.""" seen: Set[str] = set() unique_list: List[str] = [] for x in seq: if x is not None and x not in seen: seen.add(x) unique_list.append(x) return unique_list data_set = uniq(self.autos) error_list = uniq(self.errors) return "\n".join(error_list if error_list else data_set) class SchemaWrongKeyError(SchemaError): """Error Should be raised when an unexpected key is detected within the data set being.""" pass class SchemaMissingKeyError(SchemaError): """Error should be raised when a mandatory key is not found within the data set being validated""" pass class SchemaOnlyOneAllowedError(SchemaError): """Error should be raised when an only_one Or key has multiple matching candidates""" pass class SchemaForbiddenKeyError(SchemaError): """Error should be raised when a forbidden key is found within the data set being validated, and its value matches the value that was specified""" pass class SchemaUnexpectedTypeError(SchemaError): """Error should be raised when a type mismatch is detected within the data set being validated.""" pass # Type variable to represent a Schema-like type TSchema = TypeVar("TSchema", bound="Schema") class And(Generic[TSchema]): """ Utility function to combine validation directives in AND Boolean fashion. """ def __init__( self, *args: Union[TSchema, Callable[..., Any]], error: Union[str, None] = None, ignore_extra_keys: bool = False, schema: Union[Type[TSchema], None] = None, ) -> None: self._args: Tuple[Union[TSchema, Callable[..., Any]], ...] = args self._error: Union[str, None] = error self._ignore_extra_keys: bool = ignore_extra_keys self._schema_class: Type[TSchema] = schema if schema is not None else Schema def __repr__(self) -> str: return f"{self.__class__.__name__}({', '.join(repr(a) for a in self._args)})" @property def args(self) -> Tuple[Union[TSchema, Callable[..., Any]], ...]: """The provided parameters""" return self._args def validate(self, data: Any, **kwargs: Any) -> Any: """ Validate data using defined sub schema/expressions ensuring all values are valid. :param data: Data to be validated with sub defined schemas. :return: Returns validated data. """ # Annotate sub_schema with the type returned by _build_schema for sub_schema in self._build_schemas(): # type: TSchema data = sub_schema.validate(data, **kwargs) return data def _build_schemas(self) -> List[TSchema]: return [self._build_schema(s) for s in self._args] def _build_schema(self, arg: Any) -> TSchema: # Assume self._schema_class(arg, ...) returns an instance of TSchema return self._schema_class( arg, error=self._error, ignore_extra_keys=self._ignore_extra_keys ) class Or(And[TSchema]): """Utility function to combine validation directives in a OR Boolean fashion. If one wants to make an xor, one can provide only_one=True optional argument to the constructor of this object. When a validation was performed for an xor-ish Or instance and one wants to use it another time, one needs to call reset() to put the match_count back to 0.""" def __init__( self, *args: Union[TSchema, Callable[..., Any]], only_one: bool = False, **kwargs: Any, ) -> None: self.only_one: bool = only_one self.match_count: int = 0 super().__init__(*args, **kwargs) def reset(self) -> None: failed: bool = self.match_count > 1 and self.only_one self.match_count = 0 if failed: raise SchemaOnlyOneAllowedError( ["There are multiple keys present from the %r condition" % self] ) def validate(self, data: Any, **kwargs: Any) -> Any: """ Validate data using sub defined schema/expressions ensuring at least one value is valid. :param data: data to be validated by provided schema. :return: return validated data if not validation """ autos: List[str] = [] errors: List[Union[str, None]] = [] for sub_schema in self._build_schemas(): try: validation: Any = sub_schema.validate(data, **kwargs) self.match_count += 1 if self.match_count > 1 and self.only_one: break return validation except SchemaError as _x: autos += _x.autos errors += _x.errors raise SchemaError( ["%r did not validate %r" % (self, data)] + autos, [self._error.format(data) if self._error else None] + errors, ) class Regex: """ Enables schema.py to validate string using regular expressions. """ # Map all flags bits to a more readable description NAMES = [ "re.ASCII", "re.DEBUG", "re.VERBOSE", "re.UNICODE", "re.DOTALL", "re.MULTILINE", "re.LOCALE", "re.IGNORECASE", "re.TEMPLATE", ] def __init__( self, pattern_str: str, flags: int = 0, error: Union[str, None] = None ) -> None: self._pattern_str: str = pattern_str flags_list = [ Regex.NAMES[i] for i, f in enumerate(f"{flags:09b}") if f != "0" ] # Name for each bit self._flags_names: str = ", flags=" + "|".join(flags_list) if flags_list else "" self._pattern: re.Pattern = re.compile(pattern_str, flags=flags) self._error: Union[str, None] = error def __repr__(self) -> str: return f"{self.__class__.__name__}({self._pattern_str!r}{self._flags_names})" @property def pattern_str(self) -> str: """The pattern string for the represented regular expression""" return self._pattern_str def validate(self, data: str, **kwargs: Any) -> str: """ Validates data using the defined regex. :param data: Data to be validated. :return: Returns validated data. """ e = self._error try: if self._pattern.search(data): return data else: error_message = ( e.format(data) if e else f"{data!r} does not match {self._pattern_str!r}" ) raise SchemaError(error_message) except TypeError: error_message = ( e.format(data) if e else f"{data!r} is not string nor buffer" ) raise SchemaError(error_message) class Use: """ For more general use cases, you can use the Use class to transform the data while it is being validated. """ def __init__( self, callable_: Callable[[Any], Any], error: Union[str, None] = None ) -> None: if not callable(callable_): raise TypeError(f"Expected a callable, not {callable_!r}") self._callable: Callable[[Any], Any] = callable_ self._error: Union[str, None] = error def __repr__(self) -> str: return f"{self.__class__.__name__}({self._callable!r})" def validate(self, data: Any, **kwargs: Any) -> Any: try: return self._callable(data) except SchemaError as x: raise SchemaError( [None] + x.autos, [self._error.format(data) if self._error else None] + x.errors, ) except BaseException as x: f = _callable_str(self._callable) raise SchemaError( "%s(%r) raised %r" % (f, data, x), self._error.format(data) if self._error else None, ) COMPARABLE, CALLABLE, VALIDATOR, TYPE, DICT, ITERABLE = range(6) def _priority(s: Any) -> int: """Return priority for a given object.""" if type(s) in (list, tuple, set, frozenset): return ITERABLE if isinstance(s, dict): return DICT if issubclass(type(s), type): return TYPE if isinstance(s, Literal): return COMPARABLE if hasattr(s, "validate"): return VALIDATOR if callable(s): return CALLABLE else: return COMPARABLE def _invoke_with_optional_kwargs(f: Callable[..., Any], **kwargs: Any) -> Any: s = inspect.signature(f) if len(s.parameters) == 0: return f() return f(**kwargs) class Schema(object): """ Entry point of the library, use this class to instantiate validation schema for the data that will be validated. """ def __init__( self, schema: Any, error: Union[str, None] = None, ignore_extra_keys: bool = False, name: Union[str, None] = None, description: Union[str, None] = None, as_reference: bool = False, ) -> None: self._schema: Any = schema self._error: Union[str, None] = error self._ignore_extra_keys: bool = ignore_extra_keys self._name: Union[str, None] = name self._description: Union[str, None] = description self.as_reference: bool = as_reference if as_reference and name is None: raise ValueError("Schema used as reference should have a name") def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self._schema) @property def schema(self) -> Any: return self._schema @property def description(self) -> Union[str, None]: return self._description @property def name(self) -> Union[str, None]: return self._name @property def ignore_extra_keys(self) -> bool: return self._ignore_extra_keys @staticmethod def _dict_key_priority(s) -> float: """Return priority for a given key object.""" if isinstance(s, Hook): return _priority(s._schema) - 0.5 if isinstance(s, Optional): return _priority(s._schema) + 0.5 return _priority(s) @staticmethod def _is_optional_type(s: Any) -> bool: """Return True if the given key is optional (does not have to be found)""" return any(isinstance(s, optional_type) for optional_type in [Optional, Hook]) def is_valid(self, data: Any, **kwargs: Dict[str, Any]) -> bool: """Return whether the given data has passed all the validations that were specified in the given schema. """ try: self.validate(data, **kwargs) except SchemaError: return False else: return True def _prepend_schema_name(self, message: str) -> str: """ If a custom schema name has been defined, prepends it to the error message that gets raised when a schema error occurs. """ if self._name: message = "{0!r} {1!s}".format(self._name, message) return message def validate(self, data: Any, **kwargs: Dict[str, Any]) -> Any: Schema = self.__class__ s: Any = self._schema e: Union[str, None] = self._error i: bool = self._ignore_extra_keys if isinstance(s, Literal): s = s.schema flavor = _priority(s) if flavor == ITERABLE: data = Schema(type(s), error=e).validate(data, **kwargs) o: Or = Or(*s, error=e, schema=Schema, ignore_extra_keys=i) return type(data)(o.validate(d, **kwargs) for d in data) if flavor == DICT: exitstack = ExitStack() data = Schema(dict, error=e).validate(data, **kwargs) new: Dict = type(data)() # new - is a dict of the validated values coverage: Set = set() # matched schema keys # for each key and value find a schema entry matching them, if any sorted_skeys = sorted(s, key=self._dict_key_priority) for skey in sorted_skeys: if hasattr(skey, "reset"): exitstack.callback(skey.reset) with exitstack: # Evaluate dictionaries last data_items = sorted( data.items(), key=lambda value: isinstance(value[1], dict) ) for key, value in data_items: for skey in sorted_skeys: svalue = s[skey] try: nkey = Schema(skey, error=e).validate(key, **kwargs) except SchemaError: pass else: if isinstance(skey, Hook): # As the content of the value makes little sense for # keys with a hook, we reverse its meaning: # we will only call the handler if the value does match # In the case of the forbidden key hook, # we will raise the SchemaErrorForbiddenKey exception # on match, allowing for excluding a key only if its # value has a certain type, and allowing Forbidden to # work well in combination with Optional. try: nvalue = Schema(svalue, error=e).validate( value, **kwargs ) except SchemaError: continue skey.handler(nkey, data, e) else: try: nvalue = Schema( svalue, error=e, ignore_extra_keys=i ).validate(value, **kwargs) except SchemaError as x: k = "Key '%s' error:" % nkey message = self._prepend_schema_name(k) raise SchemaError( [message] + x.autos, [e.format(data) if e else None] + x.errors, ) else: new[nkey] = nvalue coverage.add(skey) break required = set(k for k in s if not self._is_optional_type(k)) if not required.issubset(coverage): missing_keys = required - coverage s_missing_keys = ", ".join( repr(k) for k in sorted(missing_keys, key=repr) ) message = "Missing key%s: %s" % ( _plural_s(missing_keys), s_missing_keys, ) message = self._prepend_schema_name(message) raise SchemaMissingKeyError(message, e.format(data) if e else None) if not self._ignore_extra_keys and (len(new) != len(data)): wrong_keys = set(data.keys()) - set(new.keys()) s_wrong_keys = ", ".join(repr(k) for k in sorted(wrong_keys, key=repr)) message = "Wrong key%s %s in %r" % ( _plural_s(wrong_keys), s_wrong_keys, data, ) message = self._prepend_schema_name(message) raise SchemaWrongKeyError(message, e.format(data) if e else None) # Apply default-having optionals that haven't been used: defaults = ( set(k for k in s if isinstance(k, Optional) and hasattr(k, "default")) - coverage ) for default in defaults: new[default.key] = ( _invoke_with_optional_kwargs(default.default, **kwargs) if callable(default.default) else default.default ) return new if flavor == TYPE: if isinstance(data, s) and not (isinstance(data, bool) and s == int): return data else: message = "%r should be instance of %r" % (data, s.__name__) message = self._prepend_schema_name(message) raise SchemaUnexpectedTypeError(message, e.format(data) if e else None) if flavor == VALIDATOR: try: return s.validate(data, **kwargs) except SchemaError as x: raise SchemaError( [None] + x.autos, [e.format(data) if e else None] + x.errors ) except BaseException as x: message = "%r.validate(%r) raised %r" % (s, data, x) message = self._prepend_schema_name(message) raise SchemaError(message, e.format(data) if e else None) if flavor == CALLABLE: f = _callable_str(s) try: if s(data): return data except SchemaError as x: raise SchemaError( [None] + x.autos, [e.format(data) if e else None] + x.errors ) except BaseException as x: message = "%s(%r) raised %r" % (f, data, x) message = self._prepend_schema_name(message) raise SchemaError(message, e.format(data) if e else None) message = "%s(%r) should evaluate to True" % (f, data) message = self._prepend_schema_name(message) raise SchemaError(message, e.format(data) if e else None) if s == data: return data else: message = "%r does not match %r" % (s, data) message = self._prepend_schema_name(message) raise SchemaError(message, e.format(data) if e else None) def json_schema( self, schema_id: str, use_refs: bool = False, **kwargs: Any ) -> Dict[str, Any]: """Generate a draft-07 JSON schema dict representing the Schema. This method must be called with a schema_id. :param schema_id: The value of the $id on the main schema :param use_refs: Enable reusing object references in the resulting JSON schema. Schemas with references are harder to read by humans, but are a lot smaller when there is a lot of reuse """ seen: Dict[int, Dict[str, Any]] = {} definitions_by_name: Dict[str, Dict[str, Any]] = {} def _json_schema( schema: "Schema", is_main_schema: bool = True, description: Union[str, None] = None, allow_reference: bool = True, ) -> Dict[str, Any]: def _create_or_use_ref(return_dict: Dict[str, Any]) -> Dict[str, Any]: """If not already seen, return the provided part of the schema unchanged. If already seen, give an id to the already seen dict and return a reference to the previous part of the schema instead. """ if not use_refs or is_main_schema: return return_schema hashed = hash(repr(sorted(return_dict.items()))) if hashed not in seen: seen[hashed] = return_dict return return_dict else: id_str = "#" + str(hashed) seen[hashed]["$id"] = id_str return {"$ref": id_str} def _get_type_name(python_type: Type) -> str: """Return the JSON schema name for a Python type""" if python_type == str: return "string" elif python_type == int: return "integer" elif python_type == float: return "number" elif python_type == bool: return "boolean" elif python_type == list: return "array" elif python_type == dict: return "object" return "string" def _to_json_type(value: Any) -> Any: """Attempt to convert a constant value (for "const" and "default") to a JSON serializable value""" if value is None or type(value) in (str, int, float, bool, list, dict): return value if type(value) in (tuple, set, frozenset): return list(value) if isinstance(value, Literal): return value.schema return str(value) def _to_schema(s: Any, ignore_extra_keys: bool) -> Schema: if not isinstance(s, Schema): return Schema(s, ignore_extra_keys=ignore_extra_keys) return s s: Any = schema.schema i: bool = schema.ignore_extra_keys flavor = _priority(s) return_schema: Dict[str, Any] = {} return_description: Union[str, None] = description or schema.description if return_description: return_schema["description"] = return_description # Check if we have to create a common definition and use as reference if allow_reference and schema.as_reference: # Generate sub schema if not already done if schema.name not in definitions_by_name: definitions_by_name[ cast(str, schema.name) ] = {} # Avoid infinite loop definitions_by_name[cast(str, schema.name)] = _json_schema( schema, is_main_schema=False, allow_reference=False ) return_schema["$ref"] = "#/definitions/" + cast(str, schema.name) else: if flavor == TYPE: # Handle type return_schema["type"] = _get_type_name(s) elif flavor == ITERABLE: # Handle arrays or dict schema return_schema["type"] = "array" if len(s) == 1: return_schema["items"] = _json_schema( _to_schema(s[0], i), is_main_schema=False ) elif len(s) > 1: return_schema["items"] = _json_schema( Schema(Or(*s)), is_main_schema=False ) elif isinstance(s, Or): # Handle Or values # Check if we can use an enum if all( priority == COMPARABLE for priority in [_priority(value) for value in s.args] ): or_values = [ str(s) if isinstance(s, Literal) else s for s in s.args ] # All values are simple, can use enum or const if len(or_values) == 1: return_schema["const"] = _to_json_type(or_values[0]) return return_schema return_schema["enum"] = or_values else: # No enum, let's go with recursive calls any_of_values = [] for or_key in s.args: new_value = _json_schema( _to_schema(or_key, i), is_main_schema=False ) if new_value != {} and new_value not in any_of_values: any_of_values.append(new_value) if len(any_of_values) == 1: # Only one representable condition remains, do not put under anyOf return_schema.update(any_of_values[0]) else: return_schema["anyOf"] = any_of_values elif isinstance(s, And): # Handle And values all_of_values = [] for and_key in s.args: new_value = _json_schema( _to_schema(and_key, i), is_main_schema=False ) if new_value != {} and new_value not in all_of_values: all_of_values.append(new_value) if len(all_of_values) == 1: # Only one representable condition remains, do not put under allOf return_schema.update(all_of_values[0]) else: return_schema["allOf"] = all_of_values elif flavor == COMPARABLE: return_schema["const"] = _to_json_type(s) elif flavor == VALIDATOR and type(s) == Regex: return_schema["type"] = "string" return_schema["pattern"] = s.pattern_str else: if flavor != DICT: # If not handled, do not check return return_schema # Schema is a dict required_keys = [] expanded_schema = {} additional_properties = i for key in s: if isinstance(key, Hook): continue def _key_allows_additional_properties(key: Any) -> bool: """Check if a key is broad enough to allow additional properties""" if isinstance(key, Optional): return _key_allows_additional_properties(key.schema) return key == str or key == object def _get_key_description(key: Any) -> Union[str, None]: """Get the description associated to a key (as specified in a Literal object). Return None if not a Literal""" if isinstance(key, Optional): return _get_key_description(key.schema) if isinstance(key, Literal): return key.description return None def _get_key_name(key: Any) -> Any: """Get the name of a key (as specified in a Literal object). Return the key unchanged if not a Literal""" if isinstance(key, Optional): return _get_key_name(key.schema) if isinstance(key, Literal): return key.schema return key additional_properties = ( additional_properties or _key_allows_additional_properties(key) ) sub_schema = _to_schema(s[key], ignore_extra_keys=i) key_name = _get_key_name(key) if isinstance(key_name, str): if not isinstance(key, Optional): required_keys.append(key_name) expanded_schema[key_name] = _json_schema( sub_schema, is_main_schema=False, description=_get_key_description(key), ) if isinstance(key, Optional) and hasattr(key, "default"): expanded_schema[key_name]["default"] = _to_json_type( _invoke_with_optional_kwargs(key.default, **kwargs) if callable(key.default) else key.default ) elif isinstance(key_name, Or): # JSON schema does not support having a key named one name or another, so we just add both options # This is less strict because we cannot enforce that one or the other is required for or_key in key_name.args: expanded_schema[_get_key_name(or_key)] = _json_schema( sub_schema, is_main_schema=False, description=_get_key_description(or_key), ) return_schema.update( { "type": "object", "properties": expanded_schema, "required": required_keys, "additionalProperties": additional_properties, } ) if is_main_schema: return_schema.update( { "$id": schema_id, "$schema": "http://json-schema.org/draft-07/schema#", } ) if self._name: return_schema["title"] = self._name if definitions_by_name: return_schema["definitions"] = {} for definition_name, definition in definitions_by_name.items(): return_schema["definitions"][definition_name] = definition return _create_or_use_ref(return_schema) return _json_schema(self, True) class Optional(Schema): """Marker for an optional part of the validation Schema.""" _MARKER = object() def __init__(self, *args: Any, **kwargs: Any) -> None: default: Any = kwargs.pop("default", self._MARKER) super(Optional, self).__init__(*args, **kwargs) if default is not self._MARKER: if _priority(self._schema) != COMPARABLE: raise TypeError( "Optional keys with defaults must have simple, " "predictable values, like literal strings or ints. " f'"{self._schema!r}" is too complex.' ) self.default = default self.key = str(self._schema) def __hash__(self) -> int: return hash(self._schema) def __eq__(self, other: Any) -> bool: return ( self.__class__ is other.__class__ and getattr(self, "default", self._MARKER) == getattr(other, "default", self._MARKER) and self._schema == other._schema ) def reset(self) -> None: if hasattr(self._schema, "reset"): self._schema.reset() class Hook(Schema): def __init__(self, *args: Any, **kwargs: Any) -> None: self.handler: Callable[..., Any] = kwargs.pop("handler", lambda *args: None) super(Hook, self).__init__(*args, **kwargs) self.key = self._schema class Forbidden(Hook): def __init__(self, *args: Any, **kwargs: Any) -> None: kwargs["handler"] = self._default_function super(Forbidden, self).__init__(*args, **kwargs) @staticmethod def _default_function(nkey: Any, data: Any, error: Any) -> NoReturn: raise SchemaForbiddenKeyError( f"Forbidden key encountered: {nkey!r} in {data!r}", error ) class Literal: def __init__(self, value: Any, description: Union[str, None] = None) -> None: self._schema: Any = value self._description: Union[str, None] = description def __str__(self) -> str: return str(self._schema) def __repr__(self) -> str: return f'Literal("{self._schema}", description="{self._description or ""}")' @property def description(self) -> Union[str, None]: return self._description @property def schema(self) -> Any: return self._schema class Const(Schema): def validate(self, data: Any, **kwargs: Any) -> Any: super(Const, self).validate(data, **kwargs) return data def _callable_str(callable_: Callable[..., Any]) -> str: if hasattr(callable_, "__name__"): return callable_.__name__ return str(callable_) def _plural_s(sized: Sized) -> str: return "s" if len(sized) > 1 else "" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1714820172.370068 schema-0.7.7/setup.cfg0000664000175000017500000000057214615412114014637 0ustar00stavrosstavros[wheel] universal = 1 [semantic_release] version_variable = schema.py:__version__ [flake8] exclude = wsgi.py,env/*,*/migrations/*,venv/*,local_settings.py,doc/*,webpush/*,*.html,setup.cfg ignore = E501,W503 [isort] include_trailing_comma = true line_length = 120 force_grid_wrap = 0 multi_line_output = 3 skip = migrations,node_modules [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714819647.0 schema-0.7.7/setup.py0000664000175000017500000000266514615411077014544 0ustar00stavrosstavrosimport codecs import sys from setuptools import setup version_file = "schema.py" with open(version_file) as f: for line in f.read().split("\n"): if line.startswith("__version__ ="): version = eval(line.split("=", 1)[1]) break else: print("No __version__ attribute found in %r" % version_file) sys.exit(1) setup( name="schema", version=version, author="Vladimir Keleshev", author_email="vladimir@keleshev.com", description="Simple data validation library", license="MIT", keywords="schema json validation", url="https://github.com/keleshev/schema", py_modules=["schema"], package_data={"": ["py.typed"]}, # Include py.typed file include_package_data=True, long_description=codecs.open("README.rst", "r", "utf-8").read(), long_description_content_type="text/x-rst", install_requires=open("requirements.txt", "r").read().split("\n"), classifiers=[ "Development Status :: 3 - Alpha", "Topic :: Utilities", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: PyPy", "License :: OSI Approved :: MIT License", ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714819374.0 schema-0.7.7/test_schema.py0000664000175000017500000017035314615410456015703 0ustar00stavrosstavrosfrom __future__ import with_statement import copy import json import os import platform import re import sys from collections import defaultdict from collections import namedtuple from functools import partial from operator import methodcaller try: from unittest.mock import Mock except ImportError: from mock import Mock from pytest import mark, raises from schema import ( And, Const, Forbidden, Hook, Literal, Optional, Or, Regex, Schema, SchemaError, SchemaForbiddenKeyError, SchemaMissingKeyError, SchemaUnexpectedTypeError, SchemaWrongKeyError, Use, ) if sys.version_info[0] == 3: basestring = str # Python 3 does not have basestring unicode = str # Python 3 does not have unicode SE = raises(SchemaError) def ve(_): raise ValueError() def se(_): raise SchemaError("first auto", "first error") def sorted_dict(to_sort): """Helper function to sort list of string inside dictionaries in order to compare them""" if isinstance(to_sort, dict): new_dict = {} for k in sorted(to_sort.keys()): new_dict[k] = sorted_dict(to_sort[k]) return new_dict if isinstance(to_sort, list) and to_sort: if isinstance(to_sort[0], str): return sorted(to_sort) else: return [sorted_dict(element) for element in to_sort] return to_sort def test_schema(): assert Schema(1).validate(1) == 1 with SE: Schema(1).validate(9) assert Schema(int).validate(1) == 1 with SE: Schema(int).validate("1") assert Schema(Use(int)).validate("1") == 1 with SE: Schema(int).validate(int) with SE: Schema(int).validate(True) with SE: Schema(int).validate(False) assert Schema(str).validate("hai") == "hai" with SE: Schema(str).validate(1) assert Schema(Use(str)).validate(1) == "1" assert Schema(list).validate(["a", 1]) == ["a", 1] assert Schema(dict).validate({"a": 1}) == {"a": 1} with SE: Schema(dict).validate(["a", 1]) assert Schema(lambda n: 0 < n < 5).validate(3) == 3 with SE: Schema(lambda n: 0 < n < 5).validate(-1) def test_validate_file(): assert Schema(Use(open)).validate("LICENSE-MIT").read().startswith("Copyright") with SE: Schema(Use(open)).validate("NON-EXISTENT") assert Schema(os.path.exists).validate(".") == "." with SE: Schema(os.path.exists).validate("./non-existent/") assert Schema(os.path.isfile).validate("LICENSE-MIT") == "LICENSE-MIT" with SE: Schema(os.path.isfile).validate("NON-EXISTENT") def test_and(): assert And(int, lambda n: 0 < n < 5).validate(3) == 3 with SE: And(int, lambda n: 0 < n < 5).validate(3.33) assert And(Use(int), lambda n: 0 < n < 5).validate(3.33) == 3 with SE: And(Use(int), lambda n: 0 < n < 5).validate("3.33") def test_or(): assert Or(int, dict).validate(5) == 5 assert Or(int, dict).validate({}) == {} with SE: Or(int, dict).validate("hai") assert Or(int).validate(4) with SE: Or().validate(2) def test_or_only_one(): or_rule = Or("test1", "test2", only_one=True) schema = Schema( {or_rule: str, Optional("sub_schema"): {Optional(copy.deepcopy(or_rule)): str}} ) assert schema.validate({"test1": "value"}) assert schema.validate({"test1": "value", "sub_schema": {"test2": "value"}}) assert schema.validate({"test2": "other_value"}) with SE: schema.validate({"test1": "value", "test2": "other_value"}) with SE: schema.validate( {"test1": "value", "sub_schema": {"test1": "value", "test2": "value"}} ) with SE: schema.validate({"othertest": "value"}) extra_keys_schema = Schema({or_rule: str}, ignore_extra_keys=True) assert extra_keys_schema.validate({"test1": "value", "other-key": "value"}) assert extra_keys_schema.validate({"test2": "other_value"}) with SE: extra_keys_schema.validate({"test1": "value", "test2": "other_value"}) def test_test(): def unique_list(_list): return len(_list) == len(set(_list)) def dict_keys(key, _list): return list(map(lambda d: d[key], _list)) schema = Schema(Const(And(Use(partial(dict_keys, "index")), unique_list))) data = [{"index": 1, "value": "foo"}, {"index": 2, "value": "bar"}] assert schema.validate(data) == data bad_data = [{"index": 1, "value": "foo"}, {"index": 1, "value": "bar"}] with SE: schema.validate(bad_data) def test_regex(): # Simple case: validate string assert Regex(r"foo").validate("afoot") == "afoot" with SE: Regex(r"bar").validate("afoot") # More complex case: validate string assert Regex(r"^[a-z]+$").validate("letters") == "letters" with SE: Regex(r"^[a-z]+$").validate("letters + spaces") == "letters + spaces" # Validate dict key assert Schema({Regex(r"^foo"): str}).validate({"fookey": "value"}) == { "fookey": "value" } with SE: Schema({Regex(r"^foo"): str}).validate({"barkey": "value"}) # Validate dict value assert Schema({str: Regex(r"^foo")}).validate({"key": "foovalue"}) == { "key": "foovalue" } with SE: Schema({str: Regex(r"^foo")}).validate({"key": "barvalue"}) # Error if the value does not have a buffer interface with SE: Regex(r"bar").validate(1) with SE: Regex(r"bar").validate({}) with SE: Regex(r"bar").validate([]) with SE: Regex(r"bar").validate(None) # Validate that the pattern has a buffer interface assert Regex(re.compile(r"foo")).validate("foo") == "foo" assert Regex(unicode("foo")).validate("foo") == "foo" with raises(TypeError): Regex(1).validate("bar") with raises(TypeError): Regex({}).validate("bar") with raises(TypeError): Regex([]).validate("bar") with raises(TypeError): Regex(None).validate("bar") def test_validate_list(): assert Schema([1, 0]).validate([1, 0, 1, 1]) == [1, 0, 1, 1] assert Schema([1, 0]).validate([]) == [] with SE: Schema([1, 0]).validate(0) with SE: Schema([1, 0]).validate([2]) assert And([1, 0], lambda lst: len(lst) > 2).validate([0, 1, 0]) == [0, 1, 0] with SE: And([1, 0], lambda lst: len(lst) > 2).validate([0, 1]) def test_list_tuple_set_frozenset(): assert Schema([int]).validate([1, 2]) with SE: Schema([int]).validate(["1", 2]) assert Schema(set([int])).validate(set([1, 2])) == set([1, 2]) with SE: Schema(set([int])).validate([1, 2]) # not a set with SE: Schema(set([int])).validate(["1", 2]) assert Schema(tuple([int])).validate(tuple([1, 2])) == tuple([1, 2]) with SE: Schema(tuple([int])).validate([1, 2]) # not a set def test_strictly(): assert Schema(int).validate(1) == 1 with SE: Schema(int).validate("1") def test_dict(): assert Schema({"key": 5}).validate({"key": 5}) == {"key": 5} with SE: Schema({"key": 5}).validate({"key": "x"}) with SE: Schema({"key": 5}).validate(["key", 5]) assert Schema({"key": int}).validate({"key": 5}) == {"key": 5} assert Schema({"n": int, "f": float}).validate({"n": 5, "f": 3.14}) == { "n": 5, "f": 3.14, } with SE: Schema({"n": int, "f": float}).validate({"n": 3.14, "f": 5}) with SE: try: Schema({}).validate({"abc": None, 1: None}) except SchemaWrongKeyError as e: assert e.args[0].startswith("Wrong keys 'abc', 1 in") raise with SE: try: Schema({"key": 5}).validate({}) except SchemaMissingKeyError as e: assert e.args[0] == "Missing key: 'key'" raise with SE: try: Schema({"key": 5}).validate({"n": 5}) except SchemaMissingKeyError as e: assert e.args[0] == "Missing key: 'key'" raise with SE: try: Schema({"key": 5, "key2": 5}).validate({"n": 5}) except SchemaMissingKeyError as e: assert e.args[0] == "Missing keys: 'key', 'key2'" raise with SE: try: Schema({}).validate({"n": 5}) except SchemaWrongKeyError as e: assert e.args[0] == "Wrong key 'n' in {'n': 5}" raise with SE: try: Schema({"key": 5}).validate({"key": 5, "bad": 5}) except SchemaWrongKeyError as e: assert e.args[0] in [ "Wrong key 'bad' in {'key': 5, 'bad': 5}", "Wrong key 'bad' in {'bad': 5, 'key': 5}", ] raise with SE: try: Schema({}).validate({"a": 5, "b": 5}) except SchemaError as e: assert e.args[0] in [ "Wrong keys 'a', 'b' in {'a': 5, 'b': 5}", "Wrong keys 'a', 'b' in {'b': 5, 'a': 5}", ] raise with SE: try: Schema({int: int}).validate({"": ""}) except SchemaUnexpectedTypeError as e: assert e.args[0] in ["'' should be instance of 'int'"] def test_dict_keys(): assert Schema({str: int}).validate({"a": 1, "b": 2}) == {"a": 1, "b": 2} with SE: Schema({str: int}).validate({1: 1, "b": 2}) assert Schema({Use(str): Use(int)}).validate({1: 3.14, 3.14: 1}) == { "1": 3, "3.14": 1, } def test_ignore_extra_keys(): assert Schema({"key": 5}, ignore_extra_keys=True).validate( {"key": 5, "bad": 4} ) == {"key": 5} assert Schema({"key": 5, "dk": {"a": "a"}}, ignore_extra_keys=True).validate( {"key": 5, "bad": "b", "dk": {"a": "a", "bad": "b"}} ) == {"key": 5, "dk": {"a": "a"}} assert Schema([{"key": "v"}], ignore_extra_keys=True).validate( [{"key": "v", "bad": "bad"}] ) == [{"key": "v"}] assert Schema([{"key": "v"}], ignore_extra_keys=True).validate( [{"key": "v", "bad": "bad"}] ) == [{"key": "v"}] def test_ignore_extra_keys_validation_and_return_keys(): assert Schema({"key": 5, object: object}, ignore_extra_keys=True).validate( {"key": 5, "bad": 4} ) == { "key": 5, "bad": 4, } assert Schema( {"key": 5, "dk": {"a": "a", object: object}}, ignore_extra_keys=True ).validate({"key": 5, "dk": {"a": "a", "bad": "b"}}) == { "key": 5, "dk": {"a": "a", "bad": "b"}, } def test_dict_forbidden_keys(): with raises(SchemaForbiddenKeyError): Schema({Forbidden("b"): object}).validate({"b": "bye"}) with raises(SchemaWrongKeyError): Schema({Forbidden("b"): int}).validate({"b": "bye"}) assert Schema({Forbidden("b"): int, Optional("b"): object}).validate( {"b": "bye"} ) == {"b": "bye"} with raises(SchemaForbiddenKeyError): Schema({Forbidden("b"): object, Optional("b"): object}).validate({"b": "bye"}) def test_dict_hook(): function_mock = Mock(return_value=None) hook = Hook("b", handler=function_mock) assert Schema({hook: str, Optional("b"): object}).validate({"b": "bye"}) == { "b": "bye" } function_mock.assert_called_once() assert Schema({hook: int, Optional("b"): object}).validate({"b": "bye"}) == { "b": "bye" } function_mock.assert_called_once() assert Schema({hook: str, "b": object}).validate({"b": "bye"}) == {"b": "bye"} assert function_mock.call_count == 2 def test_dict_optional_keys(): with SE: Schema({"a": 1, "b": 2}).validate({"a": 1}) assert Schema({"a": 1, Optional("b"): 2}).validate({"a": 1}) == {"a": 1} assert Schema({"a": 1, Optional("b"): 2}).validate({"a": 1, "b": 2}) == { "a": 1, "b": 2, } # Make sure Optionals are favored over types: assert Schema({basestring: 1, Optional("b"): 2}).validate({"a": 1, "b": 2}) == { "a": 1, "b": 2, } # Make sure Optionals hash based on their key: assert len({Optional("a"): 1, Optional("a"): 1, Optional("b"): 2}) == 2 def test_dict_optional_defaults(): # Optionals fill out their defaults: assert Schema( {Optional("a", default=1): 11, Optional("b", default=2): 22} ).validate({"a": 11}) == {"a": 11, "b": 2} # Optionals take precedence over types. Here, the "a" is served by the # Optional: assert Schema({Optional("a", default=1): 11, basestring: 22}).validate( {"b": 22} ) == {"a": 1, "b": 22} with raises(TypeError): Optional(And(str, Use(int)), default=7) def test_dict_subtypes(): d = defaultdict(int, key=1) v = Schema({"key": 1}).validate(d) assert v == d assert isinstance(v, defaultdict) # Please add tests for Counter and OrderedDict once support for Python2.6 # is dropped! def test_dict_key_error(): try: Schema({"k": int}).validate({"k": "x"}) except SchemaError as e: assert e.code == "Key 'k' error:\n'x' should be instance of 'int'" try: Schema({"k": {"k2": int}}).validate({"k": {"k2": "x"}}) except SchemaError as e: code = "Key 'k' error:\nKey 'k2' error:\n'x' should be instance of 'int'" assert e.code == code try: Schema({"k": {"k2": int}}, error="k2 should be int").validate( {"k": {"k2": "x"}} ) except SchemaError as e: assert e.code == "k2 should be int" def test_complex(): s = Schema( { "": And([Use(open)], lambda lst: len(lst)), "": os.path.exists, Optional("--count"): And(int, lambda n: 0 <= n <= 5), } ) data = s.validate({"": ["./LICENSE-MIT"], "": "./"}) assert len(data) == 2 assert len(data[""]) == 1 assert data[""][0].read().startswith("Copyright") assert data[""] == "./" def test_nice_errors(): try: Schema(int, error="should be integer").validate("x") except SchemaError as e: assert e.errors == ["should be integer"] try: Schema(Use(float), error="should be a number").validate("x") except SchemaError as e: assert e.code == "should be a number" try: Schema({Optional("i"): Use(int, error="should be a number")}).validate( {"i": "x"} ) except SchemaError as e: assert e.code == "should be a number" def test_use_error_handling(): try: Use(ve).validate("x") except SchemaError as e: assert e.autos == ["ve('x') raised ValueError()"] assert e.errors == [None] try: Use(ve, error="should not raise").validate("x") except SchemaError as e: assert e.autos == ["ve('x') raised ValueError()"] assert e.errors == ["should not raise"] try: Use(se).validate("x") except SchemaError as e: assert e.autos == [None, "first auto"] assert e.errors == [None, "first error"] try: Use(se, error="second error").validate("x") except SchemaError as e: assert e.autos == [None, "first auto"] assert e.errors == ["second error", "first error"] def test_or_error_handling(): try: Or(ve).validate("x") except SchemaError as e: assert e.autos[0].startswith("Or(") assert e.autos[0].endswith(") did not validate 'x'") assert e.autos[1] == "ve('x') raised ValueError()" assert len(e.autos) == 2 assert e.errors == [None, None] try: Or(ve, error="should not raise").validate("x") except SchemaError as e: assert e.autos[0].startswith("Or(") assert e.autos[0].endswith(") did not validate 'x'") assert e.autos[1] == "ve('x') raised ValueError()" assert len(e.autos) == 2 assert e.errors == ["should not raise", "should not raise"] try: Or("o").validate("x") except SchemaError as e: assert e.autos == ["Or('o') did not validate 'x'", "'o' does not match 'x'"] assert e.errors == [None, None] try: Or("o", error="second error").validate("x") except SchemaError as e: assert e.autos == ["Or('o') did not validate 'x'", "'o' does not match 'x'"] assert e.errors == ["second error", "second error"] def test_and_error_handling(): try: And(ve).validate("x") except SchemaError as e: assert e.autos == ["ve('x') raised ValueError()"] assert e.errors == [None] try: And(ve, error="should not raise").validate("x") except SchemaError as e: assert e.autos == ["ve('x') raised ValueError()"] assert e.errors == ["should not raise"] try: And(str, se).validate("x") except SchemaError as e: assert e.autos == [None, "first auto"] assert e.errors == [None, "first error"] try: And(str, se, error="second error").validate("x") except SchemaError as e: assert e.autos == [None, "first auto"] assert e.errors == ["second error", "first error"] def test_schema_error_handling(): try: Schema(Use(ve)).validate("x") except SchemaError as e: assert e.autos == [None, "ve('x') raised ValueError()"] assert e.errors == [None, None] try: Schema(Use(ve), error="should not raise").validate("x") except SchemaError as e: assert e.autos == [None, "ve('x') raised ValueError()"] assert e.errors == ["should not raise", None] try: Schema(Use(se)).validate("x") except SchemaError as e: assert e.autos == [None, None, "first auto"] assert e.errors == [None, None, "first error"] try: Schema(Use(se), error="second error").validate("x") except SchemaError as e: assert e.autos == [None, None, "first auto"] assert e.errors == ["second error", None, "first error"] def test_use_json(): import json gist_schema = Schema( And( Use(json.loads), # first convert from JSON { Optional("description"): basestring, "public": bool, "files": {basestring: {"content": basestring}}, }, ) ) gist = """{"description": "the description for this gist", "public": true, "files": { "file1.txt": {"content": "String file contents"}, "other.txt": {"content": "Another file contents"}}}""" assert gist_schema.validate(gist) def test_error_reporting(): s = Schema( { "": [Use(open, error=" should be readable")], "": And(os.path.exists, error=" should exist"), "--count": Or( None, And(Use(int), lambda n: 0 < n < 5), error="--count should be integer 0 < n < 5", ), }, error="Error:", ) s.validate({"": [], "": "./", "--count": 3}) try: s.validate({"": [], "": "./", "--count": "10"}) except SchemaError as e: assert e.code == "Error:\n--count should be integer 0 < n < 5" try: s.validate({"": [], "": "./hai", "--count": "2"}) except SchemaError as e: assert e.code == "Error:\n should exist" try: s.validate({"": ["hai"], "": "./", "--count": "2"}) except SchemaError as e: assert e.code == "Error:\n should be readable" def test_schema_repr(): # what about repr with `error`s? schema = Schema([Or(None, And(str, Use(float)))]) repr_ = "Schema([Or(None, And(, Use()))])" # in Python 3 repr contains , not assert repr(schema).replace("class", "type") == repr_ def test_validate_object(): schema = Schema({object: str}) assert schema.validate({42: "str"}) == {42: "str"} with SE: schema.validate({42: 777}) def test_issue_9_prioritized_key_comparison(): validate = Schema({"key": 42, object: 42}).validate assert validate({"key": 42, 777: 42}) == {"key": 42, 777: 42} def test_issue_9_prioritized_key_comparison_in_dicts(): # http://stackoverflow.com/questions/14588098/docopt-schema-validation s = Schema( { "ID": Use(int, error="ID should be an int"), "FILE": Or(None, Use(open, error="FILE should be readable")), Optional(str): object, } ) data = {"ID": 10, "FILE": None, "other": "other", "other2": "other2"} assert s.validate(data) == data data = {"ID": 10, "FILE": None} assert s.validate(data) == data def test_missing_keys_exception_with_non_str_dict_keys(): s = Schema({And(str, Use(str.lower), "name"): And(str, len)}) with SE: s.validate(dict()) with SE: try: Schema({1: "x"}).validate(dict()) except SchemaMissingKeyError as e: assert e.args[0] == "Missing key: 1" raise # PyPy does have a __name__ attribute for its callables. @mark.skipif(platform.python_implementation() == "PyPy", reason="Running on PyPy") def test_issue_56_cant_rely_on_callables_to_have_name(): s = Schema(methodcaller("endswith", ".csv")) assert s.validate("test.csv") == "test.csv" with SE: try: s.validate("test.py") except SchemaError as e: assert "operator.methodcaller" in e.args[0] raise def test_exception_handling_with_bad_validators(): BadValidator = namedtuple("BadValidator", ["validate"]) s = Schema(BadValidator("haha")) with SE: try: s.validate("test") except SchemaError as e: assert "TypeError" in e.args[0] raise def test_issue_83_iterable_validation_return_type(): TestSetType = type("TestSetType", (set,), dict()) data = TestSetType(["test", "strings"]) s = Schema(set([str])) assert isinstance(s.validate(data), TestSetType) def test_optional_key_convert_failed_randomly_while_with_another_optional_object(): """ In this test, created_at string "2015-10-10 00:00:00" is expected to be converted to a datetime instance. - it works when the schema is s = Schema({ 'created_at': _datetime_validator, Optional(basestring): object, }) - but when wrapping the key 'created_at' with Optional, it fails randomly :return: """ import datetime fmt = "%Y-%m-%d %H:%M:%S" _datetime_validator = Or(None, Use(lambda i: datetime.datetime.strptime(i, fmt))) # FIXME given tests enough for i in range(1024): s = Schema( { Optional("created_at"): _datetime_validator, Optional("updated_at"): _datetime_validator, Optional("birth"): _datetime_validator, Optional(basestring): object, } ) data = {"created_at": "2015-10-10 00:00:00"} validated_data = s.validate(data) # is expected to be converted to a datetime instance, but fails randomly # (most of the time) assert isinstance(validated_data["created_at"], datetime.datetime) # assert isinstance(validated_data['created_at'], basestring) def test_copy(): s1 = SchemaError("a", None) s2 = copy.deepcopy(s1) assert s1 is not s2 assert type(s1) is type(s2) def test_inheritance(): def convert(data): if isinstance(data, int): return data + 1 return data class MySchema(Schema): def validate(self, data): return super(MySchema, self).validate(convert(data)) s = {"k": int, "d": {"k": int, "l": [{"l": [int]}]}} v = {"k": 1, "d": {"k": 2, "l": [{"l": [3, 4, 5]}]}} d = MySchema(s).validate(v) assert d["k"] == 2 and d["d"]["k"] == 3 and d["d"]["l"][0]["l"] == [4, 5, 6] def test_inheritance_validate_kwargs(): def convert(data, increment): if isinstance(data, int): return data + increment return data class MySchema(Schema): def validate(self, data, increment=1): return super(MySchema, self).validate( convert(data, increment), increment=increment ) s = {"k": int, "d": {"k": int, "l": [{"l": [int]}]}} v = {"k": 1, "d": {"k": 2, "l": [{"l": [3, 4, 5]}]}} d = MySchema(s).validate(v, increment=1) assert d["k"] == 2 and d["d"]["k"] == 3 and d["d"]["l"][0]["l"] == [4, 5, 6] d = MySchema(s).validate(v, increment=10) assert d["k"] == 11 and d["d"]["k"] == 12 and d["d"]["l"][0]["l"] == [13, 14, 15] def test_inheritance_validate_kwargs_passed_to_nested_schema(): def convert(data, increment): if isinstance(data, int): return data + increment return data class MySchema(Schema): def validate(self, data, increment=1): return super(MySchema, self).validate( convert(data, increment), increment=increment ) # note only d.k is under MySchema, and all others are under Schema without # increment s = {"k": int, "d": MySchema({"k": int, "l": [Schema({"l": [int]})]})} v = {"k": 1, "d": {"k": 2, "l": [{"l": [3, 4, 5]}]}} d = Schema(s).validate(v, increment=1) assert d["k"] == 1 and d["d"]["k"] == 3 and d["d"]["l"][0]["l"] == [3, 4, 5] d = Schema(s).validate(v, increment=10) assert d["k"] == 1 and d["d"]["k"] == 12 and d["d"]["l"][0]["l"] == [3, 4, 5] def test_optional_callable_default_get_inherited_schema_validate_kwargs(): def convert(data, increment): if isinstance(data, int): return data + increment return data s = { "k": int, "d": { Optional("k", default=lambda **kw: convert(2, kw["increment"])): int, "l": [{"l": [int]}], }, } v = {"k": 1, "d": {"l": [{"l": [3, 4, 5]}]}} d = Schema(s).validate(v, increment=1) assert d["k"] == 1 and d["d"]["k"] == 3 and d["d"]["l"][0]["l"] == [3, 4, 5] d = Schema(s).validate(v, increment=10) assert d["k"] == 1 and d["d"]["k"] == 12 and d["d"]["l"][0]["l"] == [3, 4, 5] def test_optional_callable_default_ignore_inherited_schema_validate_kwargs(): def convert(data, increment): if isinstance(data, int): return data + increment return data s = {"k": int, "d": {Optional("k", default=lambda: 42): int, "l": [{"l": [int]}]}} v = {"k": 1, "d": {"l": [{"l": [3, 4, 5]}]}} d = Schema(s).validate(v, increment=1) assert d["k"] == 1 and d["d"]["k"] == 42 and d["d"]["l"][0]["l"] == [3, 4, 5] d = Schema(s).validate(v, increment=10) assert d["k"] == 1 and d["d"]["k"] == 42 and d["d"]["l"][0]["l"] == [3, 4, 5] def test_inheritance_optional(): def convert(data, increment): if isinstance(data, int): return data + increment return data class MyOptional(Optional): """This overrides the default property so it increments according to kwargs passed to validate() """ @property def default(self): def wrapper(**kwargs): if "increment" in kwargs: return convert(self._default, kwargs["increment"]) return self._default return wrapper @default.setter def default(self, value): self._default = value s = {"k": int, "d": {MyOptional("k", default=2): int, "l": [{"l": [int]}]}} v = {"k": 1, "d": {"l": [{"l": [3, 4, 5]}]}} d = Schema(s).validate(v, increment=1) assert d["k"] == 1 and d["d"]["k"] == 3 and d["d"]["l"][0]["l"] == [3, 4, 5] d = Schema(s).validate(v, increment=10) assert d["k"] == 1 and d["d"]["k"] == 12 and d["d"]["l"][0]["l"] == [3, 4, 5] def test_literal_repr(): assert ( repr(Literal("test", description="testing")) == 'Literal("test", description="testing")' ) assert repr(Literal("test")) == 'Literal("test", description="")' def test_json_schema(): s = Schema({"test": str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"type": "string"}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_with_title(): s = Schema({"test": str}, name="Testing a schema") assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "title": "Testing a schema", "properties": {"test": {"type": "string"}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_types(): s = Schema( { Optional("test_str"): str, Optional("test_int"): int, Optional("test_float"): float, Optional("test_bool"): bool, } ) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test_str": {"type": "string"}, "test_int": {"type": "integer"}, "test_float": {"type": "number"}, "test_bool": {"type": "boolean"}, }, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_other_types(): """Test that data types not supported by JSON schema are returned as strings""" s = Schema({Optional("test_other"): bytes}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test_other": {"type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_nested(): s = Schema({"test": {"other": str}}, ignore_extra_keys=True) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test": { "type": "object", "properties": {"other": {"type": "string"}}, "additionalProperties": True, "required": ["other"], } }, "required": ["test"], "additionalProperties": True, "type": "object", } def test_json_schema_nested_schema(): s = Schema({"test": Schema({"other": str}, ignore_extra_keys=True)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test": { "type": "object", "properties": {"other": {"type": "string"}}, "additionalProperties": True, "required": ["other"], } }, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_optional_key(): s = Schema({Optional("test"): str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_optional_key_nested(): s = Schema({"test": {Optional("other"): str}}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test": { "type": "object", "properties": {"other": {"type": "string"}}, "additionalProperties": False, "required": [], } }, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_or_key(): s = Schema({Or("test1", "test2"): str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test1": {"type": "string"}, "test2": {"type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_or_values(): s = Schema({"param": Or("test1", "test2")}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"param": {"enum": ["test1", "test2"]}}, "required": ["param"], "additionalProperties": False, "type": "object", } def test_json_schema_or_values_nested(): s = Schema({"param": Or([str], [list])}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "param": { "anyOf": [ {"type": "array", "items": {"type": "string"}}, {"type": "array", "items": {"type": "array"}}, ] } }, "required": ["param"], "additionalProperties": False, "type": "object", } def test_json_schema_or_values_with_optional(): s = Schema({Optional("whatever"): Or("test1", "test2")}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"whatever": {"enum": ["test1", "test2"]}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_regex(): s = Schema({Optional("username"): Regex("[a-zA-Z][a-zA-Z0-9]{3,}")}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "username": {"type": "string", "pattern": "[a-zA-Z][a-zA-Z0-9]{3,}"} }, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_or_types(): s = Schema({"test": Or(str, int)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"anyOf": [{"type": "string"}, {"type": "integer"}]}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_or_only_one(): s = Schema({"test": Or(str, lambda x: len(x) < 5)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"type": "string"}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_and_types(): # Can't determine the type, it will not be checked s = Schema({"test": And(str, lambda x: len(x) < 5)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"type": "string"}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_or_one_value(): s = Schema({"test": Or(True)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"const": True}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_const_is_none(): s = Schema({"test": None}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"const": None}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_const_is_callable(): def something_callable(x): return x * 2 s = Schema({"test": something_callable}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_const_is_custom_type(): class SomethingSerializable: def __str__(self): return "Hello!" s = Schema({"test": SomethingSerializable()}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"const": "Hello!"}}, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_default_is_custom_type(): class SomethingSerializable: def __str__(self): return "Hello!" s = Schema({Optional("test", default=SomethingSerializable()): str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"default": "Hello!", "type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_default_is_callable(): def default_func(): return "Hello!" s = Schema({Optional("test", default=default_func): str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"default": "Hello!", "type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_default_is_callable_with_args_passed_from_json_schema(): def default_func(**kwargs): return "Hello, " + kwargs["name"] s = Schema({Optional("test", default=default_func): str}) assert s.json_schema("my-id", name="World!") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"default": "Hello, World!", "type": "string"}}, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_object_or_array_of_object(): # Complex test where "test" accepts either an object or an array of that object o = {"param1": "test1", Optional("param2"): "test2"} s = Schema({"test": Or(o, [o])}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test": { "anyOf": [ { "additionalProperties": False, "properties": { "param1": {"const": "test1"}, "param2": {"const": "test2"}, }, "required": ["param1"], "type": "object", }, { "type": "array", "items": { "additionalProperties": False, "properties": { "param1": {"const": "test1"}, "param2": {"const": "test2"}, }, "required": ["param1"], "type": "object", }, }, ] } }, "required": ["test"], "additionalProperties": False, "type": "object", } def test_json_schema_and_simple(): s = Schema({"test1": And(str, "test2")}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test1": {"allOf": [{"type": "string"}, {"const": "test2"}]}}, "required": ["test1"], "additionalProperties": False, "type": "object", } def test_json_schema_and_list(): s = Schema({"param1": And(["choice1", "choice2"], list)}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "param1": { "allOf": [ {"type": "array", "items": {"enum": ["choice1", "choice2"]}}, {"type": "array"}, ] } }, "required": ["param1"], "additionalProperties": False, "type": "object", } def test_json_schema_forbidden_key_ignored(): s = Schema({Forbidden("forbidden"): str, "test": str}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": {"test": {"type": "string"}}, "required": ["test"], "additionalProperties": False, "type": "object", } @mark.parametrize( "input_schema, ignore_extra_keys, additional_properties", [ ({}, False, False), ({str: str}, False, True), ({Optional(str): str}, False, True), ({object: int}, False, True), ({}, True, True), ], ) def test_json_schema_additional_properties( input_schema, ignore_extra_keys, additional_properties ): s = Schema(input_schema, ignore_extra_keys=ignore_extra_keys) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "required": [], "properties": {}, "additionalProperties": additional_properties, "type": "object", } def test_json_schema_additional_properties_multiple(): s = Schema({"named_property": bool, object: int}) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "required": ["named_property"], "properties": {"named_property": {"type": "boolean"}}, "additionalProperties": True, "type": "object", } @mark.parametrize( "input_schema, expected_keyword, expected_value", [ (int, "type", "integer"), (float, "type", "number"), (list, "type", "array"), (bool, "type", "boolean"), (dict, "type", "object"), ("test", "const", "test"), (Or(1, 2, 3), "enum", [1, 2, 3]), (Or(str, int), "anyOf", [{"type": "string"}, {"type": "integer"}]), (And(str, "value"), "allOf", [{"type": "string"}, {"const": "value"}]), ], ) def test_json_schema_root_not_dict(input_schema, expected_keyword, expected_value): """Test generating simple JSON Schemas where the root element is not a dict""" json_schema = Schema(input_schema).json_schema("my-id") assert json_schema == { expected_keyword: expected_value, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } @mark.parametrize( "input_schema, expected_keyword, expected_value", [([1, 2, 3], "enum", [1, 2, 3]), ([1], "const", 1), ([str], "type", "string")], ) def test_json_schema_array(input_schema, expected_keyword, expected_value): json_schema = Schema(input_schema).json_schema("my-id") assert json_schema == { "type": "array", "items": {expected_keyword: expected_value}, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_regex_root(): json_schema = Schema(Regex("^v\\d+")).json_schema("my-id") assert json_schema == { "type": "string", "pattern": "^v\\d+", "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_dict_type(): json_schema = Schema({Optional("test1", default={}): dict}).json_schema("my-id") assert json_schema == { "type": "object", "properties": {"test1": {"default": {}, "type": "object"}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_title_and_description(): s = Schema( {Literal("productId", description="The unique identifier for a product"): int}, name="Product", description="A product in the catalog", ) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "title": "Product", "description": "A product in the catalog", "properties": { "productId": { "description": "The unique identifier for a product", "type": "integer", } }, "required": ["productId"], "additionalProperties": False, "type": "object", } def test_json_schema_description_nested(): s = Schema( { Optional( Literal("test1", description="A description here"), default={} ): Or([str], [list]) } ) assert s.json_schema("my-id") == { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "my-id", "properties": { "test1": { "default": {}, "description": "A description here", "anyOf": [ {"items": {"type": "string"}, "type": "array"}, {"items": {"type": "array"}, "type": "array"}, ], } }, "required": [], "additionalProperties": False, "type": "object", } def test_json_schema_description_or_nested(): s = Schema( { Optional( Or( Literal("test1", description="A description here"), Literal("test2", description="Another"), ) ): Or([str], [list]) } ) assert s.json_schema("my-id") == { "type": "object", "properties": { "test1": { "description": "A description here", "anyOf": [ {"items": {"type": "string"}, "type": "array"}, {"items": {"type": "array"}, "type": "array"}, ], }, "test2": { "description": "Another", "anyOf": [ {"items": {"type": "string"}, "type": "array"}, {"items": {"type": "array"}, "type": "array"}, ], }, }, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_literal_with_enum(): s = Schema( { Literal("test", description="A test"): Or( Literal("literal1", description="A literal with description"), Literal("literal2", description="Another literal with description"), ) } ) assert s.json_schema("my-id") == { "type": "object", "properties": { "test": {"description": "A test", "enum": ["literal1", "literal2"]} }, "required": ["test"], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_description_and_nested(): s = Schema( { Optional( Or( Literal("test1", description="A description here"), Literal("test2", description="Another"), ) ): And([str], [list]) } ) assert s.json_schema("my-id") == { "type": "object", "properties": { "test1": { "description": "A description here", "allOf": [ {"items": {"type": "string"}, "type": "array"}, {"items": {"type": "array"}, "type": "array"}, ], }, "test2": { "description": "Another", "allOf": [ {"items": {"type": "string"}, "type": "array"}, {"items": {"type": "array"}, "type": "array"}, ], }, }, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_description(): s = Schema( {Optional(Literal("test1", description="A description here"), default={}): dict} ) assert s.validate({"test1": {}}) def test_description_with_default(): s = Schema( {Optional(Literal("test1", description="A description here"), default={}): dict} ) assert s.validate({}) == {"test1": {}} def test_json_schema_ref_in_list(): s = Schema( Or( Schema([str], name="Inner test", as_reference=True), Schema([str], name="Inner test2", as_reference=True), ) ) generated_json_schema = s.json_schema("my-id") assert generated_json_schema == { "definitions": { "Inner test": {"items": {"type": "string"}, "type": "array"}, "Inner test2": {"items": {"type": "string"}, "type": "array"}, }, "anyOf": [ {"$ref": "#/definitions/Inner test"}, {"$ref": "#/definitions/Inner test2"}, ], "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_refs(): s = Schema({"test1": str, "test2": str, "test3": str}) hashed = "#" + str(hash(repr(sorted({"type": "string"}.items())))) generated_json_schema = s.json_schema("my-id", use_refs=True) # The order can change, so let's check indirectly assert generated_json_schema["type"] == "object" assert sorted(generated_json_schema["required"]) == ["test1", "test2", "test3"] assert generated_json_schema["additionalProperties"] is False assert generated_json_schema["$id"] == "my-id" assert generated_json_schema["$schema"] == "http://json-schema.org/draft-07/schema#" # There will be one of the property being the id and 2 referencing it, but which one is random id_schema_part = {"type": "string", "$id": hashed} ref_schema_part = {"$ref": hashed} nb_id_schema = 0 nb_ref_schema = 0 for v in generated_json_schema["properties"].values(): if v == id_schema_part: nb_id_schema += 1 elif v == ref_schema_part: nb_ref_schema += 1 assert nb_id_schema == 1 assert nb_ref_schema == 2 def test_json_schema_refs_is_smaller(): key_names = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", ] key_values = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "value1", "value2", "value3", "value4", "value5", None, ] s = Schema( { Literal( Or(*key_names), description="A key that can have many names" ): key_values } ) assert len(json.dumps(s.json_schema("my-id", use_refs=False))) > len( json.dumps(s.json_schema("my-id", use_refs=True)) ) def test_json_schema_refs_no_missing(): key_names = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", ] key_values = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "value1", "value2", "value3", "value4", "value5", None, ] s = Schema( { Literal( Or(*key_names), description="A key that can have many names" ): key_values } ) json_s = s.json_schema("my-id", use_refs=True) schema_ids = [] refs = [] def _get_ids_and_refs(schema_dict): for k, v in schema_dict.items(): if isinstance(v, dict): _get_ids_and_refs(v) continue if k == "$id" and v != "my-id": schema_ids.append(v) elif k == "$ref": refs.append(v) _get_ids_and_refs(json_s) # No ID is repeated assert len(schema_ids) == len(set(schema_ids)) # All IDs are used in a ref for schema_id in schema_ids: assert schema_id in refs # All refs have an associated ID for ref in refs: assert ref in schema_ids def test_json_schema_definitions(): sub_schema = Schema({"sub_key1": int}, name="sub_schema", as_reference=True) main_schema = Schema({"main_key1": str, "main_key2": sub_schema}) json_schema = main_schema.json_schema("my-id") assert sorted_dict(json_schema) == { "type": "object", "properties": { "main_key1": {"type": "string"}, "main_key2": {"$ref": "#/definitions/sub_schema"}, }, "required": ["main_key1", "main_key2"], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "sub_schema": { "type": "object", "properties": {"sub_key1": {"type": "integer"}}, "required": ["sub_key1"], "additionalProperties": False, } }, } def test_json_schema_definitions_and_literals(): sub_schema = Schema( {Literal("sub_key1", description="Sub key 1"): int}, name="sub_schema", as_reference=True, description="Sub Schema", ) main_schema = Schema( { Literal("main_key1", description="Main Key 1"): str, Literal("main_key2", description="Main Key 2"): sub_schema, Literal("main_key3", description="Main Key 3"): sub_schema, } ) json_schema = main_schema.json_schema("my-id") assert sorted_dict(json_schema) == { "type": "object", "properties": { "main_key1": {"description": "Main Key 1", "type": "string"}, "main_key2": { "$ref": "#/definitions/sub_schema", "description": "Main Key 2", }, "main_key3": { "$ref": "#/definitions/sub_schema", "description": "Main Key 3", }, }, "required": ["main_key1", "main_key2", "main_key3"], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "sub_schema": { "description": "Sub Schema", "type": "object", "properties": { "sub_key1": {"description": "Sub key 1", "type": "integer"} }, "required": ["sub_key1"], "additionalProperties": False, } }, } def test_json_schema_definitions_nested(): sub_sub_schema = Schema( {"sub_sub_key1": int}, name="sub_sub_schema", as_reference=True ) sub_schema = Schema( {"sub_key1": int, "sub_key2": sub_sub_schema}, name="sub_schema", as_reference=True, ) main_schema = Schema({"main_key1": str, "main_key2": sub_schema}) json_schema = main_schema.json_schema("my-id") assert sorted_dict(json_schema) == { "type": "object", "properties": { "main_key1": {"type": "string"}, "main_key2": {"$ref": "#/definitions/sub_schema"}, }, "required": ["main_key1", "main_key2"], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "sub_schema": { "type": "object", "properties": { "sub_key1": {"type": "integer"}, "sub_key2": {"$ref": "#/definitions/sub_sub_schema"}, }, "required": ["sub_key1", "sub_key2"], "additionalProperties": False, }, "sub_sub_schema": { "type": "object", "properties": {"sub_sub_key1": {"type": "integer"}}, "required": ["sub_sub_key1"], "additionalProperties": False, }, }, } def test_json_schema_definitions_recursive(): """Create a JSON schema with an object that refers to itself This is the example from here: https://json-schema.org/understanding-json-schema/structuring.html#recursion """ children = [] person = Schema( {Optional("name"): str, Optional("children"): children}, name="person", as_reference=True, ) children.append(person) json_schema = person.json_schema("my-id") assert json_schema == { "$ref": "#/definitions/person", "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", "title": "person", "definitions": { "person": { "type": "object", "properties": { "name": {"type": "string"}, "children": { "type": "array", "items": {"$ref": "#/definitions/person"}, }, }, "required": [], "additionalProperties": False, } }, } def test_json_schema_definitions_invalid(): with raises(ValueError): _ = Schema({"test1": str}, as_reference=True) def test_json_schema_default_value(): s = Schema({Optional("test1", default=42): int}) assert s.json_schema("my-id") == { "type": "object", "properties": {"test1": {"type": "integer", "default": 42}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_default_value_with_literal(): s = Schema({Optional(Literal("test1"), default=False): bool}) assert s.json_schema("my-id") == { "type": "object", "properties": {"test1": {"type": "boolean", "default": False}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_default_is_none(): s = Schema({Optional("test1", default=None): str}) assert s.json_schema("my-id") == { "type": "object", "properties": {"test1": {"type": "string", "default": None}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_default_is_tuple(): s = Schema({Optional("test1", default=(1, 2)): list}) assert s.json_schema("my-id") == { "type": "object", "properties": {"test1": {"type": "array", "default": [1, 2]}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_json_schema_default_is_literal(): s = Schema({Optional("test1", default=Literal("Hello!")): str}) assert s.json_schema("my-id") == { "type": "object", "properties": {"test1": {"type": "string", "default": "Hello!"}}, "required": [], "additionalProperties": False, "$id": "my-id", "$schema": "http://json-schema.org/draft-07/schema#", } def test_prepend_schema_name(): try: Schema({"key1": int}).validate({"key1": "a"}) except SchemaError as e: assert str(e) == "Key 'key1' error:\n'a' should be instance of 'int'" try: Schema({"key1": int}, name="custom_schemaname").validate({"key1": "a"}) except SchemaError as e: assert ( str(e) == "'custom_schemaname' Key 'key1' error:\n'a' should be instance of 'int'" ) try: Schema(int, name="custom_schemaname").validate("a") except SchemaUnexpectedTypeError as e: assert str(e) == "'custom_schemaname' 'a' should be instance of 'int'" def test_dict_literal_error_string(): # this is a simplified regression test of the bug in github issue #240 assert Schema(Or({"a": 1}, error="error: {}")).is_valid(dict(a=1)) def test_callable_error(): # this tests for the behavior desired in github pull request #238 e = None try: Schema(lambda d: False, error="{}").validate("This is the error message") except SchemaError as ex: e = ex assert e.errors == ["This is the error message"]