Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Lib/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@
'[2.0, 1.0]'


Using json.tool from the shell to validate and pretty-print::
Using json from the shell to validate and pretty-print::

$ echo '{"json":"obj"}' | python -m json.tool
$ echo '{"json":"obj"}' | python -m json
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m json.tool
$ echo '{ 1.2:3.4}' | python -m json
Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
"""
__version__ = '2.0.9'
Expand Down
20 changes: 20 additions & 0 deletions Lib/json/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Command-line tool to validate and pretty-print JSON

Usage::

$ echo '{"json":"obj"}' | python -m json
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m json
Expecting property name enclosed in double quotes: line 1 column 3 (char 2)

"""
import json.tool


if __name__ == '__main__':
try:
json.tool.main()
except BrokenPipeError as exc:
raise SystemExit(exc.errno)
119 changes: 67 additions & 52 deletions Lib/json/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,37 +295,40 @@ def _iterencode_list(lst, _current_indent_level):
else:
newline_indent = None
separator = _item_separator
first = True
for value in lst:
if first:
first = False
else:
for i, value in enumerate(lst):
if i:
buf = separator
if isinstance(value, str):
yield buf + _encoder(value)
elif value is None:
yield buf + 'null'
elif value is True:
yield buf + 'true'
elif value is False:
yield buf + 'false'
elif isinstance(value, int):
# Subclasses of int/float may override __repr__, but we still
# want to encode them as integers/floats in JSON. One example
# within the standard library is IntEnum.
yield buf + _intstr(value)
elif isinstance(value, float):
# see comment above for int
yield buf + _floatstr(value)
else:
yield buf
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
try:
if isinstance(value, str):
yield buf + _encoder(value)
elif value is None:
yield buf + 'null'
elif value is True:
yield buf + 'true'
elif value is False:
yield buf + 'false'
elif isinstance(value, int):
# Subclasses of int/float may override __repr__, but we still
# want to encode them as integers/floats in JSON. One example
# within the standard library is IntEnum.
yield buf + _intstr(value)
elif isinstance(value, float):
# see comment above for int
yield buf + _floatstr(value)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
yield buf
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
except GeneratorExit:
raise
except BaseException as exc:
exc.add_note(f'when serializing {type(lst).__name__} item {i}')
raise
if newline_indent is not None:
_current_indent_level -= 1
yield '\n' + _indent * _current_indent_level
Expand Down Expand Up @@ -385,28 +388,34 @@ def _iterencode_dict(dct, _current_indent_level):
yield item_separator
yield _encoder(key)
yield _key_separator
if isinstance(value, str):
yield _encoder(value)
elif value is None:
yield 'null'
elif value is True:
yield 'true'
elif value is False:
yield 'false'
elif isinstance(value, int):
# see comment for int/float in _make_iterencode
yield _intstr(value)
elif isinstance(value, float):
# see comment for int/float in _make_iterencode
yield _floatstr(value)
else:
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
try:
if isinstance(value, str):
yield _encoder(value)
elif value is None:
yield 'null'
elif value is True:
yield 'true'
elif value is False:
yield 'false'
elif isinstance(value, int):
# see comment for int/float in _make_iterencode
yield _intstr(value)
elif isinstance(value, float):
# see comment for int/float in _make_iterencode
yield _floatstr(value)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
if isinstance(value, (list, tuple)):
chunks = _iterencode_list(value, _current_indent_level)
elif isinstance(value, dict):
chunks = _iterencode_dict(value, _current_indent_level)
else:
chunks = _iterencode(value, _current_indent_level)
yield from chunks
except GeneratorExit:
raise
except BaseException as exc:
exc.add_note(f'when serializing {type(dct).__name__} item {key!r}')
raise
if not first and newline_indent is not None:
_current_indent_level -= 1
yield '\n' + _indent * _current_indent_level
Expand Down Expand Up @@ -439,8 +448,14 @@ def _iterencode(o, _current_indent_level):
if markerid in markers:
raise ValueError("Circular reference detected")
markers[markerid] = o
o = _default(o)
yield from _iterencode(o, _current_indent_level)
newobj = _default(o)
try:
yield from _iterencode(newobj, _current_indent_level)
except GeneratorExit:
raise
except BaseException as exc:
exc.add_note(f'when serializing {type(o).__name__} object')
raise
if markers is not None:
del markers[markerid]
return _iterencode
64 changes: 48 additions & 16 deletions Lib/json/tool.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,50 @@
r"""Command-line tool to validate and pretty-print JSON

Usage::

$ echo '{"json":"obj"}' | python -m json.tool
{
"json": "obj"
}
$ echo '{ 1.2:3.4}' | python -m json.tool
Expecting property name enclosed in double quotes: line 1 column 3 (char 2)
"""Command-line tool to validate and pretty-print JSON

See `json.__main__` for a usage example (invocation as
`python -m json.tool` is supported for backwards compatibility).
"""
import argparse
import json
import re
import sys
from _colorize import get_theme, can_colorize


# The string we are colorizing is valid JSON,
# so we can use a looser but simpler regex to match
# the various parts, most notably strings and numbers,
# where the regex given by the spec is much more complex.
_color_pattern = re.compile(r'''
(?P<key>"(\\.|[^"\\])*")(?=:) |
(?P<string>"(\\.|[^"\\])*") |
(?P<number>NaN|-?Infinity|[0-9\-+.Ee]+) |
(?P<boolean>true|false) |
(?P<null>null)
''', re.VERBOSE)

_group_to_theme_color = {
"key": "definition",
"string": "string",
"number": "number",
"boolean": "keyword",
"null": "keyword",
}


def _colorize_json(json_str, theme):
def _replace_match_callback(match):
for group, color in _group_to_theme_color.items():
if m := match.group(group):
return f"{theme[color]}{m}{theme.reset}"
return match.group()

return re.sub(_color_pattern, _replace_match_callback, json_str)


def main():
prog = 'python -m json.tool'
description = ('A simple command line interface for json module '
'to validate and pretty-print JSON objects.')
parser = argparse.ArgumentParser(prog=prog, description=description)
parser = argparse.ArgumentParser(description=description, color=True)
parser.add_argument('infile', nargs='?',
help='a JSON file to be validated or pretty-printed',
default='-')
Expand Down Expand Up @@ -75,9 +100,16 @@ def main():
else:
outfile = open(options.outfile, 'w', encoding='utf-8')
with outfile:
for obj in objs:
json.dump(obj, outfile, **dump_args)
outfile.write('\n')
if can_colorize(file=outfile):
t = get_theme(tty_file=outfile).syntax
for obj in objs:
json_str = json.dumps(obj, **dump_args)
outfile.write(_colorize_json(json_str, t))
outfile.write('\n')
else:
for obj in objs:
json.dump(obj, outfile, **dump_args)
outfile.write('\n')
except ValueError as e:
raise SystemExit(e)

Expand All @@ -86,4 +118,4 @@ def main():
try:
main()
except BrokenPipeError as exc:
sys.exit(exc.errno)
raise SystemExit(exc.errno)
3 changes: 1 addition & 2 deletions Lib/test/test_json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ def test_pyjson(self):
'json.encoder')

class TestCTest(CTest):
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_cjson(self):
self.assertEqual(self.json.scanner.make_scanner.__module__, '_json')
self.assertEqual(self.json.decoder.scanstring.__module__, '_json')
Expand Down
6 changes: 1 addition & 5 deletions Lib/test/test_json/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ def test_float(self):
self.assertIsInstance(rval, float)
self.assertEqual(rval, 1.0)

# TODO: RUSTPYTHON
@unittest.skip("TODO: RUSTPYTHON; called `Result::unwrap()` on an `Err` value: ParseFloatError { kind: Invalid }")
@unittest.skip('TODO: RUSTPYTHON; called `Result::unwrap()` on an `Err` value: ParseFloatError { kind: Invalid }')
def test_nonascii_digits_rejected(self):
# JSON specifies only ascii digits, see gh-125687
for num in ["1\uff10", "0.\uff10", "0e\uff10"]:
Expand Down Expand Up @@ -138,9 +137,6 @@ def test_limit_int(self):
class TestPyDecode(TestDecode, PyTest): pass

class TestCDecode(TestDecode, CTest):
def test_keys_reuse(self):
return super().test_keys_reuse()

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_limit_int(self):
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_json/test_default.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import collections
import unittest # XXX: RUSTPYTHON; importing to be able to skip tests
from test.test_json import PyTest, CTest


Expand All @@ -8,6 +9,26 @@ def test_default(self):
self.dumps(type, default=repr),
self.dumps(repr(type)))

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bad_default(self):
def default(obj):
if obj is NotImplemented:
raise ValueError
if obj is ...:
return NotImplemented
if obj is type:
return collections
return [...]

with self.assertRaises(ValueError) as cm:
self.dumps(type, default=default)
self.assertEqual(cm.exception.__notes__,
['when serializing ellipsis object',
'when serializing list item 0',
'when serializing module object',
'when serializing type object'])

def test_ordereddict(self):
od = collections.OrderedDict(a=1, b=2, c=3, d=4)
od.move_to_end('b')
Expand Down
27 changes: 24 additions & 3 deletions Lib/test/test_json/test_fail.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from test.test_json import PyTest, CTest

import unittest # XXX: RUSTPYTHON; importing to be able to skip tests

from test.test_json import PyTest, CTest

# 2007-10-05
JSONDOCS = [
# https://json.org/JSON_checker/test/fail1.json
Expand Down Expand Up @@ -99,11 +99,32 @@ def test_non_string_keys_dict(self):
'keys must be str, int, float, bool or None, not tuple'):
self.dumps(data)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_not_serializable(self):
import sys
with self.assertRaisesRegex(TypeError,
'Object of type module is not JSON serializable'):
'Object of type module is not JSON serializable') as cm:
self.dumps(sys)
self.assertNotHasAttr(cm.exception, '__notes__')

with self.assertRaises(TypeError) as cm:
self.dumps([1, [2, 3, sys]])
self.assertEqual(cm.exception.__notes__,
['when serializing list item 2',
'when serializing list item 1'])

with self.assertRaises(TypeError) as cm:
self.dumps((1, (2, 3, sys)))
self.assertEqual(cm.exception.__notes__,
['when serializing tuple item 2',
'when serializing tuple item 1'])

with self.assertRaises(TypeError) as cm:
self.dumps({'a': {'b': sys}})
self.assertEqual(cm.exception.__notes__,
["when serializing dict item 'b'",
"when serializing dict item 'a'"])

def test_truncated_input(self):
test_cases = [
Expand Down
Loading
Loading