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