10000 bpo-33927: Add support for same infile and outfile to json.tool by remilapeyre · Pull Request #7865 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-33927: Add support for same infile and outfile to json.tool #7865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Prev Previous commit
Next Next commit
Merge remote-tracking branch 'origin/master' into bugfix/json.tool-sa…
…me-infile-outfile
  • Loading branch information
remilapeyre committed May 28, 2020
commit 90b295c06de8f5fa0b35cb9ca13a5e0eac3e6a0c
54 changes: 37 additions & 17 deletions Lib/json/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,56 +29,76 @@ def _open_outfile(outfile, parser):
except IOError as e:
parser.error(f"can't open '{outfile}': {str(e)}")

def _write(outfile, objs, sort_keys):
def _write(outfile, objs, **kwargs):
for obj in objs:
json.dump(obj, outfile, sort_keys=sort_keys, indent=4)
json.dump(obj, outfile, **kwargs)
outfile.write('\n')

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.add_argument('infile', nargs='?', type=argparse.FileType(), default=sys.stdin,
help='a JSON file to be validated or pretty-printed')
parser.add_argument('infile', nargs='?',
type=argparse.FileType(encoding="utf-8"),
help='a JSON file to be validated or pretty-printed',
default=sys.stdin)
parser.add_argument('outfile', nargs='?', default='-',
help='write the output of infile to outfile')
parser.add_argument('--sort-keys', action='store_true', default=False,
help='sort the output of dictionaries alphabetically by key')
parser.add_argument('--no-ensure-ascii', dest='ensure_ascii', action='store_false',
help='disable escaping of non-ASCII characters')
parser.add_argument('--json-lines', action='store_true', default=False,
help='parse input using the jsonlines format')
help='parse input using the JSON Lines format. '
'Use with --no-indent or --compact to produce valid JSON Lines output.')
group = parser.add_mutually_exclusive_group()
group.add_argument('--indent', default=4, type=int,
help='separate items with newlines and use this number '
'of spaces for indentation')
group.add_argument('--tab', action='store_const', dest='indent',
const='\t', help='separate items with newlines and use '
'tabs for indentation')
group.add_argument('--no-indent', action='store_const', dest='indent',
const=None,
help='separate items with spaces rather than newlines')
group.add_argument('--compact', action='store_true',
help='suppress all whitespace separation (most compact)')
parser.add_argument('-i', '--in-place', action='store_true', default=False,
help='edit the file in-place')
options = parser.parse_args()


if options.in_place:
if options.outfile != '-':
parser.error('outfile cannot be set when -i / --in-place is used')
if options.infile is sys.stdin:
parser.error('infile must be set when -i / --in-place is used')
options.outfile = options.infile.name

outfile = options.outfile
infile = options.infile
sort_keys = options.sort_keys
json_lines = options.json_lines
dump_args = {
'sort_keys': options.sort_keys,
'indent': options.indent,
'ensure_ascii': options.ensure_ascii,
}
if options.compact:
dump_args['indent'] = None
dump_args['separators'] = ',', ':'

if options.in_place:
with infile:
objs = tuple(_read(infile, json_lines))
with options.infile as infile:
objs = tuple(_read(infile, options.json_lines))

outfile = _open_outfile(outfile, parser)
outfile = _open_outfile(options.outfile, parser)

with outfile:
_write(outfile, objs, sort_keys)
_write(outfile, objs, **dump_args)

else:
outfile = _open_outfile(outfile, parser)
with infile, outfile:
objs = _read(infile, json_lines)
_write(outfile, objs, sort_keys)
outfile = _open_outfile(options.outfile, parser)
with options.infile as infile, outfile:
objs = _read(infile, options.json_lines)
_write(outfile, objs, **dump_args)

if __name__ == '__main__':
try:
Expand Down
90 changes: 79 additions & 11 deletions Lib/test/test_json/test_tool.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import io
import errno
import os
import sys
import textwrap
import unittest
import subprocess
import io
import types
from subprocess import Popen, PIPE

from test import support
from test.support.script_helper import assert_python_ok, assert_python_failure
from unittest import mock
Expand Down Expand Up @@ -92,12 +94,10 @@ def test_stdin_stdout(self):
self.assertEqual(process.stderr, '')

def _create_infile(self, data=None):
if data is None:
data = self.data
infile = support.TESTFN
with open(infile, "w", encoding="utf-8") as fp:
self.addCleanup(os.remove, infile)
fp.write(data)
fp.write(data or self.data)
return infile

def test_infile_stdout(self):
Expand Down Expand Up @@ -146,12 +146,9 @@ def test_unavailable_outfile(self):

def test_jsonlines(self):
args = sys.executable, '-m', 'json.tool', '--json-lines'
proc = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
out, err = proc.communicate(self.jsonlines_raw.encode())
proc.wait()
self.assertEqual(proc.returncode, 0)
self.assertEqual(err, b'')
self.assertEqual(out.splitlines(), self.jsonlines_expect.encode().splitlines())
process = subprocess.run(args, input=self.jsonlines_raw, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, self.jsonlines_expect)
self.assertEqual(process.stderr, '')

def test_help_flag(self):
rc, out, err = assert_python_ok('-m', 'json.tool', '-h')
Expand Down Expand Up @@ -213,6 +210,77 @@ def test_no_fd_leak_same_infile_outfile(self):
self.assertEqual(len(opened), 2)
self.assertEqual(len(opened), 2)

def test_indent(self):
input_ = '[1, 2]'
expect = textwrap.dedent('''\
[
1,
2
]
''')
args = sys.executable, '-m', 'json.tool', '--indent', '2'
process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, expect)
self.assertEqual(process.stderr, '')

def test_no_indent(self):
input_ = '[1,\n2]'
expect = '[1, 2]\n'
args = sys.executable, '-m', 'json.tool', '--no-indent'
process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, expect)
self.assertEqual(process.stderr, '')

def test_tab(self):
input_ = '[1, 2]'
expect = '[\n\t1,\n\t2\n]\n'
args = sys.executable, '-m', 'json.tool', '--tab'
process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, expect)
self.assertEqual(process.stderr, '')

def test_compact(self):
input_ = '[ 1 ,\n 2]'
expect = '[1,2]\n'
args = sys.executable, '-m', 'json.tool', '--compact'
process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True)
self.assertEqual(process.stdout, expect)
self.assertEqual(process.stderr, '')

def test_no_ensure_ascii_flag(self):
infile = self._create_infile('{"key":"💩"}')
outfile = support.TESTFN + '.out'
self.addCleanup(os.remove, outfile)
assert_python_ok('-m', 'json.tool', '--no-ensure-ascii', infile, outfile)
with open(outfile, "rb") as f:
lines = f.read().splitlines()
# asserting utf-8 encoded output file
expected = [b'{', b' "key": "\xf0\x9f\x92\xa9"', b"}"]
self.assertEqual(lines, expected)

def test_ensure_ascii_default(self):
infile = self._create_infile('{"key":"💩"}')
outfile = support.TESTFN + '.out'
self.addCleanup(os.remove, outfile)
assert_python_ok('-m', 'json.tool', infile, outfile)
with open(outfile, "rb") as f:
lines = f.read().splitlines()
# asserting an ascii encoded output file
expected = [b'{', rb' "key": "\ud83d\udca9"', b"}"]
self.assertEqual(lines, expected)

@unittest.skipIf(sys.platform =="win32", "The test is failed with ValueError on Windows")
def test_broken_pipe_error(self):
cmd = [sys.executable, '-m', 'json.tool']
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
# bpo-39828: Closing before json.tool attempts to write into stdout.
proc.stdout.close()
proc.communicate(b'"{}"')
self.assertEqual(proc.returncode, errno.EPIPE)


def mock_open():
closed = []
opened = []
Expand Down
You are viewing a condensed version of this merge commit. You can view the full changes here.
0