From cae293fa94ed9246604517f4b566fa5169644a28 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 15:14:03 +0200 Subject: [PATCH 01/32] Add color to the json.tool CLI output --- Lib/json/tool.py | 36 ++++++++++++++++++++++++++++++++- Lib/test/test_json/test_tool.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 1ba91384c81f27..40c3a1e1b1b2b6 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -6,6 +6,8 @@ import argparse import json import sys +import re +from _colorize import ANSIColors, can_colorize def main(): @@ -48,6 +50,8 @@ def main(): dump_args['indent'] = None dump_args['separators'] = ',', ':' + with_colors = can_colorize() + try: if options.infile == '-': infile = sys.stdin @@ -68,12 +72,42 @@ def main(): outfile = open(options.outfile, 'w', encoding='utf-8') with outfile: for obj in objs: - json.dump(obj, outfile, **dump_args) + if with_colors: + json_str = json.dumps(obj, **dump_args) + outfile.write(colorize_json(json_str)) + else: + json.dump(obj, outfile, **dump_args) outfile.write('\n') except ValueError as e: raise SystemExit(e) +color_pattern = re.compile(r''' + (?P"(.*?)") | # String + (?P[\d\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null +''', re.VERBOSE) + + +def colorize_json(json_str): + colors = { + 'string': ANSIColors.GREEN, + 'number': ANSIColors.YELLOW, + 'boolean': ANSIColors.CYAN, + 'null': ANSIColors.CYAN, + } + + def replace(match): + for key in colors: + if match.group(key): + color = colors[key] + return f"{color}{match.group(key)}{ANSIColors.RESET}" + return match.group() + + return re.sub(color_pattern, replace, json_str) + + if __name__ == '__main__': try: main() diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 5da7cdcad709fa..b1d36833e5478a 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -232,6 +232,40 @@ def test_broken_pipe_error(self): proc.communicate(b'"{}"') self.assertEqual(proc.returncode, errno.EPIPE) + def test_colors(self): + infile = os_helper.TESTFN + self.addCleanup(os.remove, infile) + + cases = ( + ('{}', b'{}'), + ('[]', b'[]'), + ('null', b'\x1b[36mnull\x1b[0m'), + ('true', b'\x1b[36mtrue\x1b[0m'), + ('false', b'\x1b[36mfalse\x1b[0m'), + ('"foo"', b'\x1b[32m"foo"\x1b[0m'), + ('123', b'\x1b[33m123\x1b[0m'), + ('-1.2345e+23', b'\x1b[33m-1.2345e+23\x1b[0m'), + ('{"foo": "bar", "baz": 1234, "qux": [true, false, null]}', + b'''\ +{ + \x1b[32m"foo"\x1b[0m: \x1b[32m"bar"\x1b[0m, + \x1b[32m"baz"\x1b[0m: \x1b[33m1234\x1b[0m, + \x1b[32m"qux"\x1b[0m: [ + \x1b[36mtrue\x1b[0m, + \x1b[36mfalse\x1b[0m, + \x1b[36mnull\x1b[0m + ] +}'''), + ) + + for input_, expected in cases: + with self.subTest(input=input_): + with open(infile, "w", encoding="utf-8") as fp: + fp.write(input_) + _, stdout, _ = assert_python_ok('-m', self.module, infile, + PYTHON_COLORS='1') + self.assertEqual(stdout.strip(), expected) + @support.requires_subprocess() class TestTool(TestMain): From 1854ae50a66b49769f1b5222dfe39e19a7aec922 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 16:05:47 +0200 Subject: [PATCH 02/32] Add news entry --- .../next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst new file mode 100644 index 00000000000000..f153f544dc4c62 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst @@ -0,0 +1 @@ +Add color output to the :program:`json.tool` CLI. From ee39cb2d22c85ddc0da3b73a535e40fe23f06c3f Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 18:02:24 +0200 Subject: [PATCH 03/32] Fix escaped quotes --- Lib/json/tool.py | 8 ++++---- Lib/test/test_json/test_tool.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 40c3a1e1b1b2b6..6dd6faa948c1d4 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -83,10 +83,10 @@ def main(): color_pattern = re.compile(r''' - (?P"(.*?)") | # String - (?P[\d\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null + (?P"(\\"|[^"])*?") | # String + (?P[\d\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null ''', re.VERBOSE) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index b1d36833e5478a..d4f8a4c49c6ab7 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -243,6 +243,7 @@ def test_colors(self): ('true', b'\x1b[36mtrue\x1b[0m'), ('false', b'\x1b[36mfalse\x1b[0m'), ('"foo"', b'\x1b[32m"foo"\x1b[0m'), + (r'" \"foo\" "', b'\x1b[32m" \\"foo\\" "\x1b[0m'), ('123', b'\x1b[33m123\x1b[0m'), ('-1.2345e+23', b'\x1b[33m-1.2345e+23\x1b[0m'), ('{"foo": "bar", "baz": 1234, "qux": [true, false, null]}', From 5e726eede377262fa1f0c926e2db57a3e817486b Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 18:59:08 +0200 Subject: [PATCH 04/32] Fix tests --- Lib/test/test_json/test_tool.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index d4f8a4c49c6ab7..e9b250dff987a5 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -10,6 +10,14 @@ from test.support.script_helper import assert_python_ok +def no_color(func): + def inner(*args, **kwargs): + with os_helper.EnvironmentVarGuard() as env: + env['PYTHON_COLORS'] = '0' + return func(*args, **kwargs) + return inner + + @support.requires_subprocess() class TestMain(unittest.TestCase): data = """ @@ -87,12 +95,14 @@ class TestMain(unittest.TestCase): } """) + @no_color def test_stdin_stdout(self): 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, '') + @no_color def _create_infile(self, data=None): infile = os_helper.TESTFN with open(infile, "w", encoding="utf-8") as fp: @@ -100,6 +110,7 @@ def _create_infile(self, data=None): fp.write(data or self.data) return infile + @no_color def test_infile_stdout(self): infile = self._create_infile() rc, out, err = assert_python_ok('-m', self.module, infile) @@ -107,6 +118,7 @@ def test_infile_stdout(self): self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) self.assertEqual(err, b'') + @no_color def test_non_ascii_infile(self): data = '{"msg": "\u3053\u3093\u306b\u3061\u306f"}' expect = textwrap.dedent('''\ @@ -122,6 +134,7 @@ def test_non_ascii_infile(self): self.assertEqual(out.splitlines(), expect.splitlines()) self.assertEqual(err, b'') + @no_color def test_infile_outfile(self): infile = self._create_infile() outfile = os_helper.TESTFN + '.out' @@ -133,6 +146,7 @@ def test_infile_outfile(self): self.assertEqual(out, b'') self.assertEqual(err, b'') + @no_color def test_writing_in_place(self): infile = self._create_infile() rc, out, err = assert_python_ok('-m', self.module, infile, infile) @@ -142,18 +156,21 @@ def test_writing_in_place(self): self.assertEqual(out, b'') self.assertEqual(err, b'') + @no_color def test_jsonlines(self): 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, '') + @no_color def test_help_flag(self): rc, out, err = assert_python_ok('-m', self.module, '-h') self.assertEqual(rc, 0) self.assertTrue(out.startswith(b'usage: ')) self.assertEqual(err, b'') + @no_color def test_sort_keys_flag(self): infile = self._create_infile() rc, out, err = assert_python_ok('-m', self.module, '--sort-keys', infile) @@ -162,6 +179,7 @@ def test_sort_keys_flag(self): self.expect_without_sort_keys.encode().splitlines()) self.assertEqual(err, b'') + @no_color def test_indent(self): input_ = '[1, 2]' expect = textwrap.dedent('''\ @@ -175,6 +193,7 @@ def test_indent(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') + @no_color def test_no_indent(self): input_ = '[1,\n2]' expect = '[1, 2]\n' @@ -183,6 +202,7 @@ def test_no_indent(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') + @no_color def test_tab(self): input_ = '[1, 2]' expect = '[\n\t1,\n\t2\n]\n' @@ -191,6 +211,7 @@ def test_tab(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') + @no_color def test_compact(self): input_ = '[ 1 ,\n 2]' expect = '[1,2]\n' @@ -199,6 +220,7 @@ def test_compact(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') + @no_color def test_no_ensure_ascii_flag(self): infile = self._create_infile('{"key":"💩"}') outfile = os_helper.TESTFN + '.out' @@ -210,6 +232,7 @@ def test_no_ensure_ascii_flag(self): expected = [b'{', b' "key": "\xf0\x9f\x92\xa9"', b"}"] self.assertEqual(lines, expected) + @no_color def test_ensure_ascii_default(self): infile = self._create_infile('{"key":"💩"}') outfile = os_helper.TESTFN + '.out' @@ -222,6 +245,7 @@ def test_ensure_ascii_default(self): self.assertEqual(lines, expected) @unittest.skipIf(sys.platform =="win32", "The test is failed with ValueError on Windows") + @no_color def test_broken_pipe_error(self): cmd = [sys.executable, '-m', self.module] proc = subprocess.Popen(cmd, From 848a7becfdf0cff696cdf7698c991909e91ff049 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 19:10:44 +0200 Subject: [PATCH 05/32] Fix the tests for real this time --- Lib/test/test_json/test_tool.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index e9b250dff987a5..e6fbf035474e30 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -102,7 +102,6 @@ def test_stdin_stdout(self): self.assertEqual(process.stdout, self.expect) self.assertEqual(process.stderr, '') - @no_color def _create_infile(self, data=None): infile = os_helper.TESTFN with open(infile, "w", encoding="utf-8") as fp: @@ -110,15 +109,14 @@ def _create_infile(self, data=None): fp.write(data or self.data) return infile - @no_color def test_infile_stdout(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', self.module, infile) + rc, out, err = assert_python_ok('-m', self.module, infile, + PYTHON_COLORS='0') self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), self.expect.encode().splitlines()) self.assertEqual(err, b'') - @no_color def test_non_ascii_infile(self): data = '{"msg": "\u3053\u3093\u306b\u3061\u306f"}' expect = textwrap.dedent('''\ @@ -128,17 +126,18 @@ def test_non_ascii_infile(self): ''').encode() infile = self._create_infile(data) - rc, out, err = assert_python_ok('-m', self.module, infile) + rc, out, err = assert_python_ok('-m', self.module, infile, + PYTHON_COLORS='0') self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), expect.splitlines()) self.assertEqual(err, b'') - @no_color def test_infile_outfile(self): infile = self._create_infile() outfile = os_helper.TESTFN + '.out' - rc, out, err = assert_python_ok('-m', self.module, infile, outfile) + rc, out, err = assert_python_ok('-m', self.module, infile, outfile, + PYTHON_COLORS='0') self.addCleanup(os.remove, outfile) with open(outfile, "r", encoding="utf-8") as fp: self.assertEqual(fp.read(), self.expect) @@ -146,10 +145,10 @@ def test_infile_outfile(self): self.assertEqual(out, b'') self.assertEqual(err, b'') - @no_color def test_writing_in_place(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', self.module, infile, infile) + rc, out, err = assert_python_ok('-m', self.module, infile, infile, + PYTHON_COLORS='0') with open(infile, "r", encoding="utf-8") as fp: self.assertEqual(fp.read(), self.expect) self.assertEqual(rc, 0) @@ -163,17 +162,17 @@ def test_jsonlines(self): self.assertEqual(process.stdout, self.jsonlines_expect) self.assertEqual(process.stderr, '') - @no_color def test_help_flag(self): - rc, out, err = assert_python_ok('-m', self.module, '-h') + rc, out, err = assert_python_ok('-m', self.module, '-h', + PYTHON_COLORS='0') self.assertEqual(rc, 0) self.assertTrue(out.startswith(b'usage: ')) self.assertEqual(err, b'') - @no_color def test_sort_keys_flag(self): infile = self._create_infile() - rc, out, err = assert_python_ok('-m', self.module, '--sort-keys', infile) + rc, out, err = assert_python_ok('-m', self.module, '--sort-keys', infile, + PYTHON_COLORS='0') self.assertEqual(rc, 0) self.assertEqual(out.splitlines(), self.expect_without_sort_keys.encode().splitlines()) @@ -220,24 +219,23 @@ def test_compact(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') - @no_color 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', self.module, '--no-ensure-ascii', infile, outfile) + assert_python_ok('-m', self.module, '--no-ensure-ascii', infile, + outfile, PYTHON_COLORS='0') 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) - @no_color 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', self.module, infile, outfile) + assert_python_ok('-m', self.module, infile, outfile, PYTHON_COLORS='0') with open(outfile, "rb") as f: lines = f.read().splitlines() # asserting an ascii encoded output file From 8d90ccb5c23aad60027c08e274bb6aa6addc2ae5 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 21:07:14 +0200 Subject: [PATCH 06/32] Fix tests on Windows --- Lib/test/test_json/test_tool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index e6fbf035474e30..dc8aeb7ba59566 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -287,7 +287,9 @@ def test_colors(self): fp.write(input_) _, stdout, _ = assert_python_ok('-m', self.module, infile, PYTHON_COLORS='1') - self.assertEqual(stdout.strip(), expected) + stdout = stdout.replace(b'\r\n', b'\n') # normalize line endings + stdout = stdout.strip() + self.assertEqual(stdout, expected) @support.requires_subprocess() From 93e430611c123883675dc09572b20df55c5b3dd6 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 21:12:02 +0200 Subject: [PATCH 07/32] Sort imports --- Lib/json/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 6dd6faa948c1d4..1e613d535a0a85 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -5,8 +5,8 @@ """ import argparse import json -import sys import re +import sys from _colorize import ANSIColors, can_colorize From f8d697a1d05e630085ac60e95554d64966b23bce Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 5 Apr 2025 23:19:32 +0200 Subject: [PATCH 08/32] Fix string regex --- Lib/json/tool.py | 8 ++++---- Lib/test/test_json/test_tool.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 1e613d535a0a85..64b3af69daf7ba 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -83,10 +83,10 @@ def main(): color_pattern = re.compile(r''' - (?P"(\\"|[^"])*?") | # String - (?P[\d\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null + (?P"(\\.|[^"\\])*?") | # String + (?P[\d\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null ''', re.VERBOSE) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index dc8aeb7ba59566..3473e5191d8902 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -268,6 +268,16 @@ def test_colors(self): (r'" \"foo\" "', b'\x1b[32m" \\"foo\\" "\x1b[0m'), ('123', b'\x1b[33m123\x1b[0m'), ('-1.2345e+23', b'\x1b[33m-1.2345e+23\x1b[0m'), + (r'{"\\": ""}', + b'''\ +{ + \x1b[32m"\\\\"\x1b[0m: \x1b[32m""\x1b[0m +}'''), + (r'{"\\\\": ""}', + b'''\ +{ + \x1b[32m"\\\\\\\\"\x1b[0m: \x1b[32m""\x1b[0m +}'''), ('{"foo": "bar", "baz": 1234, "qux": [true, false, null]}', b'''\ { From 429e350bb4e183af787843b2b697f84ae6bb781a Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 10:03:59 +0200 Subject: [PATCH 09/32] Handle NaN & Infinity --- Lib/json/tool.py | 8 ++++---- Lib/test/test_json/test_tool.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 64b3af69daf7ba..ca3e3bd916dcbf 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -83,10 +83,10 @@ def main(): color_pattern = re.compile(r''' - (?P"(\\.|[^"\\])*?") | # String - (?P[\d\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null + (?P"(\\.|[^"\\])*?") | # String + (?PNaN|-?Infinity|[\d\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null ''', re.VERBOSE) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 3473e5191d8902..d54c9ed5811077 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -264,6 +264,9 @@ def test_colors(self): ('null', b'\x1b[36mnull\x1b[0m'), ('true', b'\x1b[36mtrue\x1b[0m'), ('false', b'\x1b[36mfalse\x1b[0m'), + ('NaN', b'\x1b[33mNaN\x1b[0m'), + ('Infinity', b'\x1b[33mInfinity\x1b[0m'), + ('-Infinity', b'\x1b[33m-Infinity\x1b[0m'), ('"foo"', b'\x1b[32m"foo"\x1b[0m'), (r'" \"foo\" "', b'\x1b[32m" \\"foo\\" "\x1b[0m'), ('123', b'\x1b[33m123\x1b[0m'), @@ -278,7 +281,13 @@ def test_colors(self): { \x1b[32m"\\\\\\\\"\x1b[0m: \x1b[32m""\x1b[0m }'''), - ('{"foo": "bar", "baz": 1234, "qux": [true, false, null]}', + ('''\ +{ + "foo": "bar", + "baz": 1234, + "qux": [true, false, null], + "xyz": [NaN, -Infinity, Infinity] +}''', b'''\ { \x1b[32m"foo"\x1b[0m: \x1b[32m"bar"\x1b[0m, @@ -287,6 +296,11 @@ def test_colors(self): \x1b[36mtrue\x1b[0m, \x1b[36mfalse\x1b[0m, \x1b[36mnull\x1b[0m + ], + \x1b[32m"xyz"\x1b[0m: [ + \x1b[33mNaN\x1b[0m, + \x1b[33m-Infinity\x1b[0m, + \x1b[33mInfinity\x1b[0m ] }'''), ) From 0abe76a62c0333ad9d4277fa88224b64614e3439 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 11:22:42 +0200 Subject: [PATCH 10/32] Use only digits in number regex --- Lib/json/tool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index ca3e3bd916dcbf..0bda807b55b2d4 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -83,10 +83,10 @@ def main(): color_pattern = re.compile(r''' - (?P"(\\.|[^"\\])*?") | # String - (?PNaN|-?Infinity|[\d\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null + (?P"(\\.|[^"\\])*?") | # String + (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null ''', re.VERBOSE) From 1acd35ddb7165e0b83a1c0c18dffef9116e5986e Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 11:24:48 +0200 Subject: [PATCH 11/32] Remove non-greedy matching in string regex --- Lib/json/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 0bda807b55b2d4..be903e40b898fa 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -83,7 +83,7 @@ def main(): color_pattern = re.compile(r''' - (?P"(\\.|[^"\\])*?") | # String + (?P"(\\.|[^"\\])*") | # String (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number (?Ptrue|false) | # Boolean (?Pnull) # Null From 691aecd3cf1af5feec6e772bf2b1482b1d222ebe Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 11:41:50 +0200 Subject: [PATCH 12/32] Test unicode --- Lib/test/test_json/test_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index d54c9ed5811077..1452d08c233f2c 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -269,6 +269,7 @@ def test_colors(self): ('-Infinity', b'\x1b[33m-Infinity\x1b[0m'), ('"foo"', b'\x1b[32m"foo"\x1b[0m'), (r'" \"foo\" "', b'\x1b[32m" \\"foo\\" "\x1b[0m'), + ('"α"', b'\x1b[32m"\\u03b1"\x1b[0m'), ('123', b'\x1b[33m123\x1b[0m'), ('-1.2345e+23', b'\x1b[33m-1.2345e+23\x1b[0m'), (r'{"\\": ""}', From 5afac978379965dfa1f454d449dff5968ad1b48b Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Sun, 6 Apr 2025 19:02:59 +0200 Subject: [PATCH 13/32] Pass the file to `can_colorize` Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/json/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index be903e40b898fa..d0dee0394b774c 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -72,7 +72,7 @@ def main(): outfile = open(options.outfile, 'w', encoding='utf-8') with outfile: for obj in objs: - if with_colors: + if can_colorize(file=outfile): json_str = json.dumps(obj, **dump_args) outfile.write(colorize_json(json_str)) else: From 911f75fd8a64d52d3a3928d9cc138f8621fc30a5 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:04:08 +0200 Subject: [PATCH 14/32] Make the test file runnable --- Lib/test/test_json/test_tool.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 1452d08c233f2c..4750f2fa191050 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -320,3 +320,7 @@ def test_colors(self): @support.requires_subprocess() class TestTool(TestMain): module = 'json.tool' + + +if __name__ == "__main__": + unittest.main() From 4c27be77333429022b2e723fc2023fd6c82f62e1 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:06:24 +0200 Subject: [PATCH 15/32] Use force_not_colorized --- Lib/test/test_json/test_tool.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 4750f2fa191050..d000382d01b184 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -6,18 +6,10 @@ import subprocess from test import support -from test.support import os_helper +from test.support import force_not_colorized, os_helper from test.support.script_helper import assert_python_ok -def no_color(func): - def inner(*args, **kwargs): - with os_helper.EnvironmentVarGuard() as env: - env['PYTHON_COLORS'] = '0' - return func(*args, **kwargs) - return inner - - @support.requires_subprocess() class TestMain(unittest.TestCase): data = """ @@ -95,7 +87,7 @@ class TestMain(unittest.TestCase): } """) - @no_color + @force_not_colorized def test_stdin_stdout(self): args = sys.executable, '-m', self.module process = subprocess.run(args, input=self.data, capture_output=True, text=True, check=True) @@ -155,7 +147,7 @@ def test_writing_in_place(self): self.assertEqual(out, b'') self.assertEqual(err, b'') - @no_color + @force_not_colorized def test_jsonlines(self): args = sys.executable, '-m', self.module, '--json-lines' process = subprocess.run(args, input=self.jsonlines_raw, capture_output=True, text=True, check=True) @@ -178,7 +170,7 @@ def test_sort_keys_flag(self): self.expect_without_sort_keys.encode().splitlines()) self.assertEqual(err, b'') - @no_color + @force_not_colorized def test_indent(self): input_ = '[1, 2]' expect = textwrap.dedent('''\ @@ -192,7 +184,7 @@ def test_indent(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') - @no_color + @force_not_colorized def test_no_indent(self): input_ = '[1,\n2]' expect = '[1, 2]\n' @@ -201,7 +193,7 @@ def test_no_indent(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') - @no_color + @force_not_colorized def test_tab(self): input_ = '[1, 2]' expect = '[\n\t1,\n\t2\n]\n' @@ -210,7 +202,7 @@ def test_tab(self): self.assertEqual(process.stdout, expect) self.assertEqual(process.stderr, '') - @no_color + @force_not_colorized def test_compact(self): input_ = '[ 1 ,\n 2]' expect = '[1,2]\n' @@ -242,8 +234,8 @@ def test_ensure_ascii_default(self): expected = [b'{', rb' "key": "\ud83d\udca9"', b"}"] self.assertEqual(lines, expected) + @force_not_colorized @unittest.skipIf(sys.platform =="win32", "The test is failed with ValueError on Windows") - @no_color def test_broken_pipe_error(self): cmd = [sys.executable, '-m', self.module] proc = subprocess.Popen(cmd, From 18ce6fac9cf3f140d9179364261a07c047b98a2e Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:08:06 +0200 Subject: [PATCH 16/32] Remove unused variable --- Lib/json/tool.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index d0dee0394b774c..ddd005bb438eda 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -50,8 +50,6 @@ def main(): dump_args['indent'] = None dump_args['separators'] = ',', ':' - with_colors = can_colorize() - try: if options.infile == '-': infile = sys.stdin From 18f7ae596e9076d3abd8b2ed7daebda3ce2b46e2 Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Sun, 6 Apr 2025 19:45:15 +0200 Subject: [PATCH 17/32] =?UTF-8?q?=F0=9F=A6=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/json/tool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index ddd005bb438eda..df7858394728ba 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -98,9 +98,9 @@ def colorize_json(json_str): def replace(match): for key in colors: - if match.group(key): + if m := match.group(key): color = colors[key] - return f"{color}{match.group(key)}{ANSIColors.RESET}" + return f"{color}{m}{ANSIColors.RESET}" return match.group() return re.sub(color_pattern, replace, json_str) From b5157af6eb8822d3c9daa6d13c2670d6af12dcc1 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:50:19 +0200 Subject: [PATCH 18/32] Add a comment to the color regex --- Lib/json/tool.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index df7858394728ba..94d9e8c7ff9886 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -80,6 +80,10 @@ def main(): raise SystemExit(e) +# The string we are colorizing is a 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"(\\.|[^"\\])*") | # String (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number From d7287e208e5da8e7d12d10152caa17076df74110 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:50:56 +0200 Subject: [PATCH 19/32] Move helper functions to the start --- Lib/json/tool.py | 60 ++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 94d9e8c7ff9886..96c0356515d057 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -10,6 +10,36 @@ from _colorize import ANSIColors, can_colorize +# The string we are colorizing is a 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"(\\.|[^"\\])*") | # String + (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number + (?Ptrue|false) | # Boolean + (?Pnull) # Null +''', re.VERBOSE) + + +def colorize_json(json_str): + colors = { + 'string': ANSIColors.GREEN, + 'number': ANSIColors.YELLOW, + 'boolean': ANSIColors.CYAN, + 'null': ANSIColors.CYAN, + } + + def replace(match): + for key in colors: + if m := match.group(key): + color = colors[key] + return f"{color}{m}{ANSIColors.RESET}" + return match.group() + + return re.sub(color_pattern, replace, json_str) + + def main(): description = ('A simple command line interface for json module ' 'to validate and pretty-print JSON objects.') @@ -80,36 +110,6 @@ def main(): raise SystemExit(e) -# The string we are colorizing is a 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"(\\.|[^"\\])*") | # String - (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null -''', re.VERBOSE) - - -def colorize_json(json_str): - colors = { - 'string': ANSIColors.GREEN, - 'number': ANSIColors.YELLOW, - 'boolean': ANSIColors.CYAN, - 'null': ANSIColors.CYAN, - } - - def replace(match): - for key in colors: - if m := match.group(key): - color = colors[key] - return f"{color}{m}{ANSIColors.RESET}" - return match.group() - - return re.sub(color_pattern, replace, json_str) - - if __name__ == '__main__': try: main() From 177107b90493af31d891438fd19e455c1bb1597a Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 19:51:39 +0200 Subject: [PATCH 20/32] Make helper functions private --- Lib/json/tool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 96c0356515d057..91c1f839097e50 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -14,7 +14,7 @@ # 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''' +_color_pattern = re.compile(r''' (?P"(\\.|[^"\\])*") | # String (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number (?Ptrue|false) | # Boolean @@ -22,7 +22,7 @@ ''', re.VERBOSE) -def colorize_json(json_str): +def _colorize_json(json_str): colors = { 'string': ANSIColors.GREEN, 'number': ANSIColors.YELLOW, @@ -37,7 +37,7 @@ def replace(match): return f"{color}{m}{ANSIColors.RESET}" return match.group() - return re.sub(color_pattern, replace, json_str) + return re.sub(_color_pattern, replace, json_str) def main(): @@ -102,7 +102,7 @@ def main(): for obj in objs: if can_colorize(file=outfile): json_str = json.dumps(obj, **dump_args) - outfile.write(colorize_json(json_str)) + outfile.write(_colorize_json(json_str)) else: json.dump(obj, outfile, **dump_args) outfile.write('\n') From b7589be40bdc76aa72754ab60cd7a5c1dae9db7e Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 20:05:21 +0200 Subject: [PATCH 21/32] Prefer global functions --- Lib/json/tool.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 91c1f839097e50..1e167931fb99f4 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -22,22 +22,24 @@ ''', re.VERBOSE) -def _colorize_json(json_str): - colors = { - 'string': ANSIColors.GREEN, - 'number': ANSIColors.YELLOW, - 'boolean': ANSIColors.CYAN, - 'null': ANSIColors.CYAN, - } +_colors = { + 'string': ANSIColors.GREEN, + 'number': ANSIColors.YELLOW, + 'boolean': ANSIColors.CYAN, + 'null': ANSIColors.CYAN, +} + - def replace(match): - for key in colors: - if m := match.group(key): - color = colors[key] - return f"{color}{m}{ANSIColors.RESET}" - return match.group() +def _replace_match_callback(match): + for key in _colors: + if m := match.group(key): + color = _colors[key] + return f"{color}{m}{ANSIColors.RESET}" + return match.group() - return re.sub(_color_pattern, replace, json_str) + +def _colorize_json(json_str): + return re.sub(_color_pattern, _replace_match_callback, json_str) def main(): From e744290f0bd06178b8fb1738cd404e405ffa4d0b Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sun, 6 Apr 2025 20:42:56 +0200 Subject: [PATCH 22/32] Remove redundant comments --- Lib/json/tool.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 1e167931fb99f4..e291b4e41e7156 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -15,10 +15,10 @@ # 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"(\\.|[^"\\])*") | # String - (?PNaN|-?Infinity|[0-9\-+.Ee]+) | # Number - (?Ptrue|false) | # Boolean - (?Pnull) # Null + (?P"(\\.|[^"\\])*") | + (?PNaN|-?Infinity|[0-9\-+.Ee]+) | + (?Ptrue|false) | + (?Pnull) ''', re.VERBOSE) From fac39c7cb6c0c89a49f9a00708d4732de6a11185 Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Sun, 6 Apr 2025 21:09:23 +0200 Subject: [PATCH 23/32] Improve news entry Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- .../next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst index f153f544dc4c62..fa803075ec3012 100644 --- a/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst +++ b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst @@ -1 +1 @@ -Add color output to the :program:`json.tool` CLI. +Add color output to the :program:`json` CLI. From 4f399bea5b8612d3e02f8f4736c7743c348bad77 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 15 Apr 2025 18:48:18 +0200 Subject: [PATCH 24/32] Highlight keys in a different color --- Lib/json/tool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index e291b4e41e7156..a2b6472d455af6 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -15,6 +15,7 @@ # 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"(\\.|[^"\\])*")(?=:) | (?P"(\\.|[^"\\])*") | (?PNaN|-?Infinity|[0-9\-+.Ee]+) | (?Ptrue|false) | @@ -23,6 +24,7 @@ _colors = { + 'key': ANSIColors.INTENSE_BLUE, 'string': ANSIColors.GREEN, 'number': ANSIColors.YELLOW, 'boolean': ANSIColors.CYAN, From 8bd0a368d8d068090879b39d5ee7f1b839833ba8 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 15 Apr 2025 18:49:50 +0200 Subject: [PATCH 25/32] Improve color contrast --- Lib/json/tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index a2b6472d455af6..229ae8c3fa8756 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -25,10 +25,10 @@ _colors = { 'key': ANSIColors.INTENSE_BLUE, - 'string': ANSIColors.GREEN, + 'string': ANSIColors.INTENSE_GREEN, 'number': ANSIColors.YELLOW, - 'boolean': ANSIColors.CYAN, - 'null': ANSIColors.CYAN, + 'boolean': ANSIColors.INTENSE_CYAN, + 'null': ANSIColors.INTENSE_CYAN, } From 37d4c0846746452b39a30c75a41772230598cf64 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 15 Apr 2025 22:43:33 +0200 Subject: [PATCH 26/32] Tone down the colors a bit --- Lib/json/tool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 229ae8c3fa8756..a2b6472d455af6 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -25,10 +25,10 @@ _colors = { 'key': ANSIColors.INTENSE_BLUE, - 'string': ANSIColors.INTENSE_GREEN, + 'string': ANSIColors.GREEN, 'number': ANSIColors.YELLOW, - 'boolean': ANSIColors.INTENSE_CYAN, - 'null': ANSIColors.INTENSE_CYAN, + 'boolean': ANSIColors.CYAN, + 'null': ANSIColors.CYAN, } From 1e6f4ea8899ad1a96312c468d9a8634482ed6f69 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Tue, 15 Apr 2025 23:39:19 +0200 Subject: [PATCH 27/32] Use bold colors & fix tests --- Lib/json/tool.py | 8 +++--- Lib/test/test_json/test_tool.py | 46 ++++++++++++++++----------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index a2b6472d455af6..3c504c293ee483 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -25,10 +25,10 @@ _colors = { 'key': ANSIColors.INTENSE_BLUE, - 'string': ANSIColors.GREEN, - 'number': ANSIColors.YELLOW, - 'boolean': ANSIColors.CYAN, - 'null': ANSIColors.CYAN, + 'string': ANSIColors.BOLD_GREEN, + 'number': ANSIColors.BOLD_YELLOW, + 'boolean': ANSIColors.BOLD_CYAN, + 'null': ANSIColors.BOLD_CYAN, } diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index d000382d01b184..14c2411912b6dc 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -253,26 +253,26 @@ def test_colors(self): cases = ( ('{}', b'{}'), ('[]', b'[]'), - ('null', b'\x1b[36mnull\x1b[0m'), - ('true', b'\x1b[36mtrue\x1b[0m'), - ('false', b'\x1b[36mfalse\x1b[0m'), - ('NaN', b'\x1b[33mNaN\x1b[0m'), - ('Infinity', b'\x1b[33mInfinity\x1b[0m'), - ('-Infinity', b'\x1b[33m-Infinity\x1b[0m'), - ('"foo"', b'\x1b[32m"foo"\x1b[0m'), - (r'" \"foo\" "', b'\x1b[32m" \\"foo\\" "\x1b[0m'), - ('"α"', b'\x1b[32m"\\u03b1"\x1b[0m'), - ('123', b'\x1b[33m123\x1b[0m'), - ('-1.2345e+23', b'\x1b[33m-1.2345e+23\x1b[0m'), + ('null', b'\x1b[1;36mnull\x1b[0m'), + ('true', b'\x1b[1;36mtrue\x1b[0m'), + ('false', b'\x1b[1;36mfalse\x1b[0m'), + ('NaN', b'\x1b[1;33mNaN\x1b[0m'), + ('Infinity', b'\x1b[1;33mInfinity\x1b[0m'), + ('-Infinity', b'\x1b[1;33m-Infinity\x1b[0m'), + ('"foo"', b'\x1b[1;32m"foo"\x1b[0m'), + (r'" \"foo\" "', b'\x1b[1;32m" \\"foo\\" "\x1b[0m'), + ('"α"', b'\x1b[1;32m"\\u03b1"\x1b[0m'), + ('123', b'\x1b[1;33m123\x1b[0m'), + ('-1.2345e+23', b'\x1b[1;33m-1.2345e+23\x1b[0m'), (r'{"\\": ""}', b'''\ { - \x1b[32m"\\\\"\x1b[0m: \x1b[32m""\x1b[0m + \x1b[94m"\\\\"\x1b[0m: \x1b[1;32m""\x1b[0m }'''), (r'{"\\\\": ""}', b'''\ { - \x1b[32m"\\\\\\\\"\x1b[0m: \x1b[32m""\x1b[0m + \x1b[94m"\\\\\\\\"\x1b[0m: \x1b[1;32m""\x1b[0m }'''), ('''\ { @@ -283,17 +283,17 @@ def test_colors(self): }''', b'''\ { - \x1b[32m"foo"\x1b[0m: \x1b[32m"bar"\x1b[0m, - \x1b[32m"baz"\x1b[0m: \x1b[33m1234\x1b[0m, - \x1b[32m"qux"\x1b[0m: [ - \x1b[36mtrue\x1b[0m, - \x1b[36mfalse\x1b[0m, - \x1b[36mnull\x1b[0m + \x1b[94m"foo"\x1b[0m: \x1b[1;32m"bar"\x1b[0m, + \x1b[94m"baz"\x1b[0m: \x1b[1;33m1234\x1b[0m, + \x1b[94m"qux"\x1b[0m: [ + \x1b[1;36mtrue\x1b[0m, + \x1b[1;36mfalse\x1b[0m, + \x1b[1;36mnull\x1b[0m ], - \x1b[32m"xyz"\x1b[0m: [ - \x1b[33mNaN\x1b[0m, - \x1b[33m-Infinity\x1b[0m, - \x1b[33mInfinity\x1b[0m + \x1b[94m"xyz"\x1b[0m: [ + \x1b[1;33mNaN\x1b[0m, + \x1b[1;33m-Infinity\x1b[0m, + \x1b[1;33mInfinity\x1b[0m ] }'''), ) From cc9251826b95a0db497723d2f3586156e97b4356 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 19 Apr 2025 11:50:07 +0200 Subject: [PATCH 28/32] Use default color for numbers --- Lib/json/tool.py | 2 -- Lib/test/test_json/test_tool.py | 18 +++++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 3c504c293ee483..a8e3484384f80b 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -17,7 +17,6 @@ _color_pattern = re.compile(r''' (?P"(\\.|[^"\\])*")(?=:) | (?P"(\\.|[^"\\])*") | - (?PNaN|-?Infinity|[0-9\-+.Ee]+) | (?Ptrue|false) | (?Pnull) ''', re.VERBOSE) @@ -26,7 +25,6 @@ _colors = { 'key': ANSIColors.INTENSE_BLUE, 'string': ANSIColors.BOLD_GREEN, - 'number': ANSIColors.BOLD_YELLOW, 'boolean': ANSIColors.BOLD_CYAN, 'null': ANSIColors.BOLD_CYAN, } diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 14c2411912b6dc..ba9c42f758e2b2 100644 --- a/Lib/test/test_json/test_tool.py +++ b/Lib/test/test_json/test_tool.py @@ -256,14 +256,14 @@ def test_colors(self): ('null', b'\x1b[1;36mnull\x1b[0m'), ('true', b'\x1b[1;36mtrue\x1b[0m'), ('false', b'\x1b[1;36mfalse\x1b[0m'), - ('NaN', b'\x1b[1;33mNaN\x1b[0m'), - ('Infinity', b'\x1b[1;33mInfinity\x1b[0m'), - ('-Infinity', b'\x1b[1;33m-Infinity\x1b[0m'), + ('NaN', b'NaN'), + ('Infinity', b'Infinity'), + ('-Infinity', b'-Infinity'), ('"foo"', b'\x1b[1;32m"foo"\x1b[0m'), (r'" \"foo\" "', b'\x1b[1;32m" \\"foo\\" "\x1b[0m'), ('"α"', b'\x1b[1;32m"\\u03b1"\x1b[0m'), - ('123', b'\x1b[1;33m123\x1b[0m'), - ('-1.2345e+23', b'\x1b[1;33m-1.2345e+23\x1b[0m'), + ('123', b'123'), + ('-1.2345e+23', b'-1.2345e+23'), (r'{"\\": ""}', b'''\ { @@ -284,16 +284,16 @@ def test_colors(self): b'''\ { \x1b[94m"foo"\x1b[0m: \x1b[1;32m"bar"\x1b[0m, - \x1b[94m"baz"\x1b[0m: \x1b[1;33m1234\x1b[0m, + \x1b[94m"baz"\x1b[0m: 1234, \x1b[94m"qux"\x1b[0m: [ \x1b[1;36mtrue\x1b[0m, \x1b[1;36mfalse\x1b[0m, \x1b[1;36mnull\x1b[0m ], \x1b[94m"xyz"\x1b[0m: [ - \x1b[1;33mNaN\x1b[0m, - \x1b[1;33m-Infinity\x1b[0m, - \x1b[1;33mInfinity\x1b[0m + NaN, + -Infinity, + Infinity ] }'''), ) From 4fc5e27767feae75df958908eb70e1cb7713d961 Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 19 Apr 2025 11:59:55 +0200 Subject: [PATCH 29/32] Add What's New entry --- Doc/whatsnew/3.14.rst | 6 ++++++ .../Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 13448d1fc07654..eea33ba1595fc7 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -789,6 +789,12 @@ json See the :ref:`JSON command-line interface ` documentation. (Contributed by Trey Hunner in :gh:`122873`.) +* By default, the output of the `JSON command-line interface ` + is highlighted in color. This can be controlled via the + :envvar:`PYTHON_COLORS` environment variable as well as the canonical + |NO_COLOR|_ and |FORCE_COLOR|_ environment variables. See also + :ref:`using-on-controlling-color`. + (Contributed by Tomas Roun in :gh:`131952`.) linecache --------- diff --git a/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst index fa803075ec3012..4679abf105d0ea 100644 --- a/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst +++ b/Misc/NEWS.d/next/Library/2025-04-05-16-05-34.gh-issue-131952.HX6gCX.rst @@ -1 +1 @@ -Add color output to the :program:`json` CLI. +Add color output to the :program:`json` CLI. Patch by Tomas Roun. From a489649070e1a902479f903bedb9a0eb9e86929d Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Sat, 19 Apr 2025 12:01:23 +0200 Subject: [PATCH 30/32] Simplify code Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> --- Lib/json/tool.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index a8e3484384f80b..2d9ca2accc5577 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -31,9 +31,8 @@ def _replace_match_callback(match): - for key in _colors: + for key, color in _colors.items(): if m := match.group(key): - color = _colors[key] return f"{color}{m}{ANSIColors.RESET}" return match.group() From 789ca88449a92f62c16876af3ec05f72d5c54bed Mon Sep 17 00:00:00 2001 From: Tomas Roun Date: Sat, 19 Apr 2025 12:04:11 +0200 Subject: [PATCH 31/32] Lint fix --- Doc/whatsnew/3.14.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index eea33ba1595fc7..b69dbfde71f927 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -789,7 +789,7 @@ json See the :ref:`JSON command-line interface ` documentation. (Contributed by Trey Hunner in :gh:`122873`.) -* By default, the output of the `JSON command-line interface ` +* By default, the output of the :ref:`JSON command-line interface ` is highlighted in color. This can be controlled via the :envvar:`PYTHON_COLORS` environment variable as well as the canonical |NO_COLOR|_ and |FORCE_COLOR|_ environment variables. See also From ce75f86968cf54492b83cc89392cc5cf6d347d63 Mon Sep 17 00:00:00 2001 From: "Tomas R." Date: Sat, 19 Apr 2025 19:34:07 +0200 Subject: [PATCH 32/32] Fix typo Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Lib/json/tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/json/tool.py b/Lib/json/tool.py index 2d9ca2accc5577..585583da8604ac 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -10,7 +10,7 @@ from _colorize import ANSIColors, can_colorize -# The string we are colorizing is a valid JSON, +# 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.