1"""Base Command class, and related routines"""
2
3from __future__ import absolute_import, print_function
4
5import logging
6import logging.config
7import optparse
8import os
9import platform
10import sys
11import traceback
12
13from pip._vendor.six import PY2
14
15from pip._internal.cli import cmdoptions
16from pip._internal.cli.command_context import CommandContextMixIn
17from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
18from pip._internal.cli.status_codes import (
19    ERROR,
20    PREVIOUS_BUILD_DIR_ERROR,
21    UNKNOWN_ERROR,
22    VIRTUALENV_NOT_FOUND,
23)
24from pip._internal.exceptions import (
25    BadCommand,
26    CommandError,
27    InstallationError,
28    NetworkConnectionError,
29    PreviousBuildDirError,
30    UninstallationError,
31)
32from pip._internal.utils.deprecation import deprecated
33from pip._internal.utils.filesystem import check_path_owner
34from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
35from pip._internal.utils.misc import get_prog, normalize_path
36from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry
37from pip._internal.utils.typing import MYPY_CHECK_RUNNING
38from pip._internal.utils.virtualenv import running_under_virtualenv
39
40if MYPY_CHECK_RUNNING:
41    from optparse import Values
42    from typing import Any, List, Optional, Tuple
43
44    from pip._internal.utils.temp_dir import (
45        TempDirectoryTypeRegistry as TempDirRegistry,
46    )
47
48__all__ = ['Command']
49
50logger = logging.getLogger(__name__)
51
52
53class Command(CommandContextMixIn):
54    usage = None  # type: str
55    ignore_require_venv = False  # type: bool
56
57    def __init__(self, name, summary, isolated=False):
58        # type: (str, str, bool) -> None
59        super(Command, self).__init__()
60        parser_kw = {
61            'usage': self.usage,
62            'prog': '{} {}'.format(get_prog(), name),
63            'formatter': UpdatingDefaultsHelpFormatter(),
64            'add_help_option': False,
65            'name': name,
66            'description': self.__doc__,
67            'isolated': isolated,
68        }
69
70        self.name = name
71        self.summary = summary
72        self.parser = ConfigOptionParser(**parser_kw)
73
74        self.tempdir_registry = None  # type: Optional[TempDirRegistry]
75
76        # Commands should add options to this option group
77        optgroup_name = '{} Options'.format(self.name.capitalize())
78        self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
79
80        # Add the general options
81        gen_opts = cmdoptions.make_option_group(
82            cmdoptions.general_group,
83            self.parser,
84        )
85        self.parser.add_option_group(gen_opts)
86
87        self.add_options()
88
89    def add_options(self):
90        # type: () -> None
91        pass
92
93    def handle_pip_version_check(self, options):
94        # type: (Values) -> None
95        """
96        This is a no-op so that commands by default do not do the pip version
97        check.
98        """
99        # Make sure we do the pip version check if the index_group options
100        # are present.
101        assert not hasattr(options, 'no_index')
102
103    def run(self, options, args):
104        # type: (Values, List[Any]) -> int
105        raise NotImplementedError
106
107    def parse_args(self, args):
108        # type: (List[str]) -> Tuple[Any, Any]
109        # factored out for testability
110        return self.parser.parse_args(args)
111
112    def main(self, args):
113        # type: (List[str]) -> int
114        try:
115            with self.main_context():
116                return self._main(args)
117        finally:
118            logging.shutdown()
119
120    def _main(self, args):
121        # type: (List[str]) -> int
122        # We must initialize this before the tempdir manager, otherwise the
123        # configuration would not be accessible by the time we clean up the
124        # tempdir manager.
125        self.tempdir_registry = self.enter_context(tempdir_registry())
126        # Intentionally set as early as possible so globally-managed temporary
127        # directories are available to the rest of the code.
128        self.enter_context(global_tempdir_manager())
129
130        options, args = self.parse_args(args)
131
132        # Set verbosity so that it can be used elsewhere.
133        self.verbosity = options.verbose - options.quiet
134
135        level_number = setup_logging(
136            verbosity=self.verbosity,
137            no_color=options.no_color,
138            user_log_file=options.log,
139        )
140
141        if (
142            sys.version_info[:2] == (2, 7) and
143            not options.no_python_version_warning
144        ):
145            message = (
146                "pip 21.0 will drop support for Python 2.7 in January 2021. "
147                "More details about Python 2 support in pip can be found at "
148                "https://pip.pypa.io/en/latest/development/release-process/#python-2-support"  # noqa
149            )
150            if platform.python_implementation() == "CPython":
151                message = (
152                    "Python 2.7 reached the end of its life on January "
153                    "1st, 2020. Please upgrade your Python as Python 2.7 "
154                    "is no longer maintained. "
155                ) + message
156            deprecated(message, replacement=None, gone_in="21.0")
157
158        if (
159            sys.version_info[:2] == (3, 5) and
160            not options.no_python_version_warning
161        ):
162            message = (
163                "Python 3.5 reached the end of its life on September "
164                "13th, 2020. Please upgrade your Python as Python 3.5 "
165                "is no longer maintained. pip 21.0 will drop support "
166                "for Python 3.5 in January 2021."
167            )
168            deprecated(message, replacement=None, gone_in="21.0")
169
170        # TODO: Try to get these passing down from the command?
171        #       without resorting to os.environ to hold these.
172        #       This also affects isolated builds and it should.
173
174        if options.no_input:
175            os.environ['PIP_NO_INPUT'] = '1'
176
177        if options.exists_action:
178            os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
179
180        if options.require_venv and not self.ignore_require_venv:
181            # If a venv is required check if it can really be found
182            if not running_under_virtualenv():
183                logger.critical(
184                    'Could not find an activated virtualenv (required).'
185                )
186                sys.exit(VIRTUALENV_NOT_FOUND)
187
188        if options.cache_dir:
189            options.cache_dir = normalize_path(options.cache_dir)
190            if not check_path_owner(options.cache_dir):
191                logger.warning(
192                    "The directory '%s' or its parent directory is not owned "
193                    "or is not writable by the current user. The cache "
194                    "has been disabled. Check the permissions and owner of "
195                    "that directory. If executing pip with sudo, you may want "
196                    "sudo's -H flag.",
197                    options.cache_dir,
198                )
199                options.cache_dir = None
200
201        if getattr(options, "build_dir", None):
202            deprecated(
203                reason=(
204                    "The -b/--build/--build-dir/--build-directory "
205                    "option is deprecated and has no effect anymore."
206                ),
207                replacement=(
208                    "use the TMPDIR/TEMP/TMP environment variable, "
209                    "possibly combined with --no-clean"
210                ),
211                gone_in="21.1",
212                issue=8333,
213            )
214
215        if '2020-resolver' in options.features_enabled and not PY2:
216            logger.warning(
217                "--use-feature=2020-resolver no longer has any effect, "
218                "since it is now the default dependency resolver in pip. "
219                "This will become an error in pip 21.0."
220            )
221
222        try:
223            status = self.run(options, args)
224            assert isinstance(status, int)
225            return status
226        except PreviousBuildDirError as exc:
227            logger.critical(str(exc))
228            logger.debug('Exception information:', exc_info=True)
229
230            return PREVIOUS_BUILD_DIR_ERROR
231        except (InstallationError, UninstallationError, BadCommand,
232                NetworkConnectionError) as exc:
233            logger.critical(str(exc))
234            logger.debug('Exception information:', exc_info=True)
235
236            return ERROR
237        except CommandError as exc:
238            logger.critical('%s', exc)
239            logger.debug('Exception information:', exc_info=True)
240
241            return ERROR
242        except BrokenStdoutLoggingError:
243            # Bypass our logger and write any remaining messages to stderr
244            # because stdout no longer works.
245            print('ERROR: Pipe to stdout was broken', file=sys.stderr)
246            if level_number <= logging.DEBUG:
247                traceback.print_exc(file=sys.stderr)
248
249            return ERROR
250        except KeyboardInterrupt:
251            logger.critical('Operation cancelled by user')
252            logger.debug('Exception information:', exc_info=True)
253
254            return ERROR
255        except BaseException:
256            logger.critical('Exception:', exc_info=True)
257
258            return UNKNOWN_ERROR
259        finally:
260            self.handle_pip_version_check(options)
261