diff --git a/CHANGES.txt b/CHANGES.txt index 567c96f7d58..be036c9e775 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -18,7 +18,7 @@ existing options have been made non functional but will still be accepted until their removal in pip v1.8. For more information please see https://pip.pypa.io/en/latest/reference/pip_install.html#caching - + * Added a virtualenv-specific configuration file. (:pull:`1364`) * Added site-wide configuation files. (:pull:`1978`) @@ -41,7 +41,7 @@ * Fixed :issue:`1618`. Pip no longer adds duplicate logging consumers, so it won't create duplicate output when being called multiple times. (:pull:`1723`) - + * Added general flag `--no-check-certificate` to tell pip NOT to verify the security of HTTPS connections. (:pull:`1741`:) @@ -67,13 +67,16 @@ * Fixed :issue:`1180`. Added support to respect proxies in ``pip search``. It also fixes :issue:`932` and :issue:`1104`. (:pull:`1902`) - + * Fixed :issue:`798` and :issue:`1060`. `pip install --download` works with vcs links. (:pull:`1926`) * Fixed :issue:`1456`. Disabled warning about insecure index host when using localhost. Based off of Guy Rozendorn's work in :pull:`1718`. (:pull:`1967`) +* Allow the use of OS standard user configuration files instead of ones simply + based around ``$HOME``. (:pull:`2021`) + **1.5.7** diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 36c50de00f5..b3668a1df79 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -242,6 +242,15 @@ all users) configuration: **Per-user**: +* On Unix the default configuration file is: :file:`$HOME/.config/pip/pip.conf` + which respects the ``XDG_CONFIG_HOME`` environment variable. +* On Mac OS X the configuration file is + :file:`$HOME/Library/Application Support/pip/pip.conf`. +* On Windows the configuration file is :file:`%APPDATA%\\pip\\pip.ini`. + +There are also a legacy per-user configuration file which is also respected, +these are located at: + * On Unix and Mac OS X the configuration file is: :file:`$HOME/.pip/pip.conf` * On Windows the configuration file is: :file:`%HOME%\\pip\\pip.ini` diff --git a/pip/baseparser.py b/pip/baseparser.py index 5e3c0609e9e..27abaf7536b 100644 --- a/pip/baseparser.py +++ b/pip/baseparser.py @@ -10,10 +10,10 @@ from pip._vendor.six import string_types from pip._vendor.six.moves import configparser from pip.locations import ( - default_config_file, default_config_basename, running_under_virtualenv, + legacy_config_file, config_basename, running_under_virtualenv, site_config_files ) -from pip.utils import get_terminal_size +from pip.utils import appdirs, get_terminal_size class PrettyHelpFormatter(optparse.IndentedHelpFormatter): @@ -154,13 +154,21 @@ def get_config_files(self): if config_file and os.path.exists(config_file): files.append(config_file) else: - files.append(default_config_file) + # This is the legacy config file, we consider it to be a lower + # priority than the new file location. + files.append(legacy_config_file) + + # This is the new config file, we consider it to be a higher + # priority than the legacy file. + files.append( + os.path.join(appdirs.user_config_dir("pip"), config_basename) + ) # finally virtualenv configuration first trumping others if running_under_virtualenv(): venv_config_file = os.path.join( sys.prefix, - default_config_basename, + config_basename, ) if os.path.exists(venv_config_file): files.append(venv_config_file) diff --git a/pip/locations.py b/pip/locations.py index 56850b52361..ff301462cf0 100644 --- a/pip/locations.py +++ b/pip/locations.py @@ -179,20 +179,24 @@ def _get_build_prefix(): if not os.path.exists(bin_py): bin_py = os.path.join(sys.prefix, 'bin') bin_user = os.path.join(user_site, 'bin') - default_storage_dir = os.path.join(user_dir, 'pip') - default_config_basename = 'pip.ini' - default_config_file = os.path.join( - default_storage_dir, - default_config_basename, + + config_basename = 'pip.ini' + + legacy_storage_dir = os.path.join(user_dir, 'pip') + legacy_config_file = os.path.join( + legacy_storage_dir, + config_basename, ) else: bin_py = os.path.join(sys.prefix, 'bin') bin_user = os.path.join(user_site, 'bin') - default_storage_dir = os.path.join(user_dir, '.pip') - default_config_basename = 'pip.conf' - default_config_file = os.path.join( - default_storage_dir, - default_config_basename, + + config_basename = 'pip.conf' + + legacy_storage_dir = os.path.join(user_dir, '.pip') + legacy_config_file = os.path.join( + legacy_storage_dir, + config_basename, ) # Forcing to use /usr/local/bin for standard Mac OS X framework installs @@ -201,7 +205,7 @@ def _get_build_prefix(): bin_py = '/usr/local/bin' site_config_files = [ - os.path.join(path, default_config_basename) + os.path.join(path, config_basename) for path in appdirs.site_config_dirs('pip') ] diff --git a/pip/utils/appdirs.py b/pip/utils/appdirs.py index 4a4c69de334..4ecc1979c9b 100644 --- a/pip/utils/appdirs.py +++ b/pip/utils/appdirs.py @@ -130,6 +130,37 @@ def user_log_dir(appname): return path +def user_config_dir(appname, roaming=True): + """Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "roaming" (boolean, default True) can be set False to not use the + Windows roaming appdata directory. That means that for users on a + Windows network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. + That means, by deafult "~/.config/". + """ + if WINDOWS: + path = user_data_dir(appname, roaming=roaming) + elif sys.platform == "darwin": + path = user_data_dir(appname) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + path = os.path.join(path, appname) + + return path + + # for the discussion regarding site_config_dirs locations # see def site_config_dirs(appname): diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index d051290287a..b00106b642e 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -172,9 +172,9 @@ def test_options_from_venv_config(script, virtualenv): Test if ConfigOptionParser reads a virtualenv-local config file """ - from pip.locations import default_config_basename + from pip.locations import config_basename conf = "[global]\nno-index = true" - ini = virtualenv.location / default_config_basename + ini = virtualenv.location / config_basename with open(ini, 'w') as f: f.write(conf) result = script.pip('install', '-vvv', 'INITools', expect_error=True) diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index 9e0b25e80b7..c95872a329a 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -153,6 +153,66 @@ def test_user_data_dir_linux_override(self, monkeypatch): assert appdirs.user_data_dir("pip") == "/home/test/.other-share/pip" +class TestUserConfigDir: + + def test_user_config_dir_win_no_roaming(self, monkeypatch): + @pretend.call_recorder + def _get_win_folder(base): + return "C:\\Users\\test\\AppData\\Local" + + monkeypatch.setattr( + appdirs, + "_get_win_folder", + _get_win_folder, + raising=False, + ) + monkeypatch.setattr(appdirs, "WINDOWS", True) + + assert ( + appdirs.user_config_dir("pip", roaming=False).replace("/", "\\") + == "C:\\Users\\test\\AppData\\Local\\pip" + ) + assert _get_win_folder.calls == [pretend.call("CSIDL_LOCAL_APPDATA")] + + def test_user_config_dir_win_yes_roaming(self, monkeypatch): + @pretend.call_recorder + def _get_win_folder(base): + return "C:\\Users\\test\\AppData\\Roaming" + + monkeypatch.setattr( + appdirs, + "_get_win_folder", + _get_win_folder, + raising=False, + ) + monkeypatch.setattr(appdirs, "WINDOWS", True) + + assert (appdirs.user_config_dir("pip").replace("/", "\\") + == "C:\\Users\\test\\AppData\\Roaming\\pip") + assert _get_win_folder.calls == [pretend.call("CSIDL_APPDATA")] + + def test_user_config_dir_osx(self, monkeypatch): + monkeypatch.setenv("HOME", "/home/test") + monkeypatch.setattr(sys, "platform", "darwin") + + assert (appdirs.user_config_dir("pip") + == "/home/test/Library/Application Support/pip") + + def test_user_config_dir_linux(self, monkeypatch): + monkeypatch.delenv("XDG_CONFIG_HOME") + monkeypatch.setenv("HOME", "/home/test") + monkeypatch.setattr(sys, "platform", "linux2") + + assert appdirs.user_config_dir("pip") == "/home/test/.config/pip" + + def test_user_config_dir_linux_override(self, monkeypatch): + monkeypatch.setenv("XDG_CONFIG_HOME", "/home/test/.other-config") + monkeypatch.setenv("HOME", "/home/test") + monkeypatch.setattr(sys, "platform", "linux2") + + assert appdirs.user_config_dir("pip") == "/home/test/.other-config/pip" + + class TestUserLogDir: def test_user_log_dir_win(self, monkeypatch): diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 7f38217eed3..75887226987 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -287,4 +287,4 @@ def test_venv_config_file_found(self, monkeypatch): ) monkeypatch.setattr(os.path, 'exists', lambda filename: True) cp = pip.baseparser.ConfigOptionParser() - assert len(cp.get_config_files()) == 3 + assert len(cp.get_config_files()) == 4