Spaces:
Running
Running
# The contents of this file are automatically written by | |
# tools/generate_schema_wrapper.py. Do not modify directly. | |
import collections | |
import contextlib | |
import inspect | |
import json | |
import jsonschema | |
import numpy as np | |
import pandas as pd | |
# If DEBUG_MODE is True, then schema objects are converted to dict and | |
# validated at creation time. This slows things down, particularly for | |
# larger specs, but leads to much more useful tracebacks for the user. | |
# Individual schema classes can override this by setting the | |
# class-level _class_is_valid_at_instantiation attribute to False | |
DEBUG_MODE = True | |
def enable_debug_mode(): | |
global DEBUG_MODE | |
DEBUG_MODE = True | |
def disable_debug_mode(): | |
global DEBUG_MODE | |
DEBUG_MODE = True | |
def debug_mode(arg): | |
global DEBUG_MODE | |
original = DEBUG_MODE | |
DEBUG_MODE = arg | |
try: | |
yield | |
finally: | |
DEBUG_MODE = original | |
def _subclasses(cls): | |
"""Breadth-first sequence of all classes which inherit from cls.""" | |
seen = set() | |
current_set = {cls} | |
while current_set: | |
seen |= current_set | |
current_set = set.union(*(set(cls.__subclasses__()) for cls in current_set)) | |
for cls in current_set - seen: | |
yield cls | |
def _todict(obj, validate, context): | |
"""Convert an object to a dict representation.""" | |
if isinstance(obj, SchemaBase): | |
return obj.to_dict(validate=validate, context=context) | |
elif isinstance(obj, (list, tuple, np.ndarray)): | |
return [_todict(v, validate, context) for v in obj] | |
elif isinstance(obj, dict): | |
return { | |
k: _todict(v, validate, context) | |
for k, v in obj.items() | |
if v is not Undefined | |
} | |
elif hasattr(obj, "to_dict"): | |
return obj.to_dict() | |
elif isinstance(obj, np.number): | |
return float(obj) | |
elif isinstance(obj, (pd.Timestamp, np.datetime64)): | |
return pd.Timestamp(obj).isoformat() | |
else: | |
return obj | |
def _resolve_references(schema, root=None): | |
"""Resolve schema references.""" | |
resolver = jsonschema.RefResolver.from_schema(root or schema) | |
while "$ref" in schema: | |
with resolver.resolving(schema["$ref"]) as resolved: | |
schema = resolved | |
return schema | |
class SchemaValidationError(jsonschema.ValidationError): | |
"""A wrapper for jsonschema.ValidationError with friendlier traceback""" | |
def __init__(self, obj, err): | |
super(SchemaValidationError, self).__init__(**self._get_contents(err)) | |
self.obj = obj | |
def _get_contents(err): | |
"""Get a dictionary with the contents of a ValidationError""" | |
try: | |
# works in jsonschema 2.3 or later | |
contents = err._contents() | |
except AttributeError: | |
try: | |
# works in Python >=3.4 | |
spec = inspect.getfullargspec(err.__init__) | |
except AttributeError: | |
# works in Python <3.4 | |
spec = inspect.getargspec(err.__init__) | |
contents = {key: getattr(err, key) for key in spec.args[1:]} | |
return contents | |
def __str__(self): | |
cls = self.obj.__class__ | |
schema_path = ["{}.{}".format(cls.__module__, cls.__name__)] | |
schema_path.extend(self.schema_path) | |
schema_path = "->".join( | |
str(val) | |
for val in schema_path[:-1] | |
if val not in ("properties", "additionalProperties", "patternProperties") | |
) | |
return """Invalid specification | |
{}, validating {!r} | |
{} | |
""".format( | |
schema_path, self.validator, self.message | |
) | |
class UndefinedType(object): | |
"""A singleton object for marking undefined attributes""" | |
__instance = None | |
def __new__(cls, *args, **kwargs): | |
if not isinstance(cls.__instance, cls): | |
cls.__instance = object.__new__(cls, *args, **kwargs) | |
return cls.__instance | |
def __repr__(self): | |
return "Undefined" | |
Undefined = UndefinedType() | |
class SchemaBase(object): | |
"""Base class for schema wrappers. | |
Each derived class should set the _schema class attribute (and optionally | |
the _rootschema class attribute) which is used for validation. | |
""" | |
_schema = None | |
_rootschema = None | |
_class_is_valid_at_instantiation = True | |
_validator = jsonschema.Draft7Validator | |
def __init__(self, *args, **kwds): | |
# Two valid options for initialization, which should be handled by | |
# derived classes: | |
# - a single arg with no kwds, for, e.g. {'type': 'string'} | |
# - zero args with zero or more kwds for {'type': 'object'} | |
if self._schema is None: | |
raise ValueError( | |
"Cannot instantiate object of type {}: " | |
"_schema class attribute is not defined." | |
"".format(self.__class__) | |
) | |
if kwds: | |
assert len(args) == 0 | |
else: | |
assert len(args) in [0, 1] | |
# use object.__setattr__ because we override setattr below. | |
object.__setattr__(self, "_args", args) | |
object.__setattr__(self, "_kwds", kwds) | |
if DEBUG_MODE and self._class_is_valid_at_instantiation: | |
self.to_dict(validate=True) | |
def copy(self, deep=True, ignore=()): | |
"""Return a copy of the object | |
Parameters | |
---------- | |
deep : boolean or list, optional | |
If True (default) then return a deep copy of all dict, list, and | |
SchemaBase objects within the object structure. | |
If False, then only copy the top object. | |
If a list or iterable, then only copy the listed attributes. | |
ignore : list, optional | |
A list of keys for which the contents should not be copied, but | |
only stored by reference. | |
""" | |
def _shallow_copy(obj): | |
if isinstance(obj, SchemaBase): | |
return obj.copy(deep=False) | |
elif isinstance(obj, list): | |
return obj[:] | |
elif isinstance(obj, dict): | |
return obj.copy() | |
else: | |
return obj | |
def _deep_copy(obj, ignore=()): | |
if isinstance(obj, SchemaBase): | |
args = tuple(_deep_copy(arg) for arg in obj._args) | |
kwds = { | |
k: (_deep_copy(v, ignore=ignore) if k not in ignore else v) | |
for k, v in obj._kwds.items() | |
} | |
with debug_mode(False): | |
return obj.__class__(*args, **kwds) | |
elif isinstance(obj, list): | |
return [_deep_copy(v, ignore=ignore) for v in obj] | |
elif isinstance(obj, dict): | |
return { | |
k: (_deep_copy(v, ignore=ignore) if k not in ignore else v) | |
for k, v in obj.items() | |
} | |
else: | |
return obj | |
try: | |
deep = list(deep) | |
except TypeError: | |
deep_is_list = False | |
else: | |
deep_is_list = True | |
if deep and not deep_is_list: | |
return _deep_copy(self, ignore=ignore) | |
with debug_mode(False): | |
copy = self.__class__(*self._args, **self._kwds) | |
if deep_is_list: | |
for attr in deep: | |
copy[attr] = _shallow_copy(copy._get(attr)) | |
return copy | |
def _get(self, attr, default=Undefined): | |
"""Get an attribute, returning default if not present.""" | |
attr = self._kwds.get(attr, Undefined) | |
if attr is Undefined: | |
attr = default | |
return attr | |
def __getattr__(self, attr): | |
# reminder: getattr is called after the normal lookups | |
if attr == "_kwds": | |
raise AttributeError() | |
if attr in self._kwds: | |
return self._kwds[attr] | |
else: | |
try: | |
_getattr = super(SchemaBase, self).__getattr__ | |
except AttributeError: | |
_getattr = super(SchemaBase, self).__getattribute__ | |
return _getattr(attr) | |
def __setattr__(self, item, val): | |
self._kwds[item] = val | |
def __getitem__(self, item): | |
return self._kwds[item] | |
def __setitem__(self, item, val): | |
self._kwds[item] = val | |
def __repr__(self): | |
if self._kwds: | |
args = ( | |
"{}: {!r}".format(key, val) | |
for key, val in sorted(self._kwds.items()) | |
if val is not Undefined | |
) | |
args = "\n" + ",\n".join(args) | |
return "{0}({{{1}\n}})".format( | |
self.__class__.__name__, args.replace("\n", "\n ") | |
) | |
else: | |
return "{}({!r})".format(self.__class__.__name__, self._args[0]) | |
def __eq__(self, other): | |
return ( | |
type(self) is type(other) | |
and self._args == other._args | |
and self._kwds == other._kwds | |
) | |
def to_dict(self, validate=True, ignore=None, context=None): | |
"""Return a dictionary representation of the object | |
Parameters | |
---------- | |
validate : boolean or string | |
If True (default), then validate the output dictionary | |
against the schema. If "deep" then recursively validate | |
all objects in the spec. This takes much more time, but | |
it results in friendlier tracebacks for large objects. | |
ignore : list | |
A list of keys to ignore. This will *not* passed to child to_dict | |
function calls. | |
context : dict (optional) | |
A context dictionary that will be passed to all child to_dict | |
function calls | |
Returns | |
------- | |
dct : dictionary | |
The dictionary representation of this object | |
Raises | |
------ | |
jsonschema.ValidationError : | |
if validate=True and the dict does not conform to the schema | |
""" | |
if context is None: | |
context = {} | |
if ignore is None: | |
ignore = [] | |
sub_validate = "deep" if validate == "deep" else False | |
if self._args and not self._kwds: | |
result = _todict(self._args[0], validate=sub_validate, context=context) | |
elif not self._args: | |
result = _todict( | |
{k: v for k, v in self._kwds.items() if k not in ignore}, | |
validate=sub_validate, | |
context=context, | |
) | |
else: | |
raise ValueError( | |
"{} instance has both a value and properties : " | |
"cannot serialize to dict".format(self.__class__) | |
) | |
if validate: | |
try: | |
self.validate(result) | |
except jsonschema.ValidationError as err: | |
raise SchemaValidationError(self, err) | |
return result | |
def to_json( | |
self, validate=True, ignore=[], context={}, indent=2, sort_keys=True, **kwargs | |
): | |
"""Emit the JSON representation for this object as a string. | |
Parameters | |
---------- | |
validate : boolean or string | |
If True (default), then validate the output dictionary | |
against the schema. If "deep" then recursively validate | |
all objects in the spec. This takes much more time, but | |
it results in friendlier tracebacks for large objects. | |
ignore : list | |
A list of keys to ignore. This will *not* passed to child to_dict | |
function calls. | |
context : dict (optional) | |
A context dictionary that will be passed to all child to_dict | |
function calls | |
indent : integer, default 2 | |
the number of spaces of indentation to use | |
sort_keys : boolean, default True | |
if True, sort keys in the output | |
**kwargs | |
Additional keyword arguments are passed to ``json.dumps()`` | |
Returns | |
------- | |
spec : string | |
The JSON specification of the chart object. | |
""" | |
dct = self.to_dict(validate=validate, ignore=ignore, context=context) | |
return json.dumps(dct, indent=indent, sort_keys=sort_keys, **kwargs) | |
def _default_wrapper_classes(cls): | |
"""Return the set of classes used within cls.from_dict()""" | |
return _subclasses(SchemaBase) | |
def from_dict(cls, dct, validate=True, _wrapper_classes=None): | |
"""Construct class from a dictionary representation | |
Parameters | |
---------- | |
dct : dictionary | |
The dict from which to construct the class | |
validate : boolean | |
If True (default), then validate the input against the schema. | |
_wrapper_classes : list (optional) | |
The set of SchemaBase classes to use when constructing wrappers | |
of the dict inputs. If not specified, the result of | |
cls._default_wrapper_classes will be used. | |
Returns | |
------- | |
obj : Schema object | |
The wrapped schema | |
Raises | |
------ | |
jsonschema.ValidationError : | |
if validate=True and dct does not conform to the schema | |
""" | |
if validate: | |
cls.validate(dct) | |
if _wrapper_classes is None: | |
_wrapper_classes = cls._default_wrapper_classes() | |
converter = _FromDict(_wrapper_classes) | |
return converter.from_dict(dct, cls) | |
def from_json(cls, json_string, validate=True, **kwargs): | |
"""Instantiate the object from a valid JSON string | |
Parameters | |
---------- | |
json_string : string | |
The string containing a valid JSON chart specification. | |
validate : boolean | |
If True (default), then validate the input against the schema. | |
**kwargs : | |
Additional keyword arguments are passed to json.loads | |
Returns | |
------- | |
chart : Chart object | |
The altair Chart object built from the specification. | |
""" | |
dct = json.loads(json_string, **kwargs) | |
return cls.from_dict(dct, validate=validate) | |
def validate(cls, instance, schema=None): | |
""" | |
Validate the instance against the class schema in the context of the | |
rootschema. | |
""" | |
if schema is None: | |
schema = cls._schema | |
resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) | |
return jsonschema.validate( | |
instance, schema, cls=cls._validator, resolver=resolver | |
) | |
def resolve_references(cls, schema=None): | |
"""Resolve references in the context of this object's schema or root schema.""" | |
return _resolve_references( | |
schema=(schema or cls._schema), | |
root=(cls._rootschema or cls._schema or schema), | |
) | |
def validate_property(cls, name, value, schema=None): | |
""" | |
Validate a property against property schema in the context of the | |
rootschema | |
""" | |
value = _todict(value, validate=False, context={}) | |
props = cls.resolve_references(schema or cls._schema).get("properties", {}) | |
resolver = jsonschema.RefResolver.from_schema(cls._rootschema or cls._schema) | |
return jsonschema.validate(value, props.get(name, {}), resolver=resolver) | |
def __dir__(self): | |
return list(self._kwds.keys()) | |
def _passthrough(*args, **kwds): | |
return args[0] if args else kwds | |
class _FromDict(object): | |
"""Class used to construct SchemaBase class hierarchies from a dict | |
The primary purpose of using this class is to be able to build a hash table | |
that maps schemas to their wrapper classes. The candidate classes are | |
specified in the ``class_list`` argument to the constructor. | |
""" | |
_hash_exclude_keys = ("definitions", "title", "description", "$schema", "id") | |
def __init__(self, class_list): | |
# Create a mapping of a schema hash to a list of matching classes | |
# This lets us quickly determine the correct class to construct | |
self.class_dict = collections.defaultdict(list) | |
for cls in class_list: | |
if cls._schema is not None: | |
self.class_dict[self.hash_schema(cls._schema)].append(cls) | |
def hash_schema(cls, schema, use_json=True): | |
""" | |
Compute a python hash for a nested dictionary which | |
properly handles dicts, lists, sets, and tuples. | |
At the top level, the function excludes from the hashed schema all keys | |
listed in `exclude_keys`. | |
This implements two methods: one based on conversion to JSON, and one based | |
on recursive conversions of unhashable to hashable types; the former seems | |
to be slightly faster in several benchmarks. | |
""" | |
if cls._hash_exclude_keys and isinstance(schema, dict): | |
schema = { | |
key: val | |
for key, val in schema.items() | |
if key not in cls._hash_exclude_keys | |
} | |
if use_json: | |
s = json.dumps(schema, sort_keys=True) | |
return hash(s) | |
else: | |
def _freeze(val): | |
if isinstance(val, dict): | |
return frozenset((k, _freeze(v)) for k, v in val.items()) | |
elif isinstance(val, set): | |
return frozenset(map(_freeze, val)) | |
elif isinstance(val, list) or isinstance(val, tuple): | |
return tuple(map(_freeze, val)) | |
else: | |
return val | |
return hash(_freeze(schema)) | |
def from_dict( | |
self, dct, cls=None, schema=None, rootschema=None, default_class=_passthrough | |
): | |
"""Construct an object from a dict representation""" | |
if (schema is None) == (cls is None): | |
raise ValueError("Must provide either cls or schema, but not both.") | |
if schema is None: | |
schema = schema or cls._schema | |
rootschema = rootschema or cls._rootschema | |
rootschema = rootschema or schema | |
if isinstance(dct, SchemaBase): | |
return dct | |
if cls is None: | |
# If there are multiple matches, we use the first one in the dict. | |
# Our class dict is constructed breadth-first from top to bottom, | |
# so the first class that matches is the most general match. | |
matches = self.class_dict[self.hash_schema(schema)] | |
if matches: | |
cls = matches[0] | |
else: | |
cls = default_class | |
schema = _resolve_references(schema, rootschema) | |
if "anyOf" in schema or "oneOf" in schema: | |
schemas = schema.get("anyOf", []) + schema.get("oneOf", []) | |
for possible_schema in schemas: | |
resolver = jsonschema.RefResolver.from_schema(rootschema) | |
try: | |
jsonschema.validate(dct, possible_schema, resolver=resolver) | |
except jsonschema.ValidationError: | |
continue | |
else: | |
return self.from_dict( | |
dct, | |
schema=possible_schema, | |
rootschema=rootschema, | |
default_class=cls, | |
) | |
if isinstance(dct, dict): | |
# TODO: handle schemas for additionalProperties/patternProperties | |
props = schema.get("properties", {}) | |
kwds = {} | |
for key, val in dct.items(): | |
if key in props: | |
val = self.from_dict(val, schema=props[key], rootschema=rootschema) | |
kwds[key] = val | |
return cls(**kwds) | |
elif isinstance(dct, list): | |
item_schema = schema.get("items", {}) | |
dct = [ | |
self.from_dict(val, schema=item_schema, rootschema=rootschema) | |
for val in dct | |
] | |
return cls(dct) | |
else: | |
return cls(dct) | |