<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 JSON Schema Parser
Notes on the JSON schema / Traitlets package
Goal
Step1:  OK, now let's write a traitlets class that does the same thing
Step4:  Roadmap
Start by recognizing all simple JSON types in the schema ("string", "number", "integer", "boolean", "null")
Next recognize objects containing simple types
Next recognize compound simple types (i.e. where type is a list of simple types)
Next recognize arrays & enums
Next recognize "$ref" definitions
Next recognize "anyOf", "oneOf", "allOf" definitions... first is essentially a traitlets Union, second is a Union where only one must match, and "allOf" is essentially a composite object (not sure if traitlets has that...) Note that among these, Vega-Lite only uses "anyOf"
Catalog all validation keywords... Implement custom traitlets that support all the various validation keywords for each type. (Validation keywords listed here)
Use hypothesis for testing?
Challenges & Questions to think about
JSONSchema ignores any keys/properties which are not listed in the schema. Traitlets warns, and in the future will raise an error for undefined keys/properties
this may be OK... we can just document the fact that traitlets is more prescriptive than JSONschema
JSON allows undefined values as well as explicit nulls, which map to None. Traitlets treats None as undefined. How to resolve this?
Best option is probably to use an undefined sentinel within the traitlets structure, such that the code knows when to ignore keys & produces dicts which translate directly to the correct JSON
Will probably need to define some custom trait types, e.g. Null, and also extend simple trait types to allow for the more extensive validations allowed in JSON Schema.
Generate subclasses with the code
What version of the schema should we target? Perhaps try to target multiple versions?
start with 04 because this is what's supported by jsonschema and used by Vega(-Lite)
Ideas
Easiest way
Step5:  Trying it out...
Step6:  Testing the result
<ASSISTANT_TASK:>
Python Code:
import json
import jsonschema
simple_schema = {
    "type": "object",
    "properties": {
        "foo": {"type": "string"},
        "bar": {"type": "number"}
    }
}
good_instance = {
    "foo": "hello world",
    "bar": 3.141592653,
}
bad_instance = {
    "foo" : 42,
    "bar" : "string"
}
# Should succeed
jsonschema.validate(good_instance, simple_schema)
# Should fail
try:
    jsonschema.validate(bad_instance, simple_schema)
except jsonschema.ValidationError as err:
    print(err)
Explanation: JSON Schema Parser
Notes on the JSON schema / Traitlets package
Goal: write a function that, given a JSON Schema, will generate code for traitlets objects which provide equivalent validation.
Links
JSON Schema Validation Information
jsonschema Python package
Altair 1.0 parsing code
By-Hand Example
First confirm that we're doing things correctly with the jsonschema package:
End of explanation
import traitlets as T
class SimpleInstance(T.HasTraits):
    foo = T.Unicode()
    bar = T.Float()
# Should succeed
SimpleInstance(**good_instance)
# Should fail
try:
    SimpleInstance(**bad_instance)
except T.TraitError as err:
    print(err)
Explanation: OK, now let's write a traitlets class that does the same thing:
End of explanation
import jinja2
OBJECT_TEMPLATE = 
{%- for import in cls.imports %}
{{ import }}
{%- endfor %}
class {{ cls.classname }}({{ cls.baseclass }}):
    {%- for (name, prop) in cls.properties.items() %}
    {{ name }} = {{ prop.trait_code }}
    {%- endfor %}
class JSONSchema(object):
    A class to wrap JSON Schema objects and reason about their contents
    object_template = OBJECT_TEMPLATE
    
    def __init__(self, schema, root=None):
        self.schema = schema
        self.root = root or schema
        
    @property
    def type(self):
        # TODO: should the default type be considered object?
        return self.schema.get('type', 'object')
    
    @property
    def trait_code(self):
        type_dict = {'string': 'T.Unicode()',
                     'number': 'T.Float()',
                     'integer': 'T.Integer()',
                     'boolean': 'T.Bool()'}
        if self.type not in type_dict:
            raise NotImplementedError()
        return type_dict[self.type]
    
    @property
    def classname(self):
        # TODO: deal with non-root schemas somehow...
        if self.schema is self.root:
            return "RootInstance"
        else:
            raise NotImplementedError("Non-root object schema")
            
    @property
    def baseclass(self):
        return "T.HasTraits"
    
    @property
    def imports(self):
        return ["import traitlets as T"]
    
    @property
    def properties(self):
        return {key: JSONSchema(val) for key, val in self.schema.get('properties', {}).items()}
    
    def object_code(self):
        return jinja2.Template(self.object_template).render(cls=self)
Explanation: Roadmap
Start by recognizing all simple JSON types in the schema ("string", "number", "integer", "boolean", "null")
Next recognize objects containing simple types
Next recognize compound simple types (i.e. where type is a list of simple types)
Next recognize arrays & enums
Next recognize "$ref" definitions
Next recognize "anyOf", "oneOf", "allOf" definitions... first is essentially a traitlets Union, second is a Union where only one must match, and "allOf" is essentially a composite object (not sure if traitlets has that...) Note that among these, Vega-Lite only uses "anyOf"
Catalog all validation keywords... Implement custom traitlets that support all the various validation keywords for each type. (Validation keywords listed here)
Use hypothesis for testing?
Challenges & Questions to think about
JSONSchema ignores any keys/properties which are not listed in the schema. Traitlets warns, and in the future will raise an error for undefined keys/properties
this may be OK... we can just document the fact that traitlets is more prescriptive than JSONschema
JSON allows undefined values as well as explicit nulls, which map to None. Traitlets treats None as undefined. How to resolve this?
Best option is probably to use an undefined sentinel within the traitlets structure, such that the code knows when to ignore keys & produces dicts which translate directly to the correct JSON
Will probably need to define some custom trait types, e.g. Null, and also extend simple trait types to allow for the more extensive validations allowed in JSON Schema.
Generate subclasses with the code
What version of the schema should we target? Perhaps try to target multiple versions?
start with 04 because this is what's supported by jsonschema and used by Vega(-Lite)
Ideas
Easiest way: validate everything with a single HasTraits class via jsonschema, splitting out properties into traitlets
Interface
root schema and all definitions should become their own T.HasTraits class
Objects defined inline should also have their own class with a generated anonymous name
Use Jinja templating; allow output to one file or multiple files with relative imports
root object must have type="object"... this differs from jsonschema
Testing
test cases should be an increasingly complicated set of jsonschema objects, with test cases that should pass and fail. Perhaps store these in a JSON structure? (With a schema?)
An initial prototype
Let's try generating some traitlets classes for simple cases
End of explanation
code = JSONSchema(simple_schema).object_code()
print(code)
Explanation: Trying it out...
End of explanation
exec(code)  # defines RootInstance
# Good instance should validate correctly
RootInstance(**good_instance)
# Bad instance should raise a TraitError
try:
    RootInstance(**bad_instance)
except T.TraitError as err:
    print(err)
Explanation: Testing the result
End of explanation
<END_TASK>