1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

Merge pull request #11320 from pfmoore/python_option

Add a --python option
This commit is contained in:
Paul Moore 2022-08-06 15:24:23 +01:00 committed by GitHub
commit 9473e83aa6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 3 deletions

View file

@ -19,4 +19,5 @@ local-project-installs
repeatable-installs
secure-installs
vcs-support
python-option
```

View file

@ -0,0 +1,29 @@
# Managing a different Python interpreter
```{versionadded} 22.3
```
Occasionally, you may want to use pip to manage a Python installation other than
the one pip is installed into. In this case, you can use the `--python` option
to specify the interpreter you want to manage. This option can take one of two
values:
1. The path to a Python executable.
2. The path to a virtual environment.
In both cases, pip will run exactly as if it had been invoked from that Python
environment.
One example of where this might be useful is to manage a virtual environment
that does not have pip installed.
```{pip-cli}
$ python -m venv .venv --without-pip
$ pip --python .venv install SomePackage
[...]
Successfully installed SomePackage
```
You could also use `--python .venv/bin/python` (or on Windows,
`--python .venv\Scripts\python.exe`) if you wanted to be explicit, but the
virtual environment name is shorter and works exactly the same.

2
news/11320.feature.rst Normal file
View file

@ -0,0 +1,2 @@
Add a ``--python`` option to allow pip to manage Python environments other
than the one pip is installed in.

View file

@ -39,7 +39,7 @@ class _Prefix:
self.lib_dirs = get_prefixed_libs(path)
def _get_runnable_pip() -> str:
def get_runnable_pip() -> str:
"""Get a file to pass to a Python executable, to run the currently-running pip.
This is used to run a pip subprocess, for installing requirements into the build
@ -194,7 +194,7 @@ class BuildEnvironment:
if not requirements:
return
self._install_requirements(
_get_runnable_pip(),
get_runnable_pip(),
finder,
requirements,
prefix,

View file

@ -189,6 +189,13 @@ require_virtualenv: Callable[..., Option] = partial(
),
)
python: Callable[..., Option] = partial(
Option,
"--python",
dest="python",
help="Run pip with the specified Python interpreter.",
)
verbose: Callable[..., Option] = partial(
Option,
"-v",
@ -1029,6 +1036,7 @@ general_group: Dict[str, Any] = {
debug_mode,
isolated_mode,
require_virtualenv,
python,
verbose,
version,
quiet,

View file

@ -2,9 +2,11 @@
"""
import os
import subprocess
import sys
from typing import List, Tuple
from typing import List, Optional, Tuple
from pip._internal.build_env import get_runnable_pip
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
@ -45,6 +47,25 @@ def create_main_parser() -> ConfigOptionParser:
return parser
def identify_python_interpreter(python: str) -> Optional[str]:
# If the named file exists, use it.
# If it's a directory, assume it's a virtual environment and
# look for the environment's Python executable.
if os.path.exists(python):
if os.path.isdir(python):
# bin/python for Unix, Scripts/python.exe for Windows
# Try both in case of odd cases like cygwin.
for exe in ("bin/python", "Scripts/python.exe"):
py = os.path.join(python, exe)
if os.path.exists(py):
return py
else:
return python
# Could not find the interpreter specified
return None
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()
@ -57,6 +78,32 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
# args_else: ['install', '--user', 'INITools']
general_options, args_else = parser.parse_args(args)
# --python
if general_options.python and "_PIP_RUNNING_IN_SUBPROCESS" not in os.environ:
# 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"
returncode = 0
try:
proc = subprocess.run(pip_cmd)
returncode = proc.returncode
except (subprocess.SubprocessError, OSError) as exc:
raise CommandError(f"Failed to run pip under {interpreter}: {exc}")
sys.exit(returncode)
# --version
if general_options.version:
sys.stdout.write(parser.version)

View file

@ -0,0 +1,41 @@
import json
import os
from pathlib import Path
from venv import EnvBuilder
from tests.lib import PipTestEnvironment, TestData
def test_python_interpreter(
script: PipTestEnvironment,
tmpdir: Path,
shared_data: TestData,
) -> None:
env_path = os.fspath(tmpdir / "venv")
env = EnvBuilder(with_pip=False)
env.create(env_path)
result = script.pip("--python", env_path, "list", "--format=json")
before = json.loads(result.stdout)
# Ideally we would assert that before==[], but there's a problem in CI
# that means this isn't true. See https://github.com/pypa/pip/pull/11326
# for details.
script.pip(
"--python",
env_path,
"install",
"-f",
shared_data.find_links,
"--no-index",
"simplewheel==1.0",
)
result = script.pip("--python", env_path, "list", "--format=json")
installed = json.loads(result.stdout)
assert {"name": "simplewheel", "version": "1.0"} in installed
script.pip("--python", env_path, "uninstall", "simplewheel", "--yes")
result = script.pip("--python", env_path, "list", "--format=json")
assert json.loads(result.stdout) == before

View file

@ -1,8 +1,12 @@
import os
from pathlib import Path
from typing import Optional, Tuple
from venv import EnvBuilder
import pytest
from pip._internal.cli.cmdoptions import _convert_python_version
from pip._internal.cli.main_parser import identify_python_interpreter
@pytest.mark.parametrize(
@ -29,3 +33,20 @@ def test_convert_python_version(
) -> None:
actual = _convert_python_version(value)
assert actual == expected, f"actual: {actual!r}"
def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
env_path = tmpdir / "venv"
env = EnvBuilder(with_pip=False)
env.create(env_path)
# Passing a virtual environment returns the Python executable
interp = identify_python_interpreter(os.fspath(env_path))
assert interp is not None
assert Path(interp).exists()
# Passing an executable returns it
assert identify_python_interpreter(interp) == interp
# Passing a non-existent file returns None
assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None