Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 261680c

Browse files
committed
cli.console: keep unicode chars in JSON output
1 parent 00b8951 commit 261680c

2 files changed

Lines changed: 70 additions & 39 deletions

File tree

src/streamlink_cli/console.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ def msg_json(self, *objs: Any, **keywords: Any) -> None:
7979
out.update(**obj)
8080
out.update(**keywords)
8181

82-
msg = dumps(out, cls=JSONEncoder, indent=2)
82+
# don't escape Unicode characters outside the ASCII range if the output encoding is UTF-8
83+
ensure_ascii = self.output.encoding != "utf-8"
84+
85+
msg = dumps(out, cls=JSONEncoder, ensure_ascii=ensure_ascii, indent=2)
8386
self.output.write(f"{msg}\n")
8487

8588

tests/cli/test_console.py

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from io import StringIO
1+
from io import BytesIO, TextIOWrapper
22
from textwrap import dedent
33
from unittest.mock import Mock
44

@@ -7,46 +7,80 @@
77
from streamlink_cli.console import ConsoleOutput
88

99

10+
def getvalue(output: TextIOWrapper, size: int = -1):
11+
output.seek(0)
12+
13+
return output.read(size)
14+
15+
1016
class TestConsoleOutput:
1117
@pytest.fixture(autouse=True)
1218
def _isatty(self, request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch):
1319
isatty = not request.function.__name__.endswith("_no_tty")
1420
monkeypatch.setattr("sys.stdin.isatty", lambda: isatty)
1521

16-
def test_msg(self):
17-
output = StringIO()
22+
@pytest.fixture()
23+
def output(self, request: pytest.FixtureRequest):
24+
params = getattr(request, "param", {})
25+
params.setdefault("encoding", "utf-8")
26+
params.setdefault("errors", "backslashreplace")
27+
output = TextIOWrapper(BytesIO(), **params)
28+
29+
return output
30+
31+
@pytest.mark.parametrize(("output", "expected"), [
32+
pytest.param(
33+
{"encoding": "utf-8"},
34+
"Bär: 🐻",
35+
id="utf-8 encoding",
36+
),
37+
pytest.param(
38+
{"encoding": "ascii"},
39+
"B\\xe4r: \\U0001f43b", # Unicode character: "Bear Face" (U+1F43B)
40+
id="ascii encoding",
41+
),
42+
], indirect=["output"])
43+
def test_msg(self, output: TextIOWrapper, expected: str):
1844
console = ConsoleOutput(output)
19-
console.msg("foo")
45+
console.msg("Bär: 🐻")
2046
console.msg_json({"test": 1})
21-
assert output.getvalue() == "foo\n"
22-
23-
def test_msg_json(self):
24-
output = StringIO()
47+
assert getvalue(output) == f"{expected}\n"
48+
49+
@pytest.mark.parametrize(("output", "expected"), [
50+
pytest.param(
51+
{"encoding": "utf-8"},
52+
"Bär: 🐻",
53+
id="utf-8 encoding",
54+
),
55+
pytest.param(
56+
{"encoding": "ascii"},
57+
"B\\u00e4r: \\ud83d\\udc3b", # Unicode character: "Bear Face" (U+1F43B) - UTF-16: 0xD83D 0xDC3B
58+
id="ascii encoding",
59+
),
60+
], indirect=["output"])
61+
def test_msg_json(self, output: TextIOWrapper, expected: str):
2562
console = ConsoleOutput(output, json=True)
2663
console.msg("foo")
27-
console.msg_json({"test": 1})
28-
assert output.getvalue() == '{\n "test": 1\n}\n'
64+
console.msg_json({"test": "Bär: 🐻"})
65+
assert getvalue(output) == f'{{\n "test": "{expected}"\n}}\n'
2966

30-
def test_msg_json_object(self):
31-
output = StringIO()
67+
def test_msg_json_object(self, output: TextIOWrapper):
3268
console = ConsoleOutput(output, json=True)
33-
console.msg_json(Mock(__json__=Mock(return_value={"test": 1})))
34-
assert output.getvalue() == '{\n "test": 1\n}\n'
69+
console.msg_json(Mock(__json__=lambda: {"test": "Hello world, Γειά σου Κόσμε, こんにちは世界"})) # noqa: RUF001
70+
assert getvalue(output) == '{\n "test": "Hello world, Γειά σου Κόσμε, こんにちは世界"\n}\n' # noqa: RUF001
3571

36-
def test_msg_json_list(self):
37-
output = StringIO()
72+
def test_msg_json_list(self, output: TextIOWrapper):
3873
console = ConsoleOutput(output, json=True)
39-
test_list = ["foo", "bar"]
74+
test_list = ["Hello world, Γειά σου Κόσμε, こんにちは世界", '"🐻"'] # noqa: RUF001
4075
console.msg_json(test_list)
41-
assert output.getvalue() == '[\n "foo",\n "bar"\n]\n'
76+
assert getvalue(output) == '[\n "Hello world, Γειά σου Κόσμε, こんにちは世界",\n "\\"🐻\\""\n]\n' # noqa: RUF001
4277

43-
def test_msg_json_merge_object(self):
44-
output = StringIO()
78+
def test_msg_json_merge_object(self, output: TextIOWrapper):
4579
console = ConsoleOutput(output, json=True)
4680
test_obj1 = {"test": 1, "foo": "foo"}
4781
test_obj2 = Mock(__json__=Mock(return_value={"test": 2}))
4882
console.msg_json(test_obj1, test_obj2, ["qux"], foo="bar", baz="qux")
49-
assert output.getvalue() == dedent("""
83+
assert getvalue(output) == dedent("""
5084
{
5185
"test": 2,
5286
"foo": "bar",
@@ -55,13 +89,12 @@ def test_msg_json_merge_object(self):
5589
""").lstrip()
5690
assert list(test_obj1.items()) == [("test", 1), ("foo", "foo")]
5791

58-
def test_msg_json_merge_list(self):
59-
output = StringIO()
92+
def test_msg_json_merge_list(self, output: TextIOWrapper):
6093
console = ConsoleOutput(output, json=True)
6194
test_list1 = ["foo", "bar"]
6295
test_list2 = Mock(__json__=Mock(return_value={"foo": "bar"}))
6396
console.msg_json(test_list1, ["baz"], test_list2, {"foo": "bar"}, foo="bar", baz="qux")
64-
assert output.getvalue() == dedent("""
97+
assert getvalue(output) == dedent("""
6598
[
6699
"foo",
67100
"bar",
@@ -80,45 +113,40 @@ def test_msg_json_merge_list(self):
80113
""").lstrip()
81114
assert test_list1 == ["foo", "bar"]
82115

83-
def test_ask(self, monkeypatch: pytest.MonkeyPatch):
116+
def test_ask(self, monkeypatch: pytest.MonkeyPatch, output: TextIOWrapper):
84117
monkeypatch.setattr("builtins.input", Mock(return_value="hello"))
85118

86-
output = StringIO()
87119
console = ConsoleOutput(output)
88120
assert console.ask("test: ") == "hello"
89-
assert output.getvalue() == "test: "
121+
assert getvalue(output) == "test: "
90122

91-
def test_ask_no_tty(self, monkeypatch: pytest.MonkeyPatch):
123+
def test_ask_no_tty(self, monkeypatch: pytest.MonkeyPatch, output: TextIOWrapper):
92124
mock_input = Mock()
93125
monkeypatch.setattr("builtins.input", mock_input)
94126

95-
output = StringIO()
96127
console = ConsoleOutput(output)
97128
assert console.ask("test: ") is None
98-
assert output.getvalue() == ""
129+
assert getvalue(output) == ""
99130
assert mock_input.call_args_list == []
100131

101-
def test_ask_input_exception(self, monkeypatch: pytest.MonkeyPatch):
132+
def test_ask_input_exception(self, monkeypatch: pytest.MonkeyPatch, output: TextIOWrapper):
102133
monkeypatch.setattr("builtins.input", Mock(side_effect=ValueError))
103134

104-
output = StringIO()
105135
console = ConsoleOutput(output)
106136
assert console.ask("test: ") is None
107-
assert output.getvalue() == "test: "
137+
assert getvalue(output) == "test: "
108138

109-
def test_askpass(self, monkeypatch: pytest.MonkeyPatch):
139+
def test_askpass(self, monkeypatch: pytest.MonkeyPatch, output: TextIOWrapper):
110140
def getpass(prompt, stream):
111141
stream.write(prompt)
112142
return "hello"
113143

114144
monkeypatch.setattr("streamlink_cli.console.getpass", getpass)
115145

116-
output = StringIO()
117146
console = ConsoleOutput(output)
118147
assert console.askpass("test: ") == "hello"
119-
assert output.getvalue() == "test: "
148+
assert getvalue(output) == "test: "
120149

121-
def test_askpass_no_tty(self):
122-
output = StringIO()
150+
def test_askpass_no_tty(self, output: TextIOWrapper):
123151
console = ConsoleOutput(output)
124152
assert console.askpass("test: ") is None

0 commit comments

Comments
 (0)