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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy