Skip to content

Commit e3a44d5

Browse files
github-actions[bot]bearomorphism
authored andcommitted
docs(cli/screenshots): update CLI screenshots
[skip ci]
1 parent 1eb8cde commit e3a44d5

7 files changed

Lines changed: 113 additions & 3 deletions

File tree

commitizen/out.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,34 @@
44

55
from termcolor import colored
66

7-
if sys.platform == "win32":
8-
if isinstance(sys.stdout, io.TextIOWrapper) and sys.version_info >= (3, 7):
9-
sys.stdout.reconfigure(encoding="utf-8")
7+
8+
def _ensure_utf8_stdout(stream: object) -> None:
9+
"""Reconfigure ``stream`` to UTF-8 if its current encoding can't represent
10+
the unicode characters commitizen emits (e.g. ``\U0001f680`` 🚀, the
11+
``\u2019`` typographic apostrophe).
12+
13+
Without this, ``print`` raises ``UnicodeEncodeError`` mid-output on:
14+
15+
* Windows ``cmd.exe`` defaulting to ``cp1252`` (the historical case),
16+
* Linux/macOS terminals with a non-UTF-8 ``LANG`` such as
17+
``de_CH.ISO8859-1`` (#956).
18+
19+
``errors="replace"`` is used as a safety net for terminals that
20+
genuinely can't render the bytes, so commitizen falls back to a
21+
placeholder character instead of crashing.
22+
"""
23+
if not isinstance(stream, io.TextIOWrapper):
24+
return
25+
encoding = (stream.encoding or "").lower().replace("-", "").replace("_", "")
26+
if encoding == "utf8":
27+
return
28+
try:
29+
stream.reconfigure(encoding="utf-8", errors="replace")
30+
except (AttributeError, ValueError): # pragma: no cover - safety net
31+
pass
32+
33+
34+
_ensure_utf8_stdout(sys.stdout)
1035

1136

1237
def write(value: object, *args: object) -> None:
308 Bytes
Loading
-44 Bytes
Loading
1.03 KB
Loading
-47 Bytes
Loading
747 Bytes
Loading

tests/test_out.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Tests for ``commitizen.out``.
2+
3+
Mostly focused on the stdout-encoding helper introduced for #956: the
4+
function must reconfigure non-UTF-8 streams to UTF-8 with a permissive
5+
``errors="replace"`` strategy so commitizen output (emoji, typographic
6+
quotes) doesn't crash with ``UnicodeEncodeError`` on terminals using
7+
locale-dependent encodings such as ``cp1252`` (Windows) or
8+
``ISO8859-1`` (Linux/macOS).
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import io
14+
from typing import Any
15+
16+
from commitizen.out import _ensure_utf8_stdout
17+
18+
19+
class _StubStream(io.TextIOWrapper):
20+
"""Light-weight ``TextIOWrapper`` that records calls to ``reconfigure``.
21+
22+
Subclassing ``TextIOWrapper`` keeps the ``isinstance`` check in
23+
``_ensure_utf8_stdout`` happy without monkey-patching ``sys.stdout``.
24+
"""
25+
26+
reconfigure_calls: list[dict[str, Any]]
27+
28+
def __init__(self, encoding: str) -> None:
29+
super().__init__(io.BytesIO(), encoding=encoding)
30+
self.reconfigure_calls = []
31+
32+
def reconfigure(self, **kwargs: Any) -> None:
33+
self.reconfigure_calls.append(kwargs)
34+
super().reconfigure(**kwargs)
35+
36+
37+
def test_ensure_utf8_stdout_noop_when_already_utf8():
38+
stream = _StubStream(encoding="utf-8")
39+
_ensure_utf8_stdout(stream)
40+
assert stream.reconfigure_calls == []
41+
42+
43+
def test_ensure_utf8_stdout_noop_for_dashless_utf8_alias():
44+
stream = _StubStream(encoding="UTF8")
45+
_ensure_utf8_stdout(stream)
46+
assert stream.reconfigure_calls == []
47+
48+
49+
def test_ensure_utf8_stdout_reconfigures_iso8859_1_terminal():
50+
"""Regression test for #956 (Linux/macOS ``LANG=de_CH.ISO8859-1``)."""
51+
stream = _StubStream(encoding="latin-1")
52+
_ensure_utf8_stdout(stream)
53+
assert stream.reconfigure_calls == [{"encoding": "utf-8", "errors": "replace"}]
54+
55+
56+
def test_ensure_utf8_stdout_reconfigures_windows_cp1252():
57+
"""Regression test for the historical Windows ``cmd.exe`` case."""
58+
stream = _StubStream(encoding="cp1252")
59+
_ensure_utf8_stdout(stream)
60+
assert stream.reconfigure_calls == [{"encoding": "utf-8", "errors": "replace"}]
61+
62+
63+
def test_ensure_utf8_stdout_skips_non_textio_streams():
64+
class NotATextIO:
65+
encoding = "latin-1"
66+
reconfigure_calls: list[dict[str, Any]] = []
67+
68+
def reconfigure(self, **kwargs: Any) -> None: # pragma: no cover - unused
69+
self.reconfigure_calls.append(kwargs)
70+
71+
stream = NotATextIO()
72+
_ensure_utf8_stdout(stream)
73+
assert stream.reconfigure_calls == []
74+
75+
76+
def test_ensure_utf8_stdout_after_reconfigure_can_emit_emoji():
77+
"""End-to-end: after reconfiguration, writing an emoji must not raise."""
78+
stream = _StubStream(encoding="latin-1")
79+
_ensure_utf8_stdout(stream)
80+
81+
# Should not raise UnicodeEncodeError; ``errors="replace"`` lets
82+
# genuinely-unrenderable bytes fall through as ``?`` instead of
83+
# crashing the whole command.
84+
stream.write("Configuration complete \U0001f680")
85+
stream.flush()

0 commit comments

Comments
 (0)