summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulian Berman <Julian@GrayVines.com>2014-05-18 11:28:45 -0400
committerJulian Berman <Julian@GrayVines.com>2014-05-18 11:28:45 -0400
commit80ec8c0ec2b141d7abe361cdee60f3288fecf5cb (patch)
tree6585526248f80962a8242b904d6ad640d5ff0799
parentfad8a030f357f11fbd027c014f0ed347c38d7328 (diff)
downloadjsonschema-cli.tar.gz
Rework things a bit, remove py.test, allow multiple instances, and a validator.cli
-rw-r--r--jsonschema/_reflect.py155
-rw-r--r--jsonschema/cli.py96
-rw-r--r--jsonschema/compat.py2
-rw-r--r--jsonschema/tests/test_cli.py155
4 files changed, 312 insertions, 96 deletions
diff --git a/jsonschema/_reflect.py b/jsonschema/_reflect.py
new file mode 100644
index 0000000..d09e38f
--- /dev/null
+++ b/jsonschema/_reflect.py
@@ -0,0 +1,155 @@
+# -*- test-case-name: twisted.test.test_reflect -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Standardized versions of various cool and/or strange things that you can do
+with Python's reflection capabilities.
+"""
+
+import sys
+
+from jsonschema.compat import PY3
+
+
+class _NoModuleFound(Exception):
+ """
+ No module was found because none exists.
+ """
+
+
+
+class InvalidName(ValueError):
+ """
+ The given name is not a dot-separated list of Python objects.
+ """
+
+
+
+class ModuleNotFound(InvalidName):
+ """
+ The module associated with the given name doesn't exist and it can't be
+ imported.
+ """
+
+
+
+class ObjectNotFound(InvalidName):
+ """
+ The object associated with the given name doesn't exist and it can't be
+ imported.
+ """
+
+
+
+if PY3:
+ def reraise(exception, traceback):
+ raise exception.with_traceback(traceback)
+else:
+ exec("""def reraise(exception, traceback):
+ raise exception.__class__, exception, traceback""")
+
+reraise.__doc__ = """
+Re-raise an exception, with an optional traceback, in a way that is compatible
+with both Python 2 and Python 3.
+
+Note that on Python 3, re-raised exceptions will be mutated, with their
+C{__traceback__} attribute being set.
+
+@param exception: The exception instance.
+@param traceback: The traceback to use, or C{None} indicating a new traceback.
+"""
+
+
+def _importAndCheckStack(importName):
+ """
+ Import the given name as a module, then walk the stack to determine whether
+ the failure was the module not existing, or some code in the module (for
+ example a dependent import) failing. This can be helpful to determine
+ whether any actual application code was run. For example, to distiguish
+ administrative error (entering the wrong module name), from programmer
+ error (writing buggy code in a module that fails to import).
+
+ @param importName: The name of the module to import.
+ @type importName: C{str}
+ @raise Exception: if something bad happens. This can be any type of
+ exception, since nobody knows what loading some arbitrary code might
+ do.
+ @raise _NoModuleFound: if no module was found.
+ """
+ try:
+ return __import__(importName)
+ except ImportError:
+ excType, excValue, excTraceback = sys.exc_info()
+ while excTraceback:
+ execName = excTraceback.tb_frame.f_globals["__name__"]
+ # in Python 2 execName is None when an ImportError is encountered,
+ # where in Python 3 execName is equal to the importName.
+ if execName is None or execName == importName:
+ reraise(excValue, excTraceback)
+ excTraceback = excTraceback.tb_next
+ raise _NoModuleFound()
+
+
+
+def namedAny(name):
+ """
+ Retrieve a Python object by its fully qualified name from the global Python
+ module namespace. The first part of the name, that describes a module,
+ will be discovered and imported. Each subsequent part of the name is
+ treated as the name of an attribute of the object specified by all of the
+ name which came before it. For example, the fully-qualified name of this
+ object is 'twisted.python.reflect.namedAny'.
+
+ @type name: L{str}
+ @param name: The name of the object to return.
+
+ @raise InvalidName: If the name is an empty string, starts or ends with
+ a '.', or is otherwise syntactically incorrect.
+
+ @raise ModuleNotFound: If the name is syntactically correct but the
+ module it specifies cannot be imported because it does not appear to
+ exist.
+
+ @raise ObjectNotFound: If the name is syntactically correct, includes at
+ least one '.', but the module it specifies cannot be imported because
+ it does not appear to exist.
+
+ @raise AttributeError: If an attribute of an object along the way cannot be
+ accessed, or a module along the way is not found.
+
+ @return: the Python object identified by 'name'.
+ """
+ if not name:
+ raise InvalidName('Empty module name')
+
+ names = name.split('.')
+
+ # if the name starts or ends with a '.' or contains '..', the __import__
+ # will raise an 'Empty module name' error. This will provide a better error
+ # message.
+ if '' in names:
+ raise InvalidName(
+ "name must be a string giving a '.'-separated list of Python "
+ "identifiers, not %r" % (name,))
+
+ topLevelPackage = None
+ moduleNames = names[:]
+ while not topLevelPackage:
+ if moduleNames:
+ trialname = '.'.join(moduleNames)
+ try:
+ topLevelPackage = _importAndCheckStack(trialname)
+ except _NoModuleFound:
+ moduleNames.pop()
+ else:
+ if len(names) == 1:
+ raise ModuleNotFound("No module named %r" % (name,))
+ else:
+ raise ObjectNotFound('%r does not name an object' % (name,))
+
+ obj = topLevelPackage
+ for n in names[1:]:
+ obj = getattr(obj, n)
+
+ return obj
diff --git a/jsonschema/cli.py b/jsonschema/cli.py
index 69ac5af..bdde379 100644
--- a/jsonschema/cli.py
+++ b/jsonschema/cli.py
@@ -1,40 +1,72 @@
+from __future__ import absolute_import
import argparse
import json
import sys
-from . import (
- validate, Draft4Validator, Draft3Validator,
- draft3_format_checker, draft4_format_checker,
+from jsonschema._reflect import namedAny
+from jsonschema.validators import validator_for
+
+
+def _namedAnyWithDefault(name):
+ if "." not in name:
+ name = "jsonschema." + name
+ return namedAny(name)
+
+
+def _json_file(path):
+ with open(path) as file:
+ return json.load(file)
+
+
+parser = argparse.ArgumentParser(
+ description="JSON Schema Validation CLI",
+)
+parser.add_argument(
+ "-i", "--instance",
+ action="append",
+ dest="instances",
+ type=_json_file,
+ help="a path to a JSON instance to validate "
+ "(may be specified multiple times)",
)
-from .validators import validator_for
+parser.add_argument(
+ "-F", "--error-format",
+ default="{error.instance}: {error.message}\n",
+ help="the format to use for each error output message, specified in "
+ "a form suitable for passing to str.format, which will be called "
+ "with 'error' for each error",
+)
+parser.add_argument(
+ "-V", "--validator",
+ type=_namedAnyWithDefault,
+ help="the fully qualified object name of a validator to use, or, for "
+ "validators that are registered with jsonschema, simply the name "
+ "of the class.",
+)
+parser.add_argument(
+ "schema",
+ help="the JSON Schema to validate with",
+ type=_json_file,
+)
+
+
+def parse_args(args):
+ arguments = vars(parser.parse_args(args=args or ["--help"]))
+ if arguments["validator"] is None:
+ arguments["validator"] = validator_for(arguments["schema"])
+ return arguments
def main(args=sys.argv[1:]):
- parser = argparse.ArgumentParser(description='JSON Schema validator')
- parser.add_argument('schema', help='filename of the JSON Schema')
- parser.add_argument('document', help='filename of the JSON document to validate')
- parser.add_argument('--format', help='validate value format', action='store_true')
- args = parser.parse_args(args)
-
- schema = json.load(open(args.schema, 'r'))
- document = json.load(open(args.document, 'r'))
-
- validator = validator_for(schema)
- if args.format:
- if validator == Draft4Validator:
- format_checker = draft4_format_checker
- elif validator == Draft3Validator:
- format_checker = draft3_format_checker
- else:
- raise NotImplementedError("No format validator for %s specified"
- % validator.__name__)
- else:
- format_checker = None
-
- validate(document, schema, validator, format_checker=format_checker)
- # validate raises if the document is invalid, and will show a Traceback to
- # the user. If the document is valid, show a congratulating message.
- print("JSON document is valid.")
-
-if __name__ == '__main__':
- main()
+ sys.exit(run(arguments=parse_args(args=args)))
+
+
+def run(arguments, stdout=sys.stdout, stderr=sys.stderr):
+ error_format = arguments["error_format"]
+ validator = arguments["validator"](schema=arguments["schema"])
+ errored = False
+ for instance in arguments["instances"] or ():
+ for error in validator.iter_errors(instance):
+ stderr.write(error_format.format(error=error))
+ errored = True
+ return errored
diff --git a/jsonschema/compat.py b/jsonschema/compat.py
index e5394f0..6ca49ab 100644
--- a/jsonschema/compat.py
+++ b/jsonschema/compat.py
@@ -11,6 +11,7 @@ PY3 = sys.version_info[0] >= 3
if PY3:
zip = zip
+ from io import StringIO
from urllib.parse import (
unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit
)
@@ -20,6 +21,7 @@ if PY3:
iteritems = operator.methodcaller("items")
else:
from itertools import izip as zip # noqa
+ from StringIO import StringIO
from urlparse import (
urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa
)
diff --git a/jsonschema/tests/test_cli.py b/jsonschema/tests/test_cli.py
index ae930c4..3f43ded 100644
--- a/jsonschema/tests/test_cli.py
+++ b/jsonschema/tests/test_cli.py
@@ -1,78 +1,105 @@
-import StringIO
+from jsonschema import Draft4Validator, ValidationError, cli
+from jsonschema.compat import StringIO
+from jsonschema.tests.compat import mock, unittest
-import pytest
-from .compat import mock, unittest
-from .. import (
- cli, Draft4Validator, Draft3Validator,
- draft3_format_checker, draft4_format_checker,
-)
+def fake_validator(*errors):
+ errors = list(reversed(errors))
-MOCK_SCHEMAS = {
- 'draft3': {"$schema": "http://json-schema.org/draft-03/schema#"},
- 'draft4': {"$schema": "http://json-schema.org/draft-04/schema#"},
-}
+ class FakeValidator(object):
+ def __init__(self, *args, **kwargs):
+ pass
+ def iter_errors(self, instance):
+ if errors:
+ return errors.pop()
+ return []
+ return FakeValidator
-class TestCLI(unittest.TestCase):
- def test_missing_arguments(self):
- with pytest.raises(SystemExit) as e:
- cli.main([])
- @mock.patch('__builtin__.open')
- @mock.patch('jsonschema.cli.validate')
- def test_filename_argument_order(self, validate, open_):
- def mock_file(filename, mode):
- return StringIO.StringIO('{"filename": "%s"}' % filename)
- open_.side_effect = mock_file
+class TestParser(unittest.TestCase):
- cli.main(['document.json', 'schema.json'])
+ FakeValidator = fake_validator()
- open_.assert_has_calls([mock.call('document.json', 'r'),
- mock.call('schema.json', 'r')],
- any_order=True)
- self.assertEqual(open_.call_count, 2)
+ def setUp(self):
+ self.open = mock.mock_open(read_data='{}')
+ patch = mock.patch.object(cli, "open", self.open, create=True)
+ patch.start()
+ self.addCleanup(patch.stop)
- validate.assert_called_once_with({'filename': 'schema.json'},
- {'filename': 'document.json'},
- Draft4Validator,
- format_checker=None)
+ def test_find_validator_by_fully_qualified_object_name(self):
+ arguments = cli.parse_args(
+ [
+ "--validator",
+ "jsonschema.tests.test_cli.TestParser.FakeValidator",
+ "--instance", "foo.json",
+ "schema.json",
+ ]
+ )
+ self.assertIs(arguments["validator"], self.FakeValidator)
- @mock.patch('__builtin__.open')
- @mock.patch('jsonschema.cli.json.load')
- @mock.patch('jsonschema.cli.validate')
- def test_raise_exception(self, validate, json_load, open_):
- validate.side_effect = Exception('Did not validate correctly')
- with pytest.raises(Exception) as e:
- cli.main([None, None])
- self.assertEqual(e.exconly(), "Exception: Did not validate correctly")
+ def test_find_validator_in_jsonschema(self):
+ arguments = cli.parse_args(
+ [
+ "--validator", "Draft4Validator",
+ "--instance", "foo.json",
+ "schema.json",
+ ]
+ )
+ self.assertIs(arguments["validator"], Draft4Validator)
- @mock.patch('__builtin__.open')
- @mock.patch('jsonschema.cli.json.load')
- @mock.patch('jsonschema.cli.validate')
- def test_format(self, validate, json_load, open_):
- schema = {"$schema": "http://json-schema.org/draft-04/schema#"}
- json_load.return_value = schema
- cli.main([None, None])
- validate.assert_called_once_with(schema, schema, Draft4Validator,
- format_checker=None)
- validate.reset_mock()
- cli.main([None, None, '--format'])
- validate.assert_called_once_with(schema, schema, Draft4Validator,
- format_checker=draft4_format_checker)
+class TestCLI(unittest.TestCase):
+ def test_successful_validation(self):
+ stdout, stderr = StringIO(), StringIO()
+ exit_code = cli.run(
+ {
+ "validator" : fake_validator(),
+ "schema" : {},
+ "instances" : [1],
+ "error_format" : "{error.message}",
+ },
+ stdout=stdout,
+ stderr=stderr,
+ )
+ self.assertFalse(stdout.getvalue())
+ self.assertFalse(stderr.getvalue())
+ self.assertEqual(exit_code, 0)
- @mock.patch('__builtin__.open')
- @mock.patch('jsonschema.cli.json.load')
- @mock.patch('jsonschema.cli.validate')
- def test_draft3(self, validate, json_load, open_):
- schema = {"$schema": "http://json-schema.org/draft-03/schema#"}
- json_load.return_value = schema
+ def test_unsuccessful_validation(self):
+ error = ValidationError("I am an error!", instance=1)
+ stdout, stderr = StringIO(), StringIO()
+ exit_code = cli.run(
+ {
+ "validator" : fake_validator([error]),
+ "schema" : {},
+ "instances" : [1],
+ "error_format" : "{error.instance} - {error.message}",
+ },
+ stdout=stdout,
+ stderr=stderr,
+ )
+ self.assertFalse(stdout.getvalue())
+ self.assertEqual(stderr.getvalue(), "1 - I am an error!")
+ self.assertEqual(exit_code, 1)
- cli.main([None, None])
- validate.assert_called_once_with(schema, schema, Draft3Validator,
- format_checker=None)
- validate.reset_mock()
- cli.main([None, None, '--format'])
- validate.assert_called_once_with(schema, schema, Draft3Validator,
- format_checker=draft3_format_checker)
+ def test_unsuccessful_validation_multiple_instances(self):
+ first_errors = [
+ ValidationError("9", instance=1),
+ ValidationError("8", instance=1),
+ ]
+ second_errors = [ValidationError("7", instance=2)]
+ stdout, stderr = StringIO(), StringIO()
+ exit_code = cli.run(
+ {
+ "validator" : fake_validator(first_errors, second_errors),
+ "schema" : {},
+ "instances" : [1, 2],
+ "error_format" : "{error.instance} - {error.message}\t",
+ },
+ stdout=stdout,
+ stderr=stderr,
+ )
+ self.assertFalse(stdout.getvalue())
+ self.assertEqual(stderr.getvalue(), "1 - 9\t1 - 8\t2 - 7\t")
+ self.assertEqual(exit_code, 1)