main_parser.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. """A single place for constructing and exposing the main parser
  2. """
  3. import os
  4. import subprocess
  5. import sys
  6. from typing import List, Optional, Tuple
  7. from pip._internal.build_env import get_runnable_pip
  8. from pip._internal.cli import cmdoptions
  9. from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
  10. from pip._internal.commands import commands_dict, get_similar_commands
  11. from pip._internal.exceptions import CommandError
  12. from pip._internal.utils.misc import get_pip_version, get_prog
  13. __all__ = ["create_main_parser", "parse_command"]
  14. def create_main_parser() -> ConfigOptionParser:
  15. """Creates and returns the main parser for pip's CLI"""
  16. parser = ConfigOptionParser(
  17. usage="\n%prog <command> [options]",
  18. add_help_option=False,
  19. formatter=UpdatingDefaultsHelpFormatter(),
  20. name="global",
  21. prog=get_prog(),
  22. )
  23. parser.disable_interspersed_args()
  24. parser.version = get_pip_version()
  25. # add the general options
  26. gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)
  27. parser.add_option_group(gen_opts)
  28. # so the help formatter knows
  29. parser.main = True # type: ignore
  30. # create command listing for description
  31. description = [""] + [
  32. f"{name:27} {command_info.summary}"
  33. for name, command_info in commands_dict.items()
  34. ]
  35. parser.description = "\n".join(description)
  36. return parser
  37. def identify_python_interpreter(python: str) -> Optional[str]:
  38. # If the named file exists, use it.
  39. # If it's a directory, assume it's a virtual environment and
  40. # look for the environment's Python executable.
  41. if os.path.exists(python):
  42. if os.path.isdir(python):
  43. # bin/python for Unix, Scripts/python.exe for Windows
  44. # Try both in case of odd cases like cygwin.
  45. for exe in ("bin/python", "Scripts/python.exe"):
  46. py = os.path.join(python, exe)
  47. if os.path.exists(py):
  48. return py
  49. else:
  50. return python
  51. # Could not find the interpreter specified
  52. return None
  53. def parse_command(args: List[str]) -> Tuple[str, List[str]]:
  54. parser = create_main_parser()
  55. # Note: parser calls disable_interspersed_args(), so the result of this
  56. # call is to split the initial args into the general options before the
  57. # subcommand and everything else.
  58. # For example:
  59. # args: ['--timeout=5', 'install', '--user', 'INITools']
  60. # general_options: ['--timeout==5']
  61. # args_else: ['install', '--user', 'INITools']
  62. general_options, args_else = parser.parse_args(args)
  63. # --python
  64. if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
  65. # Re-invoke pip using the specified Python interpreter
  66. interpreter = identify_python_interpreter(general_options.python)
  67. if interpreter is None:
  68. raise CommandError(
  69. f"Could not locate Python interpreter {general_options.python}"
  70. )
  71. pip_cmd = [
  72. interpreter,
  73. get_runnable_pip(),
  74. ]
  75. pip_cmd.extend(args)
  76. # Set a flag so the child doesn't re-invoke itself, causing
  77. # an infinite loop.
  78. os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
  79. returncode = 0
  80. try:
  81. proc = subprocess.run(pip_cmd)
  82. returncode = proc.returncode
  83. except (subprocess.SubprocessError, OSError) as exc:
  84. raise CommandError(f"Failed to run pip under {interpreter}: {exc}")
  85. sys.exit(returncode)
  86. # --version
  87. if general_options.version:
  88. sys.stdout.write(parser.version)
  89. sys.stdout.write(os.linesep)
  90. sys.exit()
  91. # pip || pip help -> print_help()
  92. if not args_else or (args_else[0] == "help" and len(args_else) == 1):
  93. parser.print_help()
  94. sys.exit()
  95. # the subcommand name
  96. cmd_name = args_else[0]
  97. if cmd_name not in commands_dict:
  98. guess = get_similar_commands(cmd_name)
  99. msg = [f'unknown command "{cmd_name}"']
  100. if guess:
  101. msg.append(f'maybe you meant "{guess}"')
  102. raise CommandError(" - ".join(msg))
  103. # all the args without the subcommand
  104. cmd_args = args[:]
  105. cmd_args.remove(cmd_name)
  106. return cmd_name, cmd_args