# 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 @contextlib.contextmanager 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 @staticmethod 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) @classmethod def _default_wrapper_classes(cls): """Return the set of classes used within cls.from_dict()""" return _subclasses(SchemaBase) @classmethod 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) @classmethod 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) @classmethod 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 ) @classmethod 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), ) @classmethod 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) @classmethod 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)