diff --git a/CHANGES.txt b/CHANGES.txt index e58ff6d9916..59382f2a050 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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`) diff --git a/pip/utils/appdirs.py b/pip/utils/appdirs.py index 15ca3cd03e8..a4948cd9239 100644 --- a/pip/utils/appdirs.py +++ b/pip/utils/appdirs.py @@ -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): @@ -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": @@ -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 + . + + 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 diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index fe44c1377ee..7fba5f53a28 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -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: