Skip to content

Commit

Permalink
feat: option to ship pip when creating a venv (#1463)
Browse files Browse the repository at this point in the history
  • Loading branch information
frostming authored Oct 26, 2022
1 parent 39d6c04 commit 8ef9276
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 24 deletions.
1 change: 1 addition & 0 deletions docs/docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ The following configuration items can be retrieved and modified by [`pdm config`
| `venv.backend` | Default backend to create virtualenv | `virtualenv` | Yes | `PDM_VENV_BACKEND` |
| `venv.prompt` | Formatted string to be displayed in the prompt when virtualenv is active | `{project_name}-{python_version}` | Yes | `PDM_VENV_PROMPT` |
| `venv.in-project` | Create virtualenv in `.venv` under project root | `False` | Yes | `PDM_VENV_IN_PROJECT` |
| `venv.with-pip` | Install pip when creating a new venv | `False` | Yes | `PDM_VENV_WITH_PIP` |

_If the corresponding env var is set, the value will take precedence over what is saved in the config file._
1 change: 1 addition & 0 deletions news/1463.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add both option and config item to ship `pip` when creating a new venv.
35 changes: 27 additions & 8 deletions src/pdm/cli/commands/venv/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import subprocess
import sys
from pathlib import Path
from typing import Any, List, Mapping, Optional, Tuple, Type
from typing import Any, Iterable, List, Mapping, Optional, Tuple, Type

from pdm import termui
from pdm.cli.commands.venv.utils import get_venv_prefix
Expand All @@ -24,6 +24,10 @@ def __init__(self, project: Project, python: Optional[str]) -> None:
self.project = project
self.python = python

@abc.abstractmethod
def pip_args(self, with_pip: bool) -> Iterable[str]:
pass

@cached_property
def _resolved_interpreter(self) -> PythonInfo:
if not self.python:
Expand Down Expand Up @@ -91,11 +95,13 @@ def create(
force: bool = False,
in_project: bool = False,
prompt: Optional[str] = None,
with_pip: bool = False,
) -> Path:
if in_project:
location = self.project.root / ".venv"
else:
location = self.get_location(name)
args = (*self.pip_args(with_pip), *args)
if prompt is not None:
prompt_string = prompt.format(
project_name=self.project.root.name.lower() or "virtualenv",
Expand All @@ -112,30 +118,38 @@ def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:


class VirtualenvBackend(Backend):
def pip_args(self, with_pip: bool) -> Iterable[str]:
if with_pip or self.project.config["venv.with_pip"]:
return ()
return ("--no-pip", "--no-setuptools", "--no-wheel")

def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
cmd = [
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-setuptools",
"--no-wheel",
str(location),
"-p",
str(self._resolved_interpreter.executable),
*args,
]
cmd.extend(["-p", str(self._resolved_interpreter.executable)])
cmd.extend(args)
self.subprocess_call(cmd)


class VenvBackend(VirtualenvBackend):
def pip_args(self, with_pip: bool) -> Iterable[str]:
if with_pip or self.project.config["venv.with_pip"]:
return ()
return ("--without-pip",)

def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
cmd = [
str(self._resolved_interpreter.executable),
"-m",
"venv",
"--without-pip",
str(location),
] + list(args)
*args,
]
self.subprocess_call(cmd)


Expand All @@ -148,6 +162,11 @@ def ident(self) -> str:
return self.python
return super().ident

def pip_args(self, with_pip: bool) -> Iterable[str]:
if with_pip or self.project.config["venv.with_pip"]:
return ("pip",)
return ()

def perform_create(self, location: Path, args: Tuple[str, ...]) -> None:
if self.python:
python_ver = self.python
Expand Down
10 changes: 9 additions & 1 deletion src/pdm/cli/commands/venv/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
help="Recreate if the virtualenv already exists",
)
parser.add_argument("-n", "--name", help="Specify the name of the virtualenv")
parser.add_argument(
"--with-pip", action="store_true", help="Install pip with the virtualenv"
)
parser.add_argument(
"python",
nargs="?",
Expand All @@ -54,6 +57,11 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
f"Creating virtualenv using [success]{backend}[/]..."
):
path = venv_backend.create(
options.name, options.venv_args, options.force, in_project, prompt
options.name,
options.venv_args,
options.force,
in_project,
prompt,
options.with_pip,
)
project.core.ui.echo(f"Virtualenv [success]{path}[/] is created successfully")
6 changes: 6 additions & 0 deletions src/pdm/project/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ class Config(MutableMapping[str, str]):
default="{project_name}-{python_version}",
env_var="PDM_VENV_PROMPT",
),
"venv.with_pip": ConfigItem(
"Install pip when creating a new venv",
default=False,
env_var="PDM_VENV_WITH_PIP",
coerce=ensure_boolean,
),
}
_config_map.update(
(f"theme.{k}", ConfigItem(f"Theme color for {k}", default=v, global_only=True))
Expand Down
38 changes: 26 additions & 12 deletions tests/cli/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
from pdm.cli.commands.venv.utils import get_venv_prefix


@pytest.fixture(params=[True, False])
def with_pip(request):
return request.param


@pytest.fixture()
def fake_create(monkeypatch):
def fake_create(self, location, *args):
Expand Down Expand Up @@ -118,7 +123,7 @@ def test_venv_activate_custom_prompt(invoke, mocker, project):
result = invoke(["venv", "create"], obj=project)
assert result.exit_code == 0, result.stderr
creator.assert_called_once_with(
None, [], False, False, project.project_config["venv.prompt"]
None, [], False, False, project.project_config["venv.prompt"], False
)


Expand Down Expand Up @@ -208,51 +213,60 @@ def test_venv_purge_interactive(invoke, user_choices, is_path_exists, project):
assert os.path.exists(venv_path) == is_path_exists


def test_virtualenv_backend_create(project, mocker):
def test_virtualenv_backend_create(project, mocker, with_pip):
backend = backends.VirtualenvBackend(project, None)
assert backend.ident
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
location = backend.create(with_pip=with_pip)
pip_args = [] if with_pip else ["--no-pip", "--no-setuptools", "--no-wheel"]
mock_call.assert_called_once_with(
[
sys.executable,
"-m",
"virtualenv",
"--no-pip",
"--no-setuptools",
"--no-wheel",
str(location),
"-p",
str(backend._resolved_interpreter.executable),
*pip_args,
],
stdout=ANY,
)


def test_venv_backend_create(project, mocker):
def test_venv_backend_create(project, mocker, with_pip):
backend = backends.VenvBackend(project, None)
assert backend.ident
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
location = backend.create(with_pip=with_pip)
pip_args = [] if with_pip else ["--without-pip"]
mock_call.assert_called_once_with(
[
str(backend._resolved_interpreter.executable),
"-m",
"venv",
"--without-pip",
str(location),
*pip_args,
],
stdout=ANY,
)


def test_conda_backend_create(project, mocker):
def test_conda_backend_create(project, mocker, with_pip):
backend = backends.CondaBackend(project, "3.8")
assert backend.ident == "3.8"
mock_call = mocker.patch("subprocess.check_call")
location = backend.create()
location = backend.create(with_pip=with_pip)
pip_args = ["pip"] if with_pip else []
mock_call.assert_called_once_with(
["conda", "create", "--yes", "--prefix", str(location), "python=3.8"],
[
"conda",
"create",
"--yes",
"--prefix",
str(location),
"python=3.8",
*pip_args,
],
stdout=ANY,
)

Expand Down
9 changes: 6 additions & 3 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,16 @@ def test_create_venv_first_time(invoke, project, local_finder):
assert Path(project.project_config["python.path"]).relative_to(venv_path)


@pytest.mark.usefixtures("venv_backends")
def test_create_venv_in_project(invoke, project, local_finder):
project.project_config.update({"venv.in_project": True})
@pytest.mark.usefixtures("venv_backends", "local_finder")
@pytest.mark.parametrize("with_pip", [True, False])
def test_create_venv_in_project(invoke, project, with_pip):
project.project_config.update({"venv.in_project": True, "venv.with_pip": with_pip})
del project.project_config["python.path"]
result = invoke(["install"], obj=project)
assert result.exit_code == 0
assert project.root.joinpath(".venv").exists()
working_set = project.environment.get_working_set()
assert ("pip" in working_set) is with_pip


@pytest.mark.usefixtures("venv_backends")
Expand Down

0 comments on commit 8ef9276

Please sign in to comment.