python (3.12.0)
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.")