pip/pip/configuration.py

316 lines
10 KiB
Python
Raw Normal View History

"""Configuration management setup
Some terminology:
- name
As written in config files.
- value
Value associated with a name
- key
Name combined with it's section (section.name)
- variant
A single word describing where the configuration key-value pair came from
"""
import logging
2017-05-13 23:17:26 +02:00
import os
from pip._vendor.six import next
from pip._vendor.six.moves import configparser
2017-05-16 12:16:30 +02:00
2017-04-08 07:31:03 +02:00
from pip.exceptions import ConfigurationError
from pip.locations import (
2017-01-24 18:41:34 +01:00
legacy_config_file, new_config_file, running_under_virtualenv,
site_config_files, venv_config_file
)
from pip.utils import ensure_dir, enum
_need_file_err_msg = "Needed a specific file to be modifying."
logger = logging.getLogger(__name__)
# NOTE: Maybe use the optionx attribute to normalize keynames.
def _normalize_name(name):
"""Make a name consistent regardless of source (environment or file)
"""
name = name.lower().replace('_', '-')
if name.startswith('--'):
name = name[2:] # only prefer long opts
return name
def _disassemble_key(name):
return name.split(".", 1)
# The kinds of configurations there are.
kinds = enum(USER="user", GLOBAL="global", VENV="venv", ENV="environement")
class Configuration(object):
"""Handles management of configuration.
Provides an interface to accessing and managing configuration files.
This class converts provides an API that takes "section.key-name" style
keys and stores the value associated with it as "key-name" under the
2017-04-08 06:19:48 +02:00
section "section".
2017-04-08 06:19:48 +02:00
This allows for a clean interface wherein the both the section and the
key-name are preserved in an easy to manage form in the configuration files
and the data stored is also nice.
"""
def __init__(self, isolated, load_only=None):
super(Configuration, self).__init__()
_valid_load_only = [kinds.USER, kinds.GLOBAL, kinds.VENV, None]
if load_only not in _valid_load_only:
2017-04-08 07:31:03 +02:00
raise ConfigurationError(
"Got invalid value for load_only - should be one of {}".format(
", ".join(map(repr, _valid_load_only[:-1]))
)
)
2016-12-23 10:09:15 +01:00
self.isolated = isolated
self.load_only = load_only
# The order here determines the override order.
self._override_order = [
kinds.GLOBAL, kinds.USER, kinds.VENV, kinds.ENV
]
2017-05-14 08:38:08 +02:00
# Because we keep track of where we got the data from
self._parsers = {variant: [] for variant in self._override_order}
self._config = {variant: {} for variant in self._override_order}
self._modified_parsers = []
def load(self):
"""Loads configuration from configuration files and environment
2016-12-23 10:09:15 +01:00
"""
self._load_config_files()
2016-12-23 10:09:15 +01:00
if not self.isolated:
self._load_environment_vars()
2017-04-08 07:33:46 +02:00
def get_file(self):
"""Returns the file with highest priority in configuration
"""
try:
return self._get_parser_to_modify()[0]
except IndexError:
return None
2016-12-23 10:09:15 +01:00
def items(self):
"""Returns key-value pairs like dict.items() representing the loaded
2016-12-23 10:09:15 +01:00
configuration
"""
return self._dictionary.items()
def get_value(self, key):
2017-01-24 18:42:20 +01:00
"""Get a value from the configuration.
"""
try:
return self._dictionary[key]
except KeyError:
raise ConfigurationError("No such key - {}".format(key))
def set_value(self, key, value):
"""Modify a value in the configuration.
"""
2017-05-12 12:38:15 +02:00
self._ensure_have_load_only()
2017-05-14 08:01:56 +02:00
file_, parser = self._get_parser_to_modify()
2017-01-24 18:42:20 +01:00
if parser is not None:
section, name = _disassemble_key(key)
# Modify the parser and the configuration
if not parser.has_section(section):
parser.add_section(section)
parser.set(section, name, value)
self._config[self.load_only][key] = value
2017-05-14 08:01:56 +02:00
self._mark_as_modified(file_, parser)
def unset_value(self, key):
2017-01-24 18:42:20 +01:00
"""Unset a value in the configuration.
"""
2017-05-12 12:38:15 +02:00
self._ensure_have_load_only()
2017-01-24 18:42:20 +01:00
if key not in self._config[self.load_only]:
raise ConfigurationError("No such key - {}".format(key))
2017-01-24 18:42:20 +01:00
2017-05-14 08:01:56 +02:00
file_, parser = self._get_parser_to_modify()
2017-01-24 18:42:20 +01:00
if parser is not None:
section, name = _disassemble_key(key)
# Remove the key in the parser
modified_something = (
parser.has_section(section) and
parser.remove_option(section, name)
)
if modified_something:
# name removed from parser, section may now be empty
if next(iter(parser.items(section)), None) is None:
parser.remove_section(section)
2017-05-14 08:01:56 +02:00
self._mark_as_modified(file_, parser)
2017-01-24 18:42:20 +01:00
else:
2017-04-08 07:31:03 +02:00
raise ConfigurationError(
2017-05-12 12:34:37 +02:00
"Fatal Internal error [id=1]. Please report as a bug."
2017-04-08 07:31:03 +02:00
)
del self._config[self.load_only][key]
def save(self):
2017-05-12 12:38:15 +02:00
self._ensure_have_load_only()
2017-05-14 08:01:56 +02:00
for file_, parser in self._modified_parsers:
logger.info("Writing to %s", file_)
# Ensure directory exists.
2017-05-14 08:01:56 +02:00
ensure_dir(os.path.dirname(file_))
2017-05-14 08:01:56 +02:00
with open(file_, "w") as f:
parser.write(f)
#
# Private routines
#
2017-05-12 12:38:15 +02:00
def _ensure_have_load_only(self):
if self.load_only is None:
raise ConfigurationError("Needed a specific file to be modifying.")
@property
def _dictionary(self):
"""A dictionary representing the loaded configuration.
"""
# NOTE: Dictionaries are not populated if not loaded. So, conditionals
# are not needed here.
retval = {}
for variant in self._override_order:
retval.update(self._config[variant])
2016-12-23 10:09:15 +01:00
return retval
def _load_config_files(self):
"""Loads configuration from configuration files
"""
2017-05-12 08:55:27 +02:00
config_files = dict(self._get_config_files())
if config_files[kinds.ENV][0:1] == [os.devnull]:
2017-05-12 08:55:27 +02:00
logger.debug(
"Skipping loading configuration files due to "
2017-05-15 09:59:41 +02:00
"environment's PIP_CONFIG being os.devnull"
2017-05-12 08:55:27 +02:00
)
return
2017-05-12 08:55:27 +02:00
for variant, files in config_files.items():
2017-05-14 08:01:56 +02:00
for file_ in files:
# If there's specific variant set in `load_only`, load only
# that variant, not the others.
if self.load_only is not None and variant != self.load_only:
continue
2017-05-14 08:01:56 +02:00
parser = self._load_file(variant, file_)
# Keeping track of the parsers used
2017-05-14 08:01:56 +02:00
self._parsers[variant].append((file_, parser))
2017-05-12 12:42:35 +02:00
# XXX: This is patched in the tests.
2017-05-14 08:01:56 +02:00
def _load_file(self, variant, file_):
logger.debug("For variant '%s', will try loading '%s'", variant, file_)
parser = self._construct_parser(file_)
for section in parser.sections():
2017-05-12 08:56:25 +02:00
items = parser.items(section)
self._config[variant].update(self._normalized_keys(section, items))
return parser
2017-05-14 08:01:56 +02:00
def _construct_parser(self, file_):
parser = configparser.RawConfigParser()
# If there is no such file, don't bother reading it but create the
# parser anyway, to hold the data.
# Doing this is useful when modifying and saving files, where we don't
# need to construct a parser.
2017-05-14 08:01:56 +02:00
if os.path.exists(file_):
parser.read(file_)
return parser
2016-12-23 10:09:15 +01:00
def _load_environment_vars(self):
"""Loads configuration from environment variables
"""
self._config[kinds.ENV].update(
self._normalized_keys(":env:", self._get_environ_vars())
)
def _normalized_keys(self, section, items):
"""Normalizes items to construct a dictionary with normalized keys.
This routine is where the names become keys and are made the same
regardless of source - configuration files or environment.
"""
normalized = {}
for name, val in items:
2017-05-15 10:01:34 +02:00
key = section + "." + _normalize_name(name)
normalized[key] = val
return normalized
def _get_environ_vars(self):
"""Returns a generator with all environmental vars with prefix PIP_"""
for key, val in os.environ.items():
if key.startswith("PIP_"):
yield key[4:].lower(), val
2016-12-23 10:09:15 +01:00
def _get_config_files(self):
2017-04-08 06:19:48 +02:00
"""Yields variant and configuration files associated with it.
This should be treated like items of a dictionary.
"""
2017-05-12 08:55:27 +02:00
# SMELL: Move the conditions out of this function
2017-04-08 06:19:48 +02:00
# environment variables have the lowest priority
2017-05-15 09:59:41 +02:00
config_file = os.environ.get('PIP_CONFIG', None)
2017-05-12 08:55:27 +02:00
if config_file is not None:
yield kinds.ENV, [config_file]
2017-05-12 08:55:27 +02:00
else:
yield kinds.ENV, []
2017-05-15 09:59:41 +02:00
# at the base we have any global configuration
yield kinds.GLOBAL, list(site_config_files)
2017-05-15 09:59:41 +02:00
# per-user configuration next
should_load_user_config = not self.isolated and not (
config_file and os.path.exists(config_file)
)
2017-05-15 09:59:41 +02:00
if should_load_user_config:
2017-04-08 07:35:49 +02:00
# The legacy config file is overridden by the new config file
yield kinds.USER, [legacy_config_file, new_config_file]
# finally virtualenv configuration first trumping others
if running_under_virtualenv():
yield kinds.VENV, [venv_config_file]
def _get_parser_to_modify(self):
# Determine which parser to modify
parsers = self._parsers[self.load_only]
if not parsers:
2017-04-08 07:31:03 +02:00
# This should not happen if everything works correctly.
raise ConfigurationError(
2017-05-12 12:34:37 +02:00
"Fatal Internal error [id=2]. Please report as a bug."
2017-04-08 07:31:03 +02:00
)
# Use the highest priority parser.
return parsers[-1]
2017-04-08 07:35:49 +02:00
2017-05-12 12:42:35 +02:00
# XXX: This is patched in the tests.
2017-05-14 08:01:56 +02:00
def _mark_as_modified(self, file_, parser):
file_parser_tuple = (file_, parser)
2017-04-08 07:35:49 +02:00
if file_parser_tuple not in self._modified_parsers:
self._modified_parsers.append(file_parser_tuple)