Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@

* Added pip completion support for fish shell.

* Fix problems on Windows on Python 2 when username or hostname contains
non-ASCII characters (:issue:`3463`, :pull:`3970`, :pull:`4000`).

* Use git fetch --tags to fetch tags in addition to everything else that
is normally fetched; this is necessary in case a git requirement url
points to a tag or commit that is not on a branch (:pull:`3791`)
Expand Down
24 changes: 24 additions & 0 deletions pip/utils/appdirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sys

from pip.compat import WINDOWS, expanduser
from pip._vendor.six import PY2, text_type


def user_cache_dir(appname):
Expand Down Expand Up @@ -35,6 +36,11 @@ def user_cache_dir(appname):
# Get the base path
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))

# When using Python 2, return paths as bytes on Windows like we do on
# other operating systems. See helper function docs for more details.
if PY2 and isinstance(path, text_type):
path = _win_path_to_bytes(path)

# Add our app name and Cache directory to it
path = os.path.join(path, appname, "Cache")
elif sys.platform == "darwin":
Expand Down Expand Up @@ -222,3 +228,21 @@ def _get_win_folder_with_ctypes(csidl_name):
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
_get_win_folder = _get_win_folder_from_registry


def _win_path_to_bytes(path):
"""Encode Windows paths to bytes. Only used on Python 2.

Motivation is to be consistent with other operating systems where paths
are also returned as bytes. This avoids problems mixing bytes and Unicode
elsewhere in the codebase. For more details and discussion see
<https://github.com/pypa/pip/issues/3463>.

If encoding using ASCII and MBCS fails, return the original Unicode path.
"""
for encoding in ('ASCII', 'MBCS'):
try:
return path.encode(encoding)
except (UnicodeEncodeError, LookupError):
pass
return path
19 changes: 19 additions & 0 deletions tests/unit/test_appdirs.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,25 @@ def test_user_cache_dir_linux_home_slash(self, monkeypatch):

assert appdirs.user_cache_dir("pip") == "/.cache/pip"

def test_user_cache_dir_unicode(self, monkeypatch):
if sys.platform != 'win32':
return

def my_get_win_folder(csidl_name):
return u"\u00DF\u00E4\u03B1\u20AC"

monkeypatch.setattr(appdirs, "_get_win_folder", my_get_win_folder)

# Do not use the isinstance expression directly in the
# assert statement, as the Unicode characters in the result
# cause pytest to fail with an internal error on Python 2.7
result_is_str = isinstance(appdirs.user_cache_dir('test'), str)
assert result_is_str, "user_cache_dir did not return a str"

# Test against regression #3463
from pip import create_main_parser
create_main_parser().print_help() # This should not crash


class TestSiteConfigDirs:

Expand Down