Skip to content

Commit ddfa401

Browse files
authored
Merge pull request #6683 from cjerdonek/make-subprocess-error-path-display
Support non-ascii cwds in make_subprocess_output_error()
2 parents ca017ca + 77ad476 commit ddfa401

File tree

2 files changed

+113
-8
lines changed

2 files changed

+113
-8
lines changed

src/pip/_internal/utils/misc.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
2323
# why we ignore the type on this import.
2424
from pip._vendor.retrying import retry # type: ignore
25-
from pip._vendor.six import PY2
25+
from pip._vendor.six import PY2, text_type
2626
from pip._vendor.six.moves import input, shlex_quote
2727
from pip._vendor.six.moves.urllib import parse as urllib_parse
2828
from pip._vendor.six.moves.urllib import request as urllib_request
@@ -186,6 +186,40 @@ def rmtree_errorhandler(func, path, exc_info):
186186
raise
187187

188188

189+
def path_to_display(path):
190+
# type: (Optional[Union[str, Text]]) -> Optional[Text]
191+
"""
192+
Convert a bytes (or text) path to text (unicode in Python 2) for display
193+
and logging purposes.
194+
195+
This function should never error out. Also, this function is mainly needed
196+
for Python 2 since in Python 3 str paths are already text.
197+
"""
198+
if path is None:
199+
return None
200+
if isinstance(path, text_type):
201+
return path
202+
# Otherwise, path is a bytes object (str in Python 2).
203+
try:
204+
display_path = path.decode(sys.getfilesystemencoding(), 'strict')
205+
except UnicodeDecodeError:
206+
# Include the full bytes to make troubleshooting easier, even though
207+
# it may not be very human readable.
208+
if PY2:
209+
# Convert the bytes to a readable str representation using
210+
# repr(), and then convert the str to unicode.
211+
# Also, we add the prefix "b" to the repr() return value both
212+
# to make the Python 2 output look like the Python 3 output, and
213+
# to signal to the user that this is a bytes representation.
214+
display_path = str_to_display('b{!r}'.format(path))
215+
else:
216+
# Silence the "F821 undefined name 'ascii'" flake8 error since
217+
# in Python 3 ascii() is a built-in.
218+
display_path = ascii(path) # noqa: F821
219+
220+
return display_path
221+
222+
189223
def display_path(path):
190224
# type: (Union[str, Text]) -> str
191225
"""Gives the display value for a given path, making it relative to cwd
@@ -751,11 +785,12 @@ def make_subprocess_output_error(
751785
:param lines: A list of lines, each ending with a newline.
752786
"""
753787
command = format_command_args(cmd_args)
754-
# Convert `command` to text (unicode in Python 2) so we can use it as
755-
# an argument in the unicode format string below. This avoids
788+
# Convert `command` and `cwd` to text (unicode in Python 2) so we can use
789+
# them as arguments in the unicode format string below. This avoids
756790
# "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2
757-
# when the formatted command contains a non-ascii character.
791+
# if either contains a non-ascii character.
758792
command_display = str_to_display(command, desc='command bytes')
793+
cwd_display = path_to_display(cwd)
759794

760795
# We know the joined output value ends in a newline.
761796
output = ''.join(lines)
@@ -765,12 +800,12 @@ def make_subprocess_output_error(
765800
# argument (e.g. `output`) has a non-ascii character.
766801
u'Command errored out with exit status {exit_status}:\n'
767802
' command: {command_display}\n'
768-
' cwd: {cwd}\n'
803+
' cwd: {cwd_display}\n'
769804
'Complete output ({line_count} lines):\n{output}{divider}'
770805
).format(
771806
exit_status=exit_status,
772807
command_display=command_display,
773-
cwd=cwd,
808+
cwd_display=cwd_display,
774809
line_count=len(lines),
775810
output=output,
776811
divider=LOG_DIVIDER,

tests/unit/test_utils.py

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
from pip._internal.utils.misc import (
3333
call_subprocess, egg_link_path, ensure_dir, format_command_args,
3434
get_installed_distributions, get_prog, make_subprocess_output_error,
35-
normalize_path, normalize_version_info, path_to_url, redact_netloc,
36-
redact_password_from_url, remove_auth_from_url, rmtree,
35+
normalize_path, normalize_version_info, path_to_display, path_to_url,
36+
redact_netloc, redact_password_from_url, remove_auth_from_url, rmtree,
3737
split_auth_from_netloc, split_auth_netloc_from_url, untar_file, unzip_file,
3838
)
3939
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
@@ -384,6 +384,24 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch):
384384
rmtree('foo')
385385

386386

387+
@pytest.mark.parametrize('path, fs_encoding, expected', [
388+
(None, None, None),
389+
# Test passing a text (unicode) string.
390+
(u'/path/déf', None, u'/path/déf'),
391+
# Test a bytes object with a non-ascii character.
392+
(u'/path/déf'.encode('utf-8'), 'utf-8', u'/path/déf'),
393+
# Test a bytes object with a character that can't be decoded.
394+
(u'/path/déf'.encode('utf-8'), 'ascii', u"b'/path/d\\xc3\\xa9f'"),
395+
(u'/path/déf'.encode('utf-16'), 'utf-8',
396+
u"b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/"
397+
"\\x00d\\x00\\xe9\\x00f\\x00'"),
398+
])
399+
def test_path_to_display(monkeypatch, path, fs_encoding, expected):
400+
monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: fs_encoding)
401+
actual = path_to_display(path)
402+
assert actual == expected, 'actual: {!r}'.format(actual)
403+
404+
387405
class Test_normalize_path(object):
388406
# Technically, symlinks are possible on Windows, but you need a special
389407
# permission bit to create them, and Python 2 doesn't support it anyway, so
@@ -796,6 +814,58 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch):
796814
assert actual == expected, u'actual: {}'.format(actual)
797815

798816

817+
@pytest.mark.skipif("sys.version_info < (3,)")
818+
def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch):
819+
"""
820+
Test a str (text) cwd with a non-ascii character in Python 3.
821+
"""
822+
cmd_args = ['test']
823+
cwd = '/path/to/cwd/déf'
824+
actual = make_subprocess_output_error(
825+
cmd_args=cmd_args,
826+
cwd=cwd,
827+
lines=[],
828+
exit_status=1,
829+
)
830+
expected = dedent("""\
831+
Command errored out with exit status 1:
832+
command: test
833+
cwd: /path/to/cwd/déf
834+
Complete output (0 lines):
835+
----------------------------------------""")
836+
assert actual == expected, 'actual: {}'.format(actual)
837+
838+
839+
@pytest.mark.parametrize('encoding', [
840+
'utf-8',
841+
# Test a Windows encoding.
842+
'cp1252',
843+
])
844+
@pytest.mark.skipif("sys.version_info >= (3,)")
845+
def test_make_subprocess_output_error__non_ascii_cwd_python_2(
846+
monkeypatch, encoding,
847+
):
848+
"""
849+
Test a str (bytes object) cwd with a non-ascii character in Python 2.
850+
"""
851+
cmd_args = ['test']
852+
cwd = u'/path/to/cwd/déf'.encode(encoding)
853+
monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: encoding)
854+
actual = make_subprocess_output_error(
855+
cmd_args=cmd_args,
856+
cwd=cwd,
857+
lines=[],
858+
exit_status=1,
859+
)
860+
expected = dedent(u"""\
861+
Command errored out with exit status 1:
862+
command: test
863+
cwd: /path/to/cwd/déf
864+
Complete output (0 lines):
865+
----------------------------------------""")
866+
assert actual == expected, u'actual: {}'.format(actual)
867+
868+
799869
# This test is mainly important for checking unicode in Python 2.
800870
def test_make_subprocess_output_error__non_ascii_line():
801871
"""

0 commit comments

Comments
 (0)