From 1226315e2df0d4229558734d5f0d50f1386a025e Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 31 Jul 2024 17:35:25 -0700 Subject: [PATCH 1/4] Rename json.tool to json.__main__ --- Lib/json/{tool.py => __main__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lib/json/{tool.py => __main__.py} (100%) diff --git a/Lib/json/tool.py b/Lib/json/__main__.py similarity index 100% rename from Lib/json/tool.py rename to Lib/json/__main__.py From 7ce95d21886c7ad5278c07c1a20cda5bebab4731 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 31 Jul 2024 17:36:40 -0700 Subject: [PATCH 2/4] Make json.tool delegate to json.__main__ --- Lib/json/tool.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Lib/json/tool.py diff --git a/Lib/json/tool.py b/Lib/json/tool.py new file mode 100644 index 00000000000000..e46a9bb03820c0 --- /dev/null +++ b/Lib/json/tool.py @@ -0,0 +1,10 @@ +import sys + +from .__main__ import main + + +if __name__ == '__main__': + try: + main() + except BrokenPipeError as exc: + sys.exit(exc.errno) From ae4ca62346c690e1c6aaf1ccfed37069984b5d67 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 31 Jul 2024 18:32:16 -0700 Subject: [PATCH 3/4] Note json.tool deprecation --- Lib/json/tool.py | 9 ++++- Lib/test/test_json/test_tool.py | 71 ++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index e46a9bb03820c0..fb9e4592b411c2 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -1,6 +1,13 @@ import sys +import warnings -from .__main__ import main +from . import __main__ + + +def main(): + warnings.warn('The json.tool module is deprecated', + DeprecationWarning, stacklevel=2) + __main__.main() if __name__ == '__main__': diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 2b63810d53981e..fc9149dbd8d363 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -11,7 +11,7 @@ @support.requires_subprocess() -class TestTool(unittest.TestCase): +class TestMain(unittest.TestCase): data = """ [["blorpie"],[ "whoops" ] , [ @@ -19,6 +19,9 @@ class TestTool(unittest.TestCase): "i-vhbjkhnth", {"nifty":87}, {"morefield" :\tfalse,"field" :"yes"} ] """ + module = 'json' + env_vars = {} + warnings_expected = False expect_without_sort_keys = textwrap.dedent("""\ [ @@ -87,10 +90,11 @@ class TestTool(unittest.TestCase): """) def test_stdin_stdout(self): - args = sys.executable, '-m', 'json.tool' + args = sys.executable, '-m', self.module process = subprocess.run(args, input=self.data, capture_output=True, text=True, check=True) self.assertEqual(process.stdout, self.expect) - self.assertEqual(process.stderr, '') + if not self.warnings_expected: + self.assertEqual(process.stderr, '') def _create_infile(self, data=None): infile = os_helper.TESTFN @@ -101,7 +105,7 @@ def _create_infile(self, data=None): def test_infile_stdout(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', 'json.tool', infile) + rc, out, err = assert_python_ok('-m', self.module, infile, **self.env_vars) self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) self.assertEqual(err, b'') @@ -115,7 +119,7 @@ def test_non_ascii_infile(self): ''').encode() infile = self._create_infile(data) - rc, out, err = assert_python_ok('-m', 'json.tool', infile) + rc, out, err = assert_python_ok('-m', self.module, infile, **self.env_vars) self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), expect.splitlines()) @@ -124,7 +128,7 @@ def test_non_ascii_infile(self): def test_infile_outfile(self): infile = self._create_infile() outfile = os_helper.TESTFN + '.out' - rc, out, err = assert_python_ok('-m', 'json.tool', infile, outfile) + rc, out, err = assert_python_ok('-m', self.module, infile, outfile, **self.env_vars) self.addCleanup(os.remove, outfile) with open(outfile, "r", encoding="utf-8") as fp: self.assertEqual(fp.read(), self.expect) @@ -134,7 +138,7 @@ def test_infile_outfile(self): def test_writing_in_place(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', 'json.tool', infile, infile) + rc, out, err = assert_python_ok('-m', self.module, infile, infile, **self.env_vars) with open(infile, "r", encoding="utf-8") as fp: self.assertEqual(fp.read(), self.expect) self.assertEqual(rc, 0) @@ -142,20 +146,21 @@ def test_writing_in_place(self): self.assertEqual(err, b'') def test_jsonlines(self): - args = sys.executable, '-m', 'json.tool', '--json-lines' + args = sys.executable, '-m', self.module, '--json-lines' 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, '') + if not self.warnings_expected: + self.assertEqual(process.stderr, '') def test_help_flag(self): - rc, out, err = assert_python_ok('-m', 'json.tool', '-h') + rc, out, err = assert_python_ok('-m', self.module, '-h', **self.env_vars) self.assertEqual(rc, 0) self.assertTrue(out.startswith(b'usage: ')) self.assertEqual(err, b'') def test_sort_keys_flag(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', 'json.tool', '--sort-keys', infile) + rc, out, err = assert_python_ok('-m', self.module, '--sort-keys', infile, **self.env_vars) self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), self.expect_without_sort_keys.encode().splitlines()) @@ -169,40 +174,44 @@ def test_indent(self): 2 ] ''') - args = sys.executable, '-m', 'json.tool', '--indent', '2' + args = sys.executable, '-m', self.module, '--indent', '2' process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) self.assertEqual(process.stdout, expect) - self.assertEqual(process.stderr, '') + if not self.warnings_expected: + self.assertEqual(process.stderr, '') def test_no_indent(self): input_ = '[1,\n2]' expect = '[1, 2]\n' - args = sys.executable, '-m', 'json.tool', '--no-indent' + args = sys.executable, '-m', self.module, '--no-indent' process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) self.assertEqual(process.stdout, expect) - self.assertEqual(process.stderr, '') + if not self.warnings_expected: + 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' + args = sys.executable, '-m', self.module, '--tab' process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) self.assertEqual(process.stdout, expect) - self.assertEqual(process.stderr, '') + if not self.warnings_expected: + self.assertEqual(process.stderr, '') def test_compact(self): input_ = '[ 1 ,\n 2]' expect = '[1,2]\n' - args = sys.executable, '-m', 'json.tool', '--compact' + args = sys.executable, '-m', self.module, '--compact' process = subprocess.run(args, input=input_, capture_output=True, text=True, check=True) self.assertEqual(process.stdout, expect) - self.assertEqual(process.stderr, '') + if not self.warnings_expected: + self.assertEqual(process.stderr, '') def test_no_ensure_ascii_flag(self): infile = self._create_infile('{"key":"💩"}') outfile = os_helper.TESTFN + '.out' self.addCleanup(os.remove, outfile) - assert_python_ok('-m', 'json.tool', '--no-ensure-ascii', infile, outfile) + assert_python_ok('-m', self.module, '--no-ensure-ascii', infile, outfile, **self.env_vars) with open(outfile, "rb") as f: lines = f.read().splitlines() # asserting utf-8 encoded output file @@ -213,7 +222,7 @@ def test_ensure_ascii_default(self): infile = self._create_infile('{"key":"💩"}') outfile = os_helper.TESTFN + '.out' self.addCleanup(os.remove, outfile) - assert_python_ok('-m', 'json.tool', infile, outfile) + assert_python_ok('-m', self.module, infile, outfile, **self.env_vars) with open(outfile, "rb") as f: lines = f.read().splitlines() # asserting an ascii encoded output file @@ -222,11 +231,27 @@ def test_ensure_ascii_default(self): @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'] + cmd = [sys.executable, '-m', self.module] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) - # bpo-39828: Closing before json.tool attempts to write into stdout. + # bpo-39828: Closing before json attempts to write into stdout. proc.stdout.close() proc.communicate(b'"{}"') self.assertEqual(proc.returncode, errno.EPIPE) + + +@support.requires_subprocess() +class TestTool(TestMain): + module = 'json.tool' + env_vars = {'PYTHONWARNINGS': 'ignore'} + warnings_expected = True + + def test_with_warnings(self): + import json.tool + filename = json.tool.__file__ + infile = self._create_infile() + rc, out, err = assert_python_ok('-m', self.module, infile) + self.assertEqual(rc, 0) + self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) + self.assertEqual(err.decode(), f'{filename}:15: DeprecationWarning: The json.tool module is deprecated\n main()\n') From 5600afa497024989bad7a2a3ce0b5eb15b9d2541 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Fri, 2 Aug 2024 13:37:15 -0700 Subject: [PATCH 4/4] Move main function from json.__main__ to json.tool --- Lib/json/__main__.py | 76 ++------------------------------- Lib/json/tool.py | 72 ++++++++++++++++++++++++++++--- Lib/test/test_json/test_tool.py | 3 +- 3 files changed, 73 insertions(+), 78 deletions(-) diff --git a/Lib/json/__main__.py b/Lib/json/__main__.py index fdfc3372bcca02..7d2dc2ff02e32d 100644 --- a/Lib/json/__main__.py +++ b/Lib/json/__main__.py @@ -2,88 +2,20 @@ Usage:: - $ 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) """ -import argparse -import json +import json.tool import sys -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='?', - help='a JSON file to be validated or pretty-printed', - default='-') - parser.add_argument('outfile', nargs='?', - help='write the output of infile to outfile', - default=None) - 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 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)') - options = parser.parse_args() - - 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'] = ',', ':' - - try: - if options.infile == '-': - infile = sys.stdin - else: - infile = open(options.infile, encoding='utf-8') - try: - if options.json_lines: - objs = (json.loads(line) for line in infile) - else: - objs = (json.load(infile),) - finally: - if infile is not sys.stdin: - infile.close() - - if options.outfile is None: - outfile = sys.stdout - else: - outfile = open(options.outfile, 'w', encoding='utf-8') - with outfile: - for obj in objs: - json.dump(obj, outfile, **dump_args) - outfile.write('\n') - except ValueError as e: - raise SystemExit(e) - - if __name__ == '__main__': try: - main() + json.tool.main() except BrokenPipeError as exc: sys.exit(exc.errno) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index fb9e4592b411c2..615343acc4e729 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -1,16 +1,78 @@ +import argparse +import json import sys import warnings -from . import __main__ - def main(): - warnings.warn('The json.tool module is deprecated', - DeprecationWarning, stacklevel=2) - __main__.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='?', + help='a JSON file to be validated or pretty-printed', + default='-') + parser.add_argument('outfile', nargs='?', + help='write the output of infile to outfile', + default=None) + 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 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)') + options = parser.parse_args() + + 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'] = ',', ':' + + try: + if options.infile == '-': + infile = sys.stdin + else: + infile = open(options.infile, encoding='utf-8') + try: + if options.json_lines: + objs = (json.loads(line) for line in infile) + else: + objs = (json.load(infile),) + finally: + if infile is not sys.stdin: + infile.close() + + if options.outfile is None: + outfile = sys.stdout + else: + outfile = open(options.outfile, 'w', encoding='utf-8') + with outfile: + for obj in objs: + json.dump(obj, outfile, **dump_args) + outfile.write('\n') + except ValueError as e: + raise SystemExit(e) if __name__ == '__main__': + warnings.warn('The json.tool module is deprecated', DeprecationWarning) try: main() except BrokenPipeError as exc: diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index fc9149dbd8d363..eb568359c6b132 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -254,4 +254,5 @@ def test_with_warnings(self): rc, out, err = assert_python_ok('-m', self.module, infile) self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) - self.assertEqual(err.decode(), f'{filename}:15: DeprecationWarning: The json.tool module is deprecated\n main()\n') + self.assertRegex(err.decode(), + r'^.+/json/tool\.py:\d+: DeprecationWarning: The json.tool module is deprecated\n.+\n$')