python (3.11.7)

(root)/
lib/
python3.11/
site-packages/
pip/
_internal/
configuration.py
       1  """Configuration management setup
       2  
       3  Some terminology:
       4  - name
       5    As written in config files.
       6  - value
       7    Value associated with a name
       8  - key
       9    Name combined with it's section (section.name)
      10  - variant
      11    A single word describing where the configuration key-value pair came from
      12  """
      13  
      14  import configparser
      15  import locale
      16  import os
      17  import sys
      18  from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple
      19  
      20  from pip._internal.exceptions import (
      21      ConfigurationError,
      22      ConfigurationFileCouldNotBeLoaded,
      23  )
      24  from pip._internal.utils import appdirs
      25  from pip._internal.utils.compat import WINDOWS
      26  from pip._internal.utils.logging import getLogger
      27  from pip._internal.utils.misc import ensure_dir, enum
      28  
      29  RawConfigParser = configparser.RawConfigParser  # Shorthand
      30  Kind = NewType("Kind", str)
      31  
      32  CONFIG_BASENAME = "pip.ini" if WINDOWS else "pip.conf"
      33  ENV_NAMES_IGNORED = "version", "help"
      34  
      35  # The kinds of configurations there are.
      36  kinds = enum(
      37      USER="user",  # User Specific
      38      GLOBAL="global",  # System Wide
      39      SITE="site",  # [Virtual] Environment Specific
      40      ENV="env",  # from PIP_CONFIG_FILE
      41      ENV_VAR="env-var",  # from Environment Variables
      42  )
      43  OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR
      44  VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE
      45  
      46  logger = getLogger(__name__)
      47  
      48  
      49  # NOTE: Maybe use the optionx attribute to normalize keynames.
      50  def _normalize_name(name: str) -> str:
      51      """Make a name consistent regardless of source (environment or file)"""
      52      name = name.lower().replace("_", "-")
      53      if name.startswith("--"):
      54          name = name[2:]  # only prefer long opts
      55      return name
      56  
      57  
      58  def _disassemble_key(name: str) -> List[str]:
      59      if "." not in name:
      60          error_message = (
      61              "Key does not contain dot separated section and key. "
      62              "Perhaps you wanted to use 'global.{}' instead?"
      63          ).format(name)
      64          raise ConfigurationError(error_message)
      65      return name.split(".", 1)
      66  
      67  
      68  def get_configuration_files() -> Dict[Kind, List[str]]:
      69      global_config_files = [
      70          os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs("pip")
      71      ]
      72  
      73      site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME)
      74      legacy_config_file = os.path.join(
      75          os.path.expanduser("~"),
      76          "pip" if WINDOWS else ".pip",
      77          CONFIG_BASENAME,
      78      )
      79      new_config_file = os.path.join(appdirs.user_config_dir("pip"), CONFIG_BASENAME)
      80      return {
      81          kinds.GLOBAL: global_config_files,
      82          kinds.SITE: [site_config_file],
      83          kinds.USER: [legacy_config_file, new_config_file],
      84      }
      85  
      86  
      87  class ESC[4;38;5;81mConfiguration:
      88      """Handles management of configuration.
      89  
      90      Provides an interface to accessing and managing configuration files.
      91  
      92      This class converts provides an API that takes "section.key-name" style
      93      keys and stores the value associated with it as "key-name" under the
      94      section "section".
      95  
      96      This allows for a clean interface wherein the both the section and the
      97      key-name are preserved in an easy to manage form in the configuration files
      98      and the data stored is also nice.
      99      """
     100  
     101      def __init__(self, isolated: bool, load_only: Optional[Kind] = None) -> None:
     102          super().__init__()
     103  
     104          if load_only is not None and load_only not in VALID_LOAD_ONLY:
     105              raise ConfigurationError(
     106                  "Got invalid value for load_only - should be one of {}".format(
     107                      ", ".join(map(repr, VALID_LOAD_ONLY))
     108                  )
     109              )
     110          self.isolated = isolated
     111          self.load_only = load_only
     112  
     113          # Because we keep track of where we got the data from
     114          self._parsers: Dict[Kind, List[Tuple[str, RawConfigParser]]] = {
     115              variant: [] for variant in OVERRIDE_ORDER
     116          }
     117          self._config: Dict[Kind, Dict[str, Any]] = {
     118              variant: {} for variant in OVERRIDE_ORDER
     119          }
     120          self._modified_parsers: List[Tuple[str, RawConfigParser]] = []
     121  
     122      def load(self) -> None:
     123          """Loads configuration from configuration files and environment"""
     124          self._load_config_files()
     125          if not self.isolated:
     126              self._load_environment_vars()
     127  
     128      def get_file_to_edit(self) -> Optional[str]:
     129          """Returns the file with highest priority in configuration"""
     130          assert self.load_only is not None, "Need to be specified a file to be editing"
     131  
     132          try:
     133              return self._get_parser_to_modify()[0]
     134          except IndexError:
     135              return None
     136  
     137      def items(self) -> Iterable[Tuple[str, Any]]:
     138          """Returns key-value pairs like dict.items() representing the loaded
     139          configuration
     140          """
     141          return self._dictionary.items()
     142  
     143      def get_value(self, key: str) -> Any:
     144          """Get a value from the configuration."""
     145          orig_key = key
     146          key = _normalize_name(key)
     147          try:
     148              return self._dictionary[key]
     149          except KeyError:
     150              # disassembling triggers a more useful error message than simply
     151              # "No such key" in the case that the key isn't in the form command.option
     152              _disassemble_key(key)
     153              raise ConfigurationError(f"No such key - {orig_key}")
     154  
     155      def set_value(self, key: str, value: Any) -> None:
     156          """Modify a value in the configuration."""
     157          key = _normalize_name(key)
     158          self._ensure_have_load_only()
     159  
     160          assert self.load_only
     161          fname, parser = self._get_parser_to_modify()
     162  
     163          if parser is not None:
     164              section, name = _disassemble_key(key)
     165  
     166              # Modify the parser and the configuration
     167              if not parser.has_section(section):
     168                  parser.add_section(section)
     169              parser.set(section, name, value)
     170  
     171          self._config[self.load_only][key] = value
     172          self._mark_as_modified(fname, parser)
     173  
     174      def unset_value(self, key: str) -> None:
     175          """Unset a value in the configuration."""
     176          orig_key = key
     177          key = _normalize_name(key)
     178          self._ensure_have_load_only()
     179  
     180          assert self.load_only
     181          if key not in self._config[self.load_only]:
     182              raise ConfigurationError(f"No such key - {orig_key}")
     183  
     184          fname, parser = self._get_parser_to_modify()
     185  
     186          if parser is not None:
     187              section, name = _disassemble_key(key)
     188              if not (
     189                  parser.has_section(section) and parser.remove_option(section, name)
     190              ):
     191                  # The option was not removed.
     192                  raise ConfigurationError(
     193                      "Fatal Internal error [id=1]. Please report as a bug."
     194                  )
     195  
     196              # The section may be empty after the option was removed.
     197              if not parser.items(section):
     198                  parser.remove_section(section)
     199              self._mark_as_modified(fname, parser)
     200  
     201          del self._config[self.load_only][key]
     202  
     203      def save(self) -> None:
     204          """Save the current in-memory state."""
     205          self._ensure_have_load_only()
     206  
     207          for fname, parser in self._modified_parsers:
     208              logger.info("Writing to %s", fname)
     209  
     210              # Ensure directory exists.
     211              ensure_dir(os.path.dirname(fname))
     212  
     213              # Ensure directory's permission(need to be writeable)
     214              try:
     215                  with open(fname, "w") as f:
     216                      parser.write(f)
     217              except OSError as error:
     218                  raise ConfigurationError(
     219                      f"An error occurred while writing to the configuration file "
     220                      f"{fname}: {error}"
     221                  )
     222  
     223      #
     224      # Private routines
     225      #
     226  
     227      def _ensure_have_load_only(self) -> None:
     228          if self.load_only is None:
     229              raise ConfigurationError("Needed a specific file to be modifying.")
     230          logger.debug("Will be working with %s variant only", self.load_only)
     231  
     232      @property
     233      def _dictionary(self) -> Dict[str, Any]:
     234          """A dictionary representing the loaded configuration."""
     235          # NOTE: Dictionaries are not populated if not loaded. So, conditionals
     236          #       are not needed here.
     237          retval = {}
     238  
     239          for variant in OVERRIDE_ORDER:
     240              retval.update(self._config[variant])
     241  
     242          return retval
     243  
     244      def _load_config_files(self) -> None:
     245          """Loads configuration from configuration files"""
     246          config_files = dict(self.iter_config_files())
     247          if config_files[kinds.ENV][0:1] == [os.devnull]:
     248              logger.debug(
     249                  "Skipping loading configuration files due to "
     250                  "environment's PIP_CONFIG_FILE being os.devnull"
     251              )
     252              return
     253  
     254          for variant, files in config_files.items():
     255              for fname in files:
     256                  # If there's specific variant set in `load_only`, load only
     257                  # that variant, not the others.
     258                  if self.load_only is not None and variant != self.load_only:
     259                      logger.debug("Skipping file '%s' (variant: %s)", fname, variant)
     260                      continue
     261  
     262                  parser = self._load_file(variant, fname)
     263  
     264                  # Keeping track of the parsers used
     265                  self._parsers[variant].append((fname, parser))
     266  
     267      def _load_file(self, variant: Kind, fname: str) -> RawConfigParser:
     268          logger.verbose("For variant '%s', will try loading '%s'", variant, fname)
     269          parser = self._construct_parser(fname)
     270  
     271          for section in parser.sections():
     272              items = parser.items(section)
     273              self._config[variant].update(self._normalized_keys(section, items))
     274  
     275          return parser
     276  
     277      def _construct_parser(self, fname: str) -> RawConfigParser:
     278          parser = configparser.RawConfigParser()
     279          # If there is no such file, don't bother reading it but create the
     280          # parser anyway, to hold the data.
     281          # Doing this is useful when modifying and saving files, where we don't
     282          # need to construct a parser.
     283          if os.path.exists(fname):
     284              locale_encoding = locale.getpreferredencoding(False)
     285              try:
     286                  parser.read(fname, encoding=locale_encoding)
     287              except UnicodeDecodeError:
     288                  # See https://github.com/pypa/pip/issues/4963
     289                  raise ConfigurationFileCouldNotBeLoaded(
     290                      reason=f"contains invalid {locale_encoding} characters",
     291                      fname=fname,
     292                  )
     293              except configparser.Error as error:
     294                  # See https://github.com/pypa/pip/issues/4893
     295                  raise ConfigurationFileCouldNotBeLoaded(error=error)
     296          return parser
     297  
     298      def _load_environment_vars(self) -> None:
     299          """Loads configuration from environment variables"""
     300          self._config[kinds.ENV_VAR].update(
     301              self._normalized_keys(":env:", self.get_environ_vars())
     302          )
     303  
     304      def _normalized_keys(
     305          self, section: str, items: Iterable[Tuple[str, Any]]
     306      ) -> Dict[str, Any]:
     307          """Normalizes items to construct a dictionary with normalized keys.
     308  
     309          This routine is where the names become keys and are made the same
     310          regardless of source - configuration files or environment.
     311          """
     312          normalized = {}
     313          for name, val in items:
     314              key = section + "." + _normalize_name(name)
     315              normalized[key] = val
     316          return normalized
     317  
     318      def get_environ_vars(self) -> Iterable[Tuple[str, str]]:
     319          """Returns a generator with all environmental vars with prefix PIP_"""
     320          for key, val in os.environ.items():
     321              if key.startswith("PIP_"):
     322                  name = key[4:].lower()
     323                  if name not in ENV_NAMES_IGNORED:
     324                      yield name, val
     325  
     326      # XXX: This is patched in the tests.
     327      def iter_config_files(self) -> Iterable[Tuple[Kind, List[str]]]:
     328          """Yields variant and configuration files associated with it.
     329  
     330          This should be treated like items of a dictionary.
     331          """
     332          # SMELL: Move the conditions out of this function
     333  
     334          # environment variables have the lowest priority
     335          config_file = os.environ.get("PIP_CONFIG_FILE", None)
     336          if config_file is not None:
     337              yield kinds.ENV, [config_file]
     338          else:
     339              yield kinds.ENV, []
     340  
     341          config_files = get_configuration_files()
     342  
     343          # at the base we have any global configuration
     344          yield kinds.GLOBAL, config_files[kinds.GLOBAL]
     345  
     346          # per-user configuration next
     347          should_load_user_config = not self.isolated and not (
     348              config_file and os.path.exists(config_file)
     349          )
     350          if should_load_user_config:
     351              # The legacy config file is overridden by the new config file
     352              yield kinds.USER, config_files[kinds.USER]
     353  
     354          # finally virtualenv configuration first trumping others
     355          yield kinds.SITE, config_files[kinds.SITE]
     356  
     357      def get_values_in_config(self, variant: Kind) -> Dict[str, Any]:
     358          """Get values present in a config file"""
     359          return self._config[variant]
     360  
     361      def _get_parser_to_modify(self) -> Tuple[str, RawConfigParser]:
     362          # Determine which parser to modify
     363          assert self.load_only
     364          parsers = self._parsers[self.load_only]
     365          if not parsers:
     366              # This should not happen if everything works correctly.
     367              raise ConfigurationError(
     368                  "Fatal Internal error [id=2]. Please report as a bug."
     369              )
     370  
     371          # Use the highest priority parser.
     372          return parsers[-1]
     373  
     374      # XXX: This is patched in the tests.
     375      def _mark_as_modified(self, fname: str, parser: RawConfigParser) -> None:
     376          file_parser_tuple = (fname, parser)
     377          if file_parser_tuple not in self._modified_parsers:
     378              self._modified_parsers.append(file_parser_tuple)
     379  
     380      def __repr__(self) -> str:
     381          return f"{self.__class__.__name__}({self._dictionary!r})"