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