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