Skip to content

Commit 1976689

Browse files
committed
Merge branch 'cli'
* cli: Rework things a bit, remove py.test, allow multiple instances, and a validator. Add support for -m. Formatting. Fixed issue where arguments were not parsed correctly Dropped use of UTF-8 character √ Added unit tests for CLI Added CLI format checking Fixed #134 -- Added CLI interface Support 3.4 For my sanity, just depend on setuptools.
2 parents 202157c + 80ec8c0 commit 1976689

File tree

6 files changed

+339
-5
lines changed

6 files changed

+339
-5
lines changed

jsonschema/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from jsonschema.cli import main
2+
main()

jsonschema/_reflect.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# -*- test-case-name: twisted.test.test_reflect -*-
2+
# Copyright (c) Twisted Matrix Laboratories.
3+
# See LICENSE for details.
4+
5+
"""
6+
Standardized versions of various cool and/or strange things that you can do
7+
with Python's reflection capabilities.
8+
"""
9+
10+
import sys
11+
12+
from jsonschema.compat import PY3
13+
14+
15+
class _NoModuleFound(Exception):
16+
"""
17+
No module was found because none exists.
18+
"""
19+
20+
21+
22+
class InvalidName(ValueError):
23+
"""
24+
The given name is not a dot-separated list of Python objects.
25+
"""
26+
27+
28+
29+
class ModuleNotFound(InvalidName):
30+
"""
31+
The module associated with the given name doesn't exist and it can't be
32+
imported.
33+
"""
34+
35+
36+
37+
class ObjectNotFound(InvalidName):
38+
"""
39+
The object associated with the given name doesn't exist and it can't be
40+
imported.
41+
"""
42+
43+
44+
45+
if PY3:
46+
def reraise(exception, traceback):
47+
raise exception.with_traceback(traceback)
48+
else:
49+
exec("""def reraise(exception, traceback):
50+
raise exception.__class__, exception, traceback""")
51+
52+
reraise.__doc__ = """
53+
Re-raise an exception, with an optional traceback, in a way that is compatible
54+
with both Python 2 and Python 3.
55+
56+
Note that on Python 3, re-raised exceptions will be mutated, with their
57+
C{__traceback__} attribute being set.
58+
59+
@param exception: The exception instance.
60+
@param traceback: The traceback to use, or C{None} indicating a new traceback.
61+
"""
62+
63+
64+
def _importAndCheckStack(importName):
65+
"""
66+
Import the given name as a module, then walk the stack to determine whether
67+
the failure was the module not existing, or some code in the module (for
68+
example a dependent import) failing. This can be helpful to determine
69+
whether any actual application code was run. For example, to distiguish
70+
administrative error (entering the wrong module name), from programmer
71+
error (writing buggy code in a module that fails to import).
72+
73+
@param importName: The name of the module to import.
74+
@type importName: C{str}
75+
@raise Exception: if something bad happens. This can be any type of
76+
exception, since nobody knows what loading some arbitrary code might
77+
do.
78+
@raise _NoModuleFound: if no module was found.
79+
"""
80+
try:
81+
return __import__(importName)
82+
except ImportError:
83+
excType, excValue, excTraceback = sys.exc_info()
84+
while excTraceback:
85+
execName = excTraceback.tb_frame.f_globals["__name__"]
86+
# in Python 2 execName is None when an ImportError is encountered,
87+
# where in Python 3 execName is equal to the importName.
88+
if execName is None or execName == importName:
89+
reraise(excValue, excTraceback)
90+
excTraceback = excTraceback.tb_next
91+
raise _NoModuleFound()
92+
93+
94+
95+
def namedAny(name):
96+
"""
97+
Retrieve a Python object by its fully qualified name from the global Python
98+
module namespace. The first part of the name, that describes a module,
99+
will be discovered and imported. Each subsequent part of the name is
100+
treated as the name of an attribute of the object specified by all of the
101+
name which came before it. For example, the fully-qualified name of this
102+
object is 'twisted.python.reflect.namedAny'.
103+
104+
@type name: L{str}
105+
@param name: The name of the object to return.
106+
107+
@raise InvalidName: If the name is an empty string, starts or ends with
108+
a '.', or is otherwise syntactically incorrect.
109+
110+
@raise ModuleNotFound: If the name is syntactically correct but the
111+
module it specifies cannot be imported because it does not appear to
112+
exist.
113+
114+
@raise ObjectNotFound: If the name is syntactically correct, includes at
115+
least one '.', but the module it specifies cannot be imported because
116+
it does not appear to exist.
117+
118+
@raise AttributeError: If an attribute of an object along the way cannot be
119+
accessed, or a module along the way is not found.
120+
121+
@return: the Python object identified by 'name'.
122+
"""
123+
if not name:
124+
raise InvalidName('Empty module name')
125+
126+
names = name.split('.')
127+
128+
# if the name starts or ends with a '.' or contains '..', the __import__
129+
# will raise an 'Empty module name' error. This will provide a better error
130+
# message.
131+
if '' in names:
132+
raise InvalidName(
133+
"name must be a string giving a '.'-separated list of Python "
134+
"identifiers, not %r" % (name,))
135+
136+
topLevelPackage = None
137+
moduleNames = names[:]
138+
while not topLevelPackage:
139+
if moduleNames:
140+
trialname = '.'.join(moduleNames)
141+
try:
142+
topLevelPackage = _importAndCheckStack(trialname)
143+
except _NoModuleFound:
144+
moduleNames.pop()
145+
else:
146+
if len(names) == 1:
147+
raise ModuleNotFound("No module named %r" % (name,))
148+
else:
149+
raise ObjectNotFound('%r does not name an object' % (name,))
150+
151+
obj = topLevelPackage
152+
for n in names[1:]:
153+
obj = getattr(obj, n)
154+
155+
return obj

jsonschema/cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import absolute_import
2+
import argparse
3+
import json
4+
import sys
5+
6+
from jsonschema._reflect import namedAny
7+
from jsonschema.validators import validator_for
8+
9+
10+
def _namedAnyWithDefault(name):
11+
if "." not in name:
12+
name = "jsonschema." + name
13+
return namedAny(name)
14+
15+
16+
def _json_file(path):
17+
with open(path) as file:
18+
return json.load(file)
19+
20+
21+
parser = argparse.ArgumentParser(
22+
description="JSON Schema Validation CLI",
23+
)
24+
parser.add_argument(
25+
"-i", "--instance",
26+
action="append",
27+
dest="instances",
28+
type=_json_file,
29+
help="a path to a JSON instance to validate "
30+
"(may be specified multiple times)",
31+
)
32+
parser.add_argument(
33+
"-F", "--error-format",
34+
default="{error.instance}: {error.message}\n",
35+
help="the format to use for each error output message, specified in "
36+
"a form suitable for passing to str.format, which will be called "
37+
"with 'error' for each error",
38+
)
39+
parser.add_argument(
40+
"-V", "--validator",
41+
type=_namedAnyWithDefault,
42+
help="the fully qualified object name of a validator to use, or, for "
43+
"validators that are registered with jsonschema, simply the name "
44+
"of the class.",
45+
)
46+
parser.add_argument(
47+
"schema",
48+
help="the JSON Schema to validate with",
49+
type=_json_file,
50+
)
51+
52+
53+
def parse_args(args):
54+
arguments = vars(parser.parse_args(args=args or ["--help"]))
55+
if arguments["validator"] is None:
56+
arguments["validator"] = validator_for(arguments["schema"])
57+
return arguments
58+
59+
60+
def main(args=sys.argv[1:]):
61+
sys.exit(run(arguments=parse_args(args=args)))
62+
63+
64+
def run(arguments, stdout=sys.stdout, stderr=sys.stderr):
65+
error_format = arguments["error_format"]
66+
validator = arguments["validator"](schema=arguments["schema"])
67+
errored = False
68+
for instance in arguments["instances"] or ():
69+
for error in validator.iter_errors(instance):
70+
stderr.write(error_format.format(error=error))
71+
errored = True
72+
return errored

jsonschema/compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
if PY3:
1313
zip = zip
14+
from io import StringIO
1415
from urllib.parse import (
1516
unquote, urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit
1617
)
@@ -20,6 +21,7 @@
2021
iteritems = operator.methodcaller("items")
2122
else:
2223
from itertools import izip as zip # noqa
24+
from StringIO import StringIO
2325
from urlparse import (
2426
urljoin, urlunsplit, SplitResult, urlsplit as _urlsplit # noqa
2527
)

jsonschema/tests/test_cli.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from jsonschema import Draft4Validator, ValidationError, cli
2+
from jsonschema.compat import StringIO
3+
from jsonschema.tests.compat import mock, unittest
4+
5+
6+
def fake_validator(*errors):
7+
errors = list(reversed(errors))
8+
9+
class FakeValidator(object):
10+
def __init__(self, *args, **kwargs):
11+
pass
12+
13+
def iter_errors(self, instance):
14+
if errors:
15+
return errors.pop()
16+
return []
17+
return FakeValidator
18+
19+
20+
class TestParser(unittest.TestCase):
21+
22+
FakeValidator = fake_validator()
23+
24+
def setUp(self):
25+
self.open = mock.mock_open(read_data='{}')
26+
patch = mock.patch.object(cli, "open", self.open, create=True)
27+
patch.start()
28+
self.addCleanup(patch.stop)
29+
30+
def test_find_validator_by_fully_qualified_object_name(self):
31+
arguments = cli.parse_args(
32+
[
33+
"--validator",
34+
"jsonschema.tests.test_cli.TestParser.FakeValidator",
35+
"--instance", "foo.json",
36+
"schema.json",
37+
]
38+
)
39+
self.assertIs(arguments["validator"], self.FakeValidator)
40+
41+
def test_find_validator_in_jsonschema(self):
42+
arguments = cli.parse_args(
43+
[
44+
"--validator", "Draft4Validator",
45+
"--instance", "foo.json",
46+
"schema.json",
47+
]
48+
)
49+
self.assertIs(arguments["validator"], Draft4Validator)
50+
51+
52+
class TestCLI(unittest.TestCase):
53+
def test_successful_validation(self):
54+
stdout, stderr = StringIO(), StringIO()
55+
exit_code = cli.run(
56+
{
57+
"validator" : fake_validator(),
58+
"schema" : {},
59+
"instances" : [1],
60+
"error_format" : "{error.message}",
61+
},
62+
stdout=stdout,
63+
stderr=stderr,
64+
)
65+
self.assertFalse(stdout.getvalue())
66+
self.assertFalse(stderr.getvalue())
67+
self.assertEqual(exit_code, 0)
68+
69+
def test_unsuccessful_validation(self):
70+
error = ValidationError("I am an error!", instance=1)
71+
stdout, stderr = StringIO(), StringIO()
72+
exit_code = cli.run(
73+
{
74+
"validator" : fake_validator([error]),
75+
"schema" : {},
76+
"instances" : [1],
77+
"error_format" : "{error.instance} - {error.message}",
78+
},
79+
stdout=stdout,
80+
stderr=stderr,
81+
)
82+
self.assertFalse(stdout.getvalue())
83+
self.assertEqual(stderr.getvalue(), "1 - I am an error!")
84+
self.assertEqual(exit_code, 1)
85+
86+
def test_unsuccessful_validation_multiple_instances(self):
87+
first_errors = [
88+
ValidationError("9", instance=1),
89+
ValidationError("8", instance=1),
90+
]
91+
second_errors = [ValidationError("7", instance=2)]
92+
stdout, stderr = StringIO(), StringIO()
93+
exit_code = cli.run(
94+
{
95+
"validator" : fake_validator(first_errors, second_errors),
96+
"schema" : {},
97+
"instances" : [1, 2],
98+
"error_format" : "{error.instance} - {error.message}\t",
99+
},
100+
stdout=stdout,
101+
stderr=stderr,
102+
)
103+
self.assertFalse(stdout.getvalue())
104+
self.assertEqual(stderr.getvalue(), "1 - 9\t1 - 8\t2 - 7\t")
105+
self.assertEqual(exit_code, 1)

setup.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
try:
2-
from setuptools import setup
3-
except ImportError:
4-
from distutils.core import setup
1+
from setuptools import setup
52

63
from jsonschema import __version__
74

@@ -19,7 +16,7 @@
1916
"Programming Language :: Python :: 2.6",
2017
"Programming Language :: Python :: 2.7",
2118
"Programming Language :: Python :: 3",
22-
"Programming Language :: Python :: 3.3",
19+
"Programming Language :: Python :: 3.4",
2320
"Programming Language :: Python :: Implementation :: CPython",
2421
"Programming Language :: Python :: Implementation :: PyPy",
2522
]
@@ -36,4 +33,5 @@
3633
license="MIT",
3734
long_description=long_description,
3835
url="http://github.com/Julian/jsonschema",
36+
entry_points={"console_scripts": ["jsonschema = jsonschema.cli:main"]},
3937
)

0 commit comments

Comments
 (0)