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:
commit
9473e83aa6
|
@ -19,4 +19,5 @@ local-project-installs
|
|||
repeatable-installs
|
||||
secure-installs
|
||||
vcs-support
|
||||
python-option
|
||||
```
|
||||
|
|
29
docs/html/topics/python-option.md
Normal file
29
docs/html/topics/python-option.md
Normal 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
2
news/11320.feature.rst
Normal file
|
@ -0,0 +1,2 @@
|
|||
Add a ``--python`` option to allow pip to manage Python environments other
|
||||
than the one pip is installed in.
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
41
tests/functional/test_python_option.py
Normal file
41
tests/functional/test_python_option.py
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue