Skip to content

Commit

Permalink
Merge pull request #372 from jakevdp/schema-loc
Browse files Browse the repository at this point in the history
Schema loc
  • Loading branch information
jakevdp authored Aug 10, 2017
2 parents a951a95 + 34547f7 commit 387c575
Show file tree
Hide file tree
Showing 15 changed files with 158 additions and 57 deletions.
13 changes: 12 additions & 1 deletion altair/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def _finalize(self, **kwargs):
# Top-level Objects
#*************************************************************************
class TopLevelMixin(object):

@staticmethod
def _png_output_available():
return node.vl_cmd_available('vl2png')
Expand Down Expand Up @@ -251,7 +252,9 @@ def to_dict(self, data=True):
spec : dict
The JSON specification of the chart object.
"""
return super(TopLevelMixin, self).to_dict(data=data)
dct = super(TopLevelMixin, self).to_dict(data=data)
dct['$schema'] = schema.vegalite_schema_url
return dct

@classmethod
def from_dict(cls, dct):
Expand All @@ -267,6 +270,14 @@ def from_dict(cls, dct):
chart : Chart object
The altair Chart object built from the specification.
"""
if '$schema' in dct:
if dct['$schema'] != schema.vegalite_schema_url:
warnings.warn('from_dict: $schema={0} does not match '
'schema used to build this Altair version '
'({1}. '
''.format(dct['$schema'],
schema.vegalite_schema_url))
dct = {k: v for k, v in dct.items() if k != '$schema'}
return super(TopLevelMixin, cls).from_dict(dct)

def to_json(self, data=True, sort_keys=True, **kwargs):
Expand Down
7 changes: 6 additions & 1 deletion altair/examples/tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ def test_json_examples_round_trip(example):

v = Chart.from_dict(json_dict)
v_dict = v.to_dict()
if '$schema' not in json_dict:
v_dict.pop('$schema')
assert v_dict == json_dict

# code generation discards empty function calls, and so we
# filter these out before comparison
v2 = eval(v.to_python())
assert v2.to_dict() == remove_empty_fields(json_dict)
v2_dict = v2.to_dict()
if '$schema' not in json_dict:
v2_dict.pop('$schema')
assert v2_dict == remove_empty_fields(json_dict)


def test_load_example():
Expand Down
1 change: 1 addition & 0 deletions altair/expr/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class DataFrame(object):
>>> chart = Chart(df)
>>> print(chart.to_json(indent=2)) # doctest: +NORMALIZE_WHITESPACE
{
"$schema": "https://vega.github.io/schema/vega-lite/v1.2.1.json",
"data": {
"url": "url/to/my/data.json"
},
Expand Down
2 changes: 1 addition & 1 deletion altair/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ._interface import *
from ._schema import SCHEMA_FILE, load_schema
from ._vegalite_version import vegalite_version
from ._vegalite_version import vegalite_version, vegalite_schema_url
from . import visitors

from ._interface import jstraitlets
Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated by schemapi: do not modify file directly
# - schemapi version: 0.3.0.dev0+414a076
# - date: 2017-08-07 17:41:13
# - schemapi version: 0.3.0.dev0+ee22edf
# - date: 2017-08-09 12:14:26

from .schema import Root
from .schema import AggregateOp
Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/channel_collections.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated file: do not modify directly
# - altair version info: v1.2.0-96-g99cfe91
# - date: 2017-08-07 17:41:14
# - altair version info: v1.2.0-98-g8a98636
# - date: 2017-08-09 12:14:27

import traitlets as T
from . import jstraitlets as jst
Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/channel_wrappers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated file: do not modify directly
# - altair version info: v1.2.0-96-g99cfe91
# - date: 2017-08-07 17:41:14
# - altair version info: v1.2.0-98-g8a98636
# - date: 2017-08-09 12:14:26

import pandas as pd

Expand Down
24 changes: 22 additions & 2 deletions altair/schema/_interface/jstraitlets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated by schemapi: do not modify file directly
# - schemapi version: 0.3.0.dev0+414a076
# - date: 2017-08-07 17:41:13
# - schemapi version: 0.3.0.dev0+ee22edf
# - date: 2017-08-09 12:14:26

"""
Extensions to traitlets for compatibility with JSON Schema
Expand Down Expand Up @@ -41,6 +41,7 @@ def __new__(cls, *args, **kwargs):
def __repr__(self):
return "undefined"


undefined = UndefinedType()


Expand All @@ -65,7 +66,14 @@ class JSONHasTraits(T.HasTraits):
_required_traits = [] # traits required at export. If undefined, a traiterror will be raised
_converter_registry = {} # converter classes to use for to_dict, from_dict

# Metadata is meant to hold top-level metadata keys used in JSON Schema,
# for example '$schema' and/or '$id'
_metadata = {'$schema': undefined, '$id': undefined}

def __init__(self, **kwargs):
# make a copy of the _metadata so we can modify locally
self._metadata = self._metadata.copy()

# Add default traits if needed
default = self._get_additional_traits()
# TODO: protect against overwriting class attributes defined above.
Expand Down Expand Up @@ -664,6 +672,9 @@ def visit_JSONHasTraits(self, obj, *args, **kwargs):
elif key in obj._required_traits:
raise T.TraitError("Required trait '{0}' is undefined'"
"".format(key))
for key, val in obj._metadata.items():
if val is not undefined:
dct[key] = val
return dct


Expand Down Expand Up @@ -698,12 +709,21 @@ def clsvisit_JSONHasTraits(self, cls, dct, *args, **kwargs):
obj = cls('')
additional_traits = cls._get_additional_traits()

# Extract metadata, if it exists
obj._metadata.update({prop: dct[prop]
for prop in obj._metadata
if prop in dct})

# Extract all other items, assigning to appropriate trait
for prop, val in dct.items():
if prop in obj._metadata:
continue
subtrait = obj.traits().get(prop, additional_traits)
if not subtrait:
raise T.TraitError("trait {0} not valid in class {1}"
"".format(prop, cls))
obj.set_trait(prop, self.visit(subtrait, val, *args, **kwargs))

return obj

def visit_Instance(self, trait, dct, *args, **kwargs):
Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/named_channels.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated file: do not modify directly
# - altair version info: v1.2.0-96-g99cfe91
# - date: 2017-08-07 17:41:14
# - altair version info: v1.2.0-98-g8a98636
# - date: 2017-08-09 12:14:26

from . import channel_wrappers

Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/schema.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated by schemapi: do not modify file directly
# - schemapi version: 0.3.0.dev0+414a076
# - date: 2017-08-07 17:41:13
# - schemapi version: 0.3.0.dev0+ee22edf
# - date: 2017-08-09 12:14:26



Expand Down
4 changes: 2 additions & 2 deletions altair/schema/_interface/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Auto-generated by schemapi: do not modify file directly
# - schemapi version: 0.3.0.dev0+414a076
# - date: 2017-08-07 17:41:13
# - schemapi version: 0.3.0.dev0+ee22edf
# - date: 2017-08-09 12:14:26

69 changes: 52 additions & 17 deletions altair/schema/_interface/tests/test_jstraitlets.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Auto-generated by schemapi: do not modify file directly
# - schemapi version: 0.3.0.dev0+414a076
# - date: 2017-08-07 17:41:13
# - schemapi version: 0.3.0.dev0+ee22edf
# - date: 2017-08-09 12:14:26

import pytest

Expand All @@ -17,7 +17,8 @@ def test_undefined_singleton():
def generate_test_cases():
"""yield tuples of (trait, failcases, passcases)"""
# Anys
yield (jst.JSONAny(), [], [1, "hello", {'a':2}, [1, 2, 3], None, undefined])
yield (jst.JSONAny(), [],
[1, "hello", {'a': 2}, [1, 2, 3], None, undefined])

# Nulls
yield (jst.JSONNull(), [0, "None"], [None, undefined])
Expand Down Expand Up @@ -69,7 +70,7 @@ def generate_test_cases():
yield (jst.JSONEnum([1, "2", None], allow_undefined=False), [undefined], [])

# Instances
yield (jst.JSONInstance(dict), [{1}, (1,), [1]], [{1:2}, undefined])
yield (jst.JSONInstance(dict), [{1}, (1,), [1]], [{1: 2}, undefined])
yield (jst.JSONInstance(dict, allow_undefined=False), [undefined], [])

# Unions and other collections
Expand All @@ -92,11 +93,12 @@ def test_traits(trait, failcases, passcases):
trait._validate(obj, passcase)

for failcase in failcases:
with pytest.raises(T.TraitError) as err:
with pytest.raises(T.TraitError):
trait._validate(obj, failcase)


def test_hastraits_defaults():

class Foo(jst.JSONHasTraits):
_additional_traits = [T.Integer()]
name = T.Unicode()
Expand All @@ -105,14 +107,15 @@ class Foo(jst.JSONHasTraits):
f.set_trait('year', 2000)
assert set(f.trait_names()) == {'name', 'age', 'year'}

with pytest.raises(T.TraitError) as err:
with pytest.raises(T.TraitError):
f.set_trait('foo', 'abc')

with pytest.raises(T.TraitError) as err:
with pytest.raises(T.TraitError):
f.set_trait('age', 'blah')


def test_hastraits_required():

class Foo(jst.JSONHasTraits):
_required_traits = ['name']
name = jst.JSONString()
Expand All @@ -122,23 +125,25 @@ class Foo(jst.JSONHasTraits):
f2 = Foo(age=32)

# contains all required pieces
D = f1.to_dict()
f1.to_dict()

with pytest.raises(T.TraitError) as err:
f2.to_dict()
assert err.match("Required trait 'name' is undefined")


def test_no_defaults():

class Foo(jst.JSONHasTraits):
_additional_traits = False
name = T.Unicode()

with pytest.raises(T.TraitError) as err:
f = Foo(name="Sarah", year=2000)
with pytest.raises(T.TraitError):
Foo(name="Sarah", year=2000)


def test_AnyOfObject():

class Foo(jst.JSONHasTraits):
intval = T.Integer()
flag = T.Bool()
Expand All @@ -155,13 +160,13 @@ class FooBar(jst.AnyOfObject):
FooBar(intval=5, flag=True)

with pytest.raises(T.TraitError):
h = FooBar(strval=666, flag=False)
FooBar(strval=666, flag=False)
with pytest.raises(T.TraitError):
h = FooBar(strval='hello', flag='bad arg')
FooBar(strval='hello', flag='bad arg')
with pytest.raises(T.TraitError):
h = FooBar(intval='bad arg', flag=False)
FooBar(intval='bad arg', flag=False)
with pytest.raises(T.TraitError):
h = FooBar(intval=42, flag='bad arg')
FooBar(intval=42, flag='bad arg')

# Test from_dict
FooBar.from_dict({'strval': 'hello', 'flag': True})
Expand Down Expand Up @@ -198,6 +203,7 @@ def test_to_from_dict_with_defaults():


def test_to_dict_explicit_null():

class MyClass(jst.JSONHasTraits):
bar = jst.JSONString(allow_none=True, allow_undefined=True)

Expand All @@ -207,31 +213,38 @@ class MyClass(jst.JSONHasTraits):


def test_defaults():

class Foo(jst.JSONHasTraits):
arr = jst.JSONArray(jst.JSONString())
val = jst.JSONInstance(dict)

assert Foo().to_dict() == {}


def test_skip():

class Foo(jst.JSONHasTraits):
_skip_on_export = ['baz']
bar = jst.JSONNumber()
baz = jst.JSONNumber()

f = Foo(bar=1, baz=2)
assert f.to_dict() == {'bar': 1}


def test_finalize():

class Foo(jst.JSONHasTraits):
bar = jst.JSONNumber()
bar_times_2 = jst.JSONNumber()
L = jst.JSONArray(jst.JSONString())

def _finalize(self):
self.bar_times_2 = 2 * self.bar
super(Foo, self)._finalize()

f = Foo(bar=4, L=['a', 'b', 'c'])
assert f.to_dict() == {'bar': 4, 'bar_times_2': 8, 'L':['a', 'b', 'c']}
assert f.to_dict() == {'bar': 4, 'bar_times_2': 8, 'L': ['a', 'b', 'c']}


def test_contains():
Expand All @@ -257,12 +270,34 @@ class Bar(jst.JSONHasTraits):
e = jst.JSONArray(jst.JSONInstance(Foo))

D = {'c': [1, 2, 3], 'd': {'a': 5, 'b': 'blah'},
'e':[{'a': 3, 'b': 'foo'}, {'a': 4, 'b': 'bar'}]}
'e': [{'a': 3, 'b': 'foo'}, {'a': 4, 'b': 'bar'}]}
obj = Bar.from_dict(D)
obj2 = eval(obj.to_python())
assert obj2.to_dict() == obj.to_dict() == D

# Make sure there is an error if required traits are missing
foo = Foo(a=4)
with pytest.raises(T.TraitError) as err:
with pytest.raises(T.TraitError):
foo.to_python()


def test_metadata():
class Foo(jst.JSONHasTraits):
bar = jst.JSONInteger()

dct = {'$schema': 'http://myschema.com/schema.json',
'$id': '#/my/context/',
'bar': 4}
metadata = {key:val for key, val in dct.items()
if key.startswith('$')}
original_metadata = Foo._metadata.copy()

# Make sure metadata is properly added
f = Foo.from_dict(dct)
assert f._metadata == metadata

# Make sure metadata is properly exported
assert f.to_dict() == dct

# Make sure above doesn't change Foo's internal class dict
assert Foo._metadata == original_metadata
4 changes: 2 additions & 2 deletions altair/schema/_vegalite_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

# Automatically written by tools/sync_vegalite.py
# Do not modify this manually
vegalite_version = '1.2.1'
vegalite_version = 'v1.2.1'
vegalite_schema_url = 'https://vega.github.io/schema/vega-lite/v1.2.1.json'
Loading

0 comments on commit 387c575

Please sign in to comment.