Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a --python option #11320

Merged
merged 19 commits into from
Aug 6, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 60 additions & 12 deletions src/pip/_internal/cli/main_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
"""

import os
import shutil
import subprocess
import sys
from typing import List, Tuple
from typing import List, Optional, Tuple

from pip._internal.build_env import _get_runnable_pip
sbidoul marked this conversation as resolved.
Show resolved Hide resolved
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.exceptions import CommandError
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.misc import get_pip_version, get_prog

__all__ = ["create_main_parser", "parse_command"]
Expand Down Expand Up @@ -47,6 +49,44 @@ def create_main_parser() -> ConfigOptionParser:
return parser


def identify_python_interpreter(python: str) -> Optional[str]:
if python == "python" or python == "py":
# Run the active Python.
# We have to be very careful here, because:
#
# 1. On Unix, "python" is probably correct but there is a "py" launcher.
# 2. On Windows, "py" is the best option if it's present.
# 3. On Windows without "py", "python" might work, but it might also
# be the shim that launches the Windows store to allow you to install
# Python.
#
# We go with getting py on Windows, and if it's not present or we're
# on Unix, get python. We don't worry about the launcher on Unix or
# the installer stub on Windows.
py = None
if WINDOWS:
py = shutil.which("py")
if py is None:
py = shutil.which("python")
pfmoore marked this conversation as resolved.
Show resolved Hide resolved
if py:
return py

# TODO: On Windows, `--python .venv/Scripts/python` won't pass the
# exists() check (no .exe extension supplied). But it's pretty
# obvious what the user intends. Should we allow this?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this without too much fuss with

directory, filename = os.path.split(python)
if shutil.which(filename, path=directory) is not None:
    ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the shutil.which docs,

On Windows, the current directory is always prepended to the path whether or not you use the default or provide your own

That could give weird false positives (for example, we have a python.exe in the CWD, and we do --python=.venv/python.exe).

On reflection, I think this is too much effort, and we should just expect the user to supply the .exe (i.e., provide the name of a file that exists). I suspect most people will use autocomplete anyway, if they are entering the path by hand.

if os.path.exists(python):
if not os.path.isdir(python):
return python
sbidoul marked this conversation as resolved.
Show resolved Hide resolved
# Might be a virtual environment
for exe in ("bin/python", "Scripts/python.exe"):
py = os.path.join(python, exe)
if os.path.exists(py):
return py

# Could not find the interpreter specified
return None


def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()

Expand All @@ -60,17 +100,25 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
general_options, args_else = parser.parse_args(args)

# --python
if general_options.python:
if "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
pip_cmd = [
general_options.python,
_get_runnable_pip(),
]
pip_cmd.extend(args)
# Block recursing indefinitely
os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
proc = subprocess.run(pip_cmd)
sys.exit(proc.returncode)
if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
sbidoul marked this conversation as resolved.
Show resolved Hide resolved
# Re-invoke pip using the specified Python interpreter
interpreter = identify_python_interpreter(general_options.python)
if interpreter is None:
raise CommandError(
f"Could not locate Python interpreter {general_options.python}"
)

pip_cmd = [
interpreter,
_get_runnable_pip(),
]
pip_cmd.extend(args)

# Set a flag so the child doesn't re-invoke itself, causing
# an infinite loop.
os.environ["_PIP_RUNNING_IN_SUBPROCESS"] = "1"
proc = subprocess.run(pip_cmd)
sbidoul marked this conversation as resolved.
Show resolved Hide resolved
sys.exit(proc.returncode)

# --version
if general_options.version:
Expand Down