python (3.11.7)
       1  import logging
       2  import os
       3  import subprocess
       4  from optparse import Values
       5  from typing import Any, List, Optional
       6  
       7  from pip._internal.cli.base_command import Command
       8  from pip._internal.cli.status_codes import ERROR, SUCCESS
       9  from pip._internal.configuration import (
      10      Configuration,
      11      Kind,
      12      get_configuration_files,
      13      kinds,
      14  )
      15  from pip._internal.exceptions import PipError
      16  from pip._internal.utils.logging import indent_log
      17  from pip._internal.utils.misc import get_prog, write_output
      18  
      19  logger = logging.getLogger(__name__)
      20  
      21  
      22  class ESC[4;38;5;81mConfigurationCommand(ESC[4;38;5;149mCommand):
      23      """
      24      Manage local and global configuration.
      25  
      26      Subcommands:
      27  
      28      - list: List the active configuration (or from the file specified)
      29      - edit: Edit the configuration file in an editor
      30      - get: Get the value associated with command.option
      31      - set: Set the command.option=value
      32      - unset: Unset the value associated with command.option
      33      - debug: List the configuration files and values defined under them
      34  
      35      Configuration keys should be dot separated command and option name,
      36      with the special prefix "global" affecting any command. For example,
      37      "pip config set global.index-url https://example.org/" would configure
      38      the index url for all commands, but "pip config set download.timeout 10"
      39      would configure a 10 second timeout only for "pip download" commands.
      40  
      41      If none of --user, --global and --site are passed, a virtual
      42      environment configuration file is used if one is active and the file
      43      exists. Otherwise, all modifications happen to the user file by
      44      default.
      45      """
      46  
      47      ignore_require_venv = True
      48      usage = """
      49          %prog [<file-option>] list
      50          %prog [<file-option>] [--editor <editor-path>] edit
      51  
      52          %prog [<file-option>] get command.option
      53          %prog [<file-option>] set command.option value
      54          %prog [<file-option>] unset command.option
      55          %prog [<file-option>] debug
      56      """
      57  
      58      def add_options(self) -> None:
      59          self.cmd_opts.add_option(
      60              "--editor",
      61              dest="editor",
      62              action="store",
      63              default=None,
      64              help=(
      65                  "Editor to use to edit the file. Uses VISUAL or EDITOR "
      66                  "environment variables if not provided."
      67              ),
      68          )
      69  
      70          self.cmd_opts.add_option(
      71              "--global",
      72              dest="global_file",
      73              action="store_true",
      74              default=False,
      75              help="Use the system-wide configuration file only",
      76          )
      77  
      78          self.cmd_opts.add_option(
      79              "--user",
      80              dest="user_file",
      81              action="store_true",
      82              default=False,
      83              help="Use the user configuration file only",
      84          )
      85  
      86          self.cmd_opts.add_option(
      87              "--site",
      88              dest="site_file",
      89              action="store_true",
      90              default=False,
      91              help="Use the current environment configuration file only",
      92          )
      93  
      94          self.parser.insert_option_group(0, self.cmd_opts)
      95  
      96      def run(self, options: Values, args: List[str]) -> int:
      97          handlers = {
      98              "list": self.list_values,
      99              "edit": self.open_in_editor,
     100              "get": self.get_name,
     101              "set": self.set_name_value,
     102              "unset": self.unset_name,
     103              "debug": self.list_config_values,
     104          }
     105  
     106          # Determine action
     107          if not args or args[0] not in handlers:
     108              logger.error(
     109                  "Need an action (%s) to perform.",
     110                  ", ".join(sorted(handlers)),
     111              )
     112              return ERROR
     113  
     114          action = args[0]
     115  
     116          # Determine which configuration files are to be loaded
     117          #    Depends on whether the command is modifying.
     118          try:
     119              load_only = self._determine_file(
     120                  options, need_value=(action in ["get", "set", "unset", "edit"])
     121              )
     122          except PipError as e:
     123              logger.error(e.args[0])
     124              return ERROR
     125  
     126          # Load a new configuration
     127          self.configuration = Configuration(
     128              isolated=options.isolated_mode, load_only=load_only
     129          )
     130          self.configuration.load()
     131  
     132          # Error handling happens here, not in the action-handlers.
     133          try:
     134              handlers[action](options, args[1:])
     135          except PipError as e:
     136              logger.error(e.args[0])
     137              return ERROR
     138  
     139          return SUCCESS
     140  
     141      def _determine_file(self, options: Values, need_value: bool) -> Optional[Kind]:
     142          file_options = [
     143              key
     144              for key, value in (
     145                  (kinds.USER, options.user_file),
     146                  (kinds.GLOBAL, options.global_file),
     147                  (kinds.SITE, options.site_file),
     148              )
     149              if value
     150          ]
     151  
     152          if not file_options:
     153              if not need_value:
     154                  return None
     155              # Default to user, unless there's a site file.
     156              elif any(
     157                  os.path.exists(site_config_file)
     158                  for site_config_file in get_configuration_files()[kinds.SITE]
     159              ):
     160                  return kinds.SITE
     161              else:
     162                  return kinds.USER
     163          elif len(file_options) == 1:
     164              return file_options[0]
     165  
     166          raise PipError(
     167              "Need exactly one file to operate upon "
     168              "(--user, --site, --global) to perform."
     169          )
     170  
     171      def list_values(self, options: Values, args: List[str]) -> None:
     172          self._get_n_args(args, "list", n=0)
     173  
     174          for key, value in sorted(self.configuration.items()):
     175              write_output("%s=%r", key, value)
     176  
     177      def get_name(self, options: Values, args: List[str]) -> None:
     178          key = self._get_n_args(args, "get [name]", n=1)
     179          value = self.configuration.get_value(key)
     180  
     181          write_output("%s", value)
     182  
     183      def set_name_value(self, options: Values, args: List[str]) -> None:
     184          key, value = self._get_n_args(args, "set [name] [value]", n=2)
     185          self.configuration.set_value(key, value)
     186  
     187          self._save_configuration()
     188  
     189      def unset_name(self, options: Values, args: List[str]) -> None:
     190          key = self._get_n_args(args, "unset [name]", n=1)
     191          self.configuration.unset_value(key)
     192  
     193          self._save_configuration()
     194  
     195      def list_config_values(self, options: Values, args: List[str]) -> None:
     196          """List config key-value pairs across different config files"""
     197          self._get_n_args(args, "debug", n=0)
     198  
     199          self.print_env_var_values()
     200          # Iterate over config files and print if they exist, and the
     201          # key-value pairs present in them if they do
     202          for variant, files in sorted(self.configuration.iter_config_files()):
     203              write_output("%s:", variant)
     204              for fname in files:
     205                  with indent_log():
     206                      file_exists = os.path.exists(fname)
     207                      write_output("%s, exists: %r", fname, file_exists)
     208                      if file_exists:
     209                          self.print_config_file_values(variant)
     210  
     211      def print_config_file_values(self, variant: Kind) -> None:
     212          """Get key-value pairs from the file of a variant"""
     213          for name, value in self.configuration.get_values_in_config(variant).items():
     214              with indent_log():
     215                  write_output("%s: %s", name, value)
     216  
     217      def print_env_var_values(self) -> None:
     218          """Get key-values pairs present as environment variables"""
     219          write_output("%s:", "env_var")
     220          with indent_log():
     221              for key, value in sorted(self.configuration.get_environ_vars()):
     222                  env_var = f"PIP_{key.upper()}"
     223                  write_output("%s=%r", env_var, value)
     224  
     225      def open_in_editor(self, options: Values, args: List[str]) -> None:
     226          editor = self._determine_editor(options)
     227  
     228          fname = self.configuration.get_file_to_edit()
     229          if fname is None:
     230              raise PipError("Could not determine appropriate file.")
     231          elif '"' in fname:
     232              # This shouldn't happen, unless we see a username like that.
     233              # If that happens, we'd appreciate a pull request fixing this.
     234              raise PipError(
     235                  f'Can not open an editor for a file name containing "\n{fname}'
     236              )
     237  
     238          try:
     239              subprocess.check_call(f'{editor} "{fname}"', shell=True)
     240          except FileNotFoundError as e:
     241              if not e.filename:
     242                  e.filename = editor
     243              raise
     244          except subprocess.CalledProcessError as e:
     245              raise PipError(
     246                  "Editor Subprocess exited with exit code {}".format(e.returncode)
     247              )
     248  
     249      def _get_n_args(self, args: List[str], example: str, n: int) -> Any:
     250          """Helper to make sure the command got the right number of arguments"""
     251          if len(args) != n:
     252              msg = (
     253                  "Got unexpected number of arguments, expected {}. "
     254                  '(example: "{} config {}")'
     255              ).format(n, get_prog(), example)
     256              raise PipError(msg)
     257  
     258          if n == 1:
     259              return args[0]
     260          else:
     261              return args
     262  
     263      def _save_configuration(self) -> None:
     264          # We successfully ran a modifying command. Need to save the
     265          # configuration.
     266          try:
     267              self.configuration.save()
     268          except Exception:
     269              logger.exception(
     270                  "Unable to save configuration. Please report this as a bug."
     271              )
     272              raise PipError("Internal Error.")
     273  
     274      def _determine_editor(self, options: Values) -> str:
     275          if options.editor is not None:
     276              return options.editor
     277          elif "VISUAL" in os.environ:
     278              return os.environ["VISUAL"]
     279          elif "EDITOR" in os.environ:
     280              return os.environ["EDITOR"]
     281          else:
     282              raise PipError("Could not determine editor to use.")