diff --git a/nox/popen.py b/nox/popen.py index 48fc9726..c152f7d0 100644 --- a/nox/popen.py +++ b/nox/popen.py @@ -12,11 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +import locale import subprocess import sys from typing import IO, Mapping, Sequence, Tuple, Union +def decode_output(output: bytes) -> str: + """Try to decode the given bytes with encodings from the system. + + :param output: output to decode + :raises UnicodeDecodeError: if all encodings fail + :return: decoded string + """ + try: + return output.decode("utf-8") + except UnicodeDecodeError: + second_encoding = locale.getpreferredencoding() + if second_encoding.casefold() in ("utf8", "utf-8"): + raise + + return output.decode(second_encoding) + + def popen( args: Sequence[str], env: Mapping[str, str] = None, @@ -45,4 +63,4 @@ def popen( return_code = proc.wait() - return return_code, out.decode("utf-8") if out else "" + return return_code, decode_output(out) if out else "" diff --git a/tests/test_command.py b/tests/test_command.py index 45919ddb..01fb06e4 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -18,6 +18,7 @@ from unittest import mock import nox.command +import nox.popen import pytest PYTHON = sys.executable @@ -294,3 +295,43 @@ def test_custom_stderr_failed_command(capsys, tmpdir): tempfile_contents = stderr.read().decode("utf-8") assert "out" not in tempfile_contents assert "err" in tempfile_contents + + +def test_output_decoding() -> None: + result = nox.popen.decode_output(b"abc") + + assert result == "abc" + + +def test_output_decoding_non_ascii() -> None: + result = nox.popen.decode_output("ü".encode("utf-8")) + + assert result == "ü" + + +def test_output_decoding_utf8_only_fail(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "utf8") + + with pytest.raises(UnicodeDecodeError) as exc: + nox.popen.decode_output(b"\x95") + + assert exc.value.encoding == "utf-8" + + +def test_output_decoding_utf8_fail_cp1252_success( + monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "cp1252") + + result = nox.popen.decode_output(b"\x95") + + assert result == "•" # U+2022 + + +def test_output_decoding_both_fail(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(nox.popen.locale, "getpreferredencoding", lambda: "ascii") + + with pytest.raises(UnicodeDecodeError) as exc: + nox.popen.decode_output(b"\x95") + + assert exc.value.encoding == "ascii"