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

feat: New option --venv VENV to run a command in the venv with the given name #1820

Merged
merged 1 commit into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
24 changes: 24 additions & 0 deletions docs/docs/usage/venv.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ $ eval $(pdm venv activate test-prompt)
(test-project-py3.10) $
```

## Run a command in a virtual environment without activating it

```bash
# Run a script
$ pdm run --venv test test
# Install packages
$ pdm sync --venv test
# List the packages installed
$ pdm list --venv test
```

There are other commands supporting `--venv` flag or `PDM_USE_VENV` environment variable, see the [CLI reference](../reference/cli.md). You should create the virtualenv with `pdm venv create --name <name>` before using this feature.

## Switch to a virtualenv as the project environment

By default, if you use `pdm use` and select a non-venv Python, the project will be switched to [PEP 582 mode](../pep582.md). We also allow you to switch to a named virtual environment via the `--venv` flag:

```bash
# Switch to a virtualenv named test
$ pdm use --venv test
# Switch to the in-project venv located at $PROJECT_ROOT/.venv
$ pdm use --venv
```

## Disable virtualenv mode

You can disable the auto-creation and auto-detection for virtualenv by `pdm config python.use_venv false`.
Expand Down
1 change: 1 addition & 0 deletions news/1705.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New option `--venv <venv>` to run a command in the virtual environment with the given name.
22 changes: 14 additions & 8 deletions src/pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,8 @@ def do_add(
group = selection.one()
tracked_names: set[str] = set()
requirements: dict[str, Requirement] = {}
lock_groups = project.lockfile.groups
if lock_groups and group not in lock_groups:
lock_groups = [] if project.lockfile.empty() else project.lockfile.groups
if lock_groups is not None and group not in lock_groups:
project.core.ui.echo(f"Adding group [success]{group}[/] to lockfile", err=True, style="info")
lock_groups.append(group)
if group == "default" or not selection.dev and group not in project.pyproject.settings.get("dev-dependencies", {}):
Expand Down Expand Up @@ -570,26 +570,32 @@ def do_use(
ignore_remembered: bool = False,
ignore_requires_python: bool = False,
save: bool = True,
venv: str | None = None,
hooks: HookManager | None = None,
) -> PythonInfo:
"""Use the specified python version and save in project config.
The python can be a version string or interpreter path.
"""
hooks = hooks or HookManager(project)

if python:
python = python.strip()
from pdm.cli.commands.venv.utils import get_venv_python, get_venv_with_name

def version_matcher(py_version: PythonInfo) -> bool:
return py_version.valid and (
ignore_requires_python or project.python_requires.contains(str(py_version.version), True)
)

hooks = hooks or HookManager(project)

selected_python: PythonInfo | None = None
if venv:
venv_path = get_venv_with_name(project, venv)
selected_python = PythonInfo.from_path(get_venv_python(venv_path))

if not project.cache_dir.exists():
project.cache_dir.mkdir(parents=True)
use_cache: JSONFileCache[str, str] = JSONFileCache(project.cache_dir / "use_cache.json")
selected_python: PythonInfo | None = None
if python and not ignore_remembered:
if python:
python = python.strip()
if selected_python is None and python and not ignore_remembered:
if python in use_cache:
path = use_cache.get(python)
cached_python = PythonInfo.from_path(path)
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
skip_option,
unconstrained_option,
update_strategy_group,
venv_option,
)
from pdm.exceptions import PdmUsageError
from pdm.project import Project
Expand All @@ -32,6 +33,7 @@ class Command(BaseCommand):
packages_group,
install_group,
dry_run_option,
venv_option,
skip_option,
]

Expand Down
5 changes: 3 additions & 2 deletions src/pdm/cli/commands/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json

from pdm.cli.commands.base import BaseCommand
from pdm.cli.options import ArgumentGroup
from pdm.cli.options import ArgumentGroup, venv_option
from pdm.cli.utils import check_project_file
from pdm.project import Project

Expand All @@ -11,6 +11,7 @@ class Command(BaseCommand):
"""Show the project information"""

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
venv_option.add_to_parser(parser)
group = ArgumentGroup("fields", is_mutually_exclusive=True)
group.add_argument("--python", action="store_true", help="Show the interpreter path")
group.add_argument(
Expand All @@ -25,7 +26,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:

def handle(self, project: Project, options: argparse.Namespace) -> None:
check_project_file(project)
interpreter = project.python
interpreter = project.environment.interpreter
packages_path = ""
if project.environment.is_local:
packages_path = project.environment.packages_path # type: ignore[attr-defined]
Expand Down
18 changes: 10 additions & 8 deletions src/pdm/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@
from pdm.cli.commands.base import BaseCommand
from pdm.cli.filters import GroupSelection
from pdm.cli.hooks import HookManager
from pdm.cli.options import (
dry_run_option,
groups_group,
install_group,
lockfile_option,
skip_option,
)
from pdm.cli.options import dry_run_option, groups_group, install_group, lockfile_option, skip_option, venv_option
from pdm.project import Project


class Command(BaseCommand):
"""Install dependencies from lock file"""

arguments = [*BaseCommand.arguments, groups_group, install_group, dry_run_option, lockfile_option, skip_option]
arguments = [
*BaseCommand.arguments,
groups_group,
install_group,
dry_run_option,
lockfile_option,
skip_option,
venv_option,
]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from pdm.cli import actions
from pdm.cli.commands.base import BaseCommand
from pdm.cli.options import venv_option
from pdm.cli.utils import (
DirectedGraph,
Package,
Expand All @@ -32,6 +33,7 @@ class Command(BaseCommand):
DEFAULT_FIELDS = "name,version,location"

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
venv_option.add_to_parser(parser)
graph = parser.add_mutually_exclusive_group()

parser.add_argument(
Expand Down
4 changes: 2 additions & 2 deletions src/pdm/cli/commands/remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
from pdm.cli.commands.base import BaseCommand
from pdm.cli.filters import GroupSelection
from pdm.cli.hooks import HookManager
from pdm.cli.options import dry_run_option, install_group, lockfile_option, skip_option
from pdm.cli.options import dry_run_option, install_group, lockfile_option, skip_option, venv_option
from pdm.project import Project


class Command(BaseCommand):
"""Remove packages from pyproject.toml"""

arguments = [*BaseCommand.arguments, install_group, dry_run_option, lockfile_option, skip_option]
arguments = [*BaseCommand.arguments, install_group, dry_run_option, lockfile_option, skip_option, venv_option]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
Expand Down
6 changes: 3 additions & 3 deletions src/pdm/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from pdm import termui
from pdm.cli.commands.base import BaseCommand
from pdm.cli.hooks import HookManager
from pdm.cli.options import skip_option
from pdm.cli.options import skip_option, venv_option
from pdm.cli.utils import check_project_file, get_pep582_path
from pdm.compat import TypedDict
from pdm.exceptions import PdmUsageError
Expand Down Expand Up @@ -163,7 +163,7 @@ def _run_process(
command, *args = args
if command.endswith(".py"):
args = [command, *args]
command = str(project.python.executable)
command = str(project.environment.interpreter.executable)
expanded_command = project_env.which(command)
if not expanded_command:
raise PdmUsageError(f"Command [success]'{command}'[/] is not found in your PATH.")
Expand Down Expand Up @@ -305,9 +305,9 @@ class Command(BaseCommand):
"""Run commands or scripts with local packages loaded"""

runner_cls: type[TaskRunner] = TaskRunner
arguments = [*BaseCommand.arguments, skip_option, venv_option]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
skip_option.add_to_parser(parser)
parser.add_argument(
"-l",
"--list",
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from packaging.version import Version

from pdm.cli.commands.base import BaseCommand
from pdm.cli.options import venv_option
from pdm.exceptions import PdmUsageError
from pdm.models.candidates import Candidate
from pdm.models.project_info import ProjectInfo
Expand All @@ -22,6 +23,7 @@ class Command(BaseCommand):
metadata_keys = ["name", "version", "summary", "license", "platform", "keywords"]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
venv_option.add_to_parser(parser)
parser.add_argument(
"package",
type=normalize_name,
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
install_group,
lockfile_option,
skip_option,
venv_option,
)
from pdm.project import Project

Expand All @@ -26,6 +27,7 @@ class Command(BaseCommand):
skip_option,
clean_group,
install_group,
venv_option,
]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
skip_option,
unconstrained_option,
update_strategy_group,
venv_option,
)
from pdm.project import Project

Expand All @@ -30,6 +31,7 @@ class Command(BaseCommand):
prerelease_option,
unconstrained_option,
skip_option,
venv_option,
]

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
Expand Down
2 changes: 2 additions & 0 deletions src/pdm/cli/commands/use.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
action="store_true",
help="Ignore the remembered selection",
)
parser.add_argument("--venv", help="Use the interpreter in the virtual environment with the given name")
parser.add_argument("python", nargs="?", help="Specify the Python version or path", default="")

def handle(self, project: Project, options: argparse.Namespace) -> None:
Expand All @@ -32,5 +33,6 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
python=options.python,
first=options.first,
ignore_remembered=options.ignore_remembered,
venv=options.venv,
hooks=HookManager(project, options.skip),
)
17 changes: 3 additions & 14 deletions src/pdm/cli/commands/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import argparse
from pathlib import Path
from pdm.cli.commands.venv.utils import iter_venvs, get_venv_python
from pdm.cli.commands.venv.utils import get_venv_with_name, iter_venvs, get_venv_python

from pdm.exceptions import PdmUsageError
from pdm.project import Project
Expand Down Expand Up @@ -32,25 +32,14 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
PurgeCommand.register_to(subparser, "purge")
self.parser = parser

def _get_venv_with_name(self, project: Project, name: str) -> Path:
venv = next((venv for key, venv in iter_venvs(project) if key == name), None)
if not venv:
project.core.ui.echo(
f"No virtualenv with key [success]{name}[/] is found",
style="warning",
err=True,
)
raise SystemExit(1)
return venv

def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.path and options.python:
raise PdmUsageError("--path and --python are mutually exclusive")
if options.path:
venv = self._get_venv_with_name(project, options.path)
venv = get_venv_with_name(project, options.path)
project.core.ui.echo(str(venv))
elif options.python:
venv = self._get_venv_with_name(project, options.python)
venv = get_venv_with_name(project, options.python)
project.core.ui.echo(str(get_venv_python(venv)))
else:
self.parser.print_help()
16 changes: 5 additions & 11 deletions src/pdm/cli/commands/venv/activate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import shellingham

from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import BIN_DIR, iter_venvs
from pdm.cli.commands.venv.utils import BIN_DIR, iter_venvs, get_venv_with_name
from pdm.cli.options import verbose_option
from pdm.project import Project
from pdm.utils import get_venv_like_prefix
Expand All @@ -21,14 +21,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:

def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.env:
venv = next((venv for key, venv in iter_venvs(project) if key == options.env), None)
if not venv:
project.core.ui.echo(
f"No virtualenv with key [success]{options.env}[/] is found",
style="warning",
err=True,
)
raise SystemExit(1)
venv = get_venv_with_name(project, options.env)
else:
# Use what is saved in .pdm-python
interpreter = project._saved_python
Expand All @@ -39,15 +32,16 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
err=True,
)
raise SystemExit(1)
venv = get_venv_like_prefix(interpreter)
if venv is None:
venv_like = get_venv_like_prefix(interpreter)
if venv_like is None:
project.core.ui.echo(
f"Can't activate a non-venv Python [success]{interpreter}[/], "
"you can specify one with [success]pdm venv activate <env_name>[/]",
style="warning",
err=True,
)
raise SystemExit(1)
venv = venv_like
project.core.ui.echo(self.get_activate_command(venv))

def get_activate_command(self, venv: Path) -> str: # pragma: no cover
Expand Down
25 changes: 8 additions & 17 deletions src/pdm/cli/commands/venv/remove.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pdm import termui
from pdm.cli.commands.base import BaseCommand
from pdm.cli.commands.venv.utils import iter_venvs
from pdm.cli.commands.venv.utils import get_venv_with_name, iter_venvs
from pdm.cli.options import verbose_option
from pdm.project import Project

Expand All @@ -25,19 +25,10 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:

def handle(self, project: Project, options: argparse.Namespace) -> None:
project.core.ui.echo("Virtualenvs created with this project:")
for ident, venv in iter_venvs(project):
if ident == options.env:
if options.yes or termui.confirm(f"[warning]Will remove: [success]{venv}[/], continue?", default=True):
shutil.rmtree(venv)
saved_python = project._saved_python
if saved_python and Path(saved_python).parent.parent == venv:
project._saved_python = None
project.core.ui.echo("Removed successfully!")
break
else:
project.core.ui.echo(
f"No virtualenv with key [success]{options.env}[/] is found",
style="warning",
err=True,
)
raise SystemExit(1)
venv = get_venv_with_name(project, options.env)
if options.yes or termui.confirm(f"[warning]Will remove: [success]{venv}[/], continue?", default=True):
shutil.rmtree(venv)
saved_python = project._saved_python
if saved_python and Path(saved_python).parent.parent == venv:
project._saved_python = None
project.core.ui.echo("Removed successfully!")
Loading