diff --git a/docs/docs/usage/venv.md b/docs/docs/usage/venv.md index 90fe09bb8c..0079fb4751 100644 --- a/docs/docs/usage/venv.md +++ b/docs/docs/usage/venv.md @@ -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 ` 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`. diff --git a/news/1705.feature.md b/news/1705.feature.md new file mode 100644 index 0000000000..1e5b543e10 --- /dev/null +++ b/news/1705.feature.md @@ -0,0 +1 @@ +New option `--venv ` to run a command in the virtual environment with the given name. diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py index ed20fea102..b4fc4d1d66 100644 --- a/src/pdm/cli/actions.py +++ b/src/pdm/cli/actions.py @@ -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", {}): @@ -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) diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py index d21184f0ad..14d45b968c 100644 --- a/src/pdm/cli/commands/add.py +++ b/src/pdm/cli/commands/add.py @@ -14,6 +14,7 @@ skip_option, unconstrained_option, update_strategy_group, + venv_option, ) from pdm.exceptions import PdmUsageError from pdm.project import Project @@ -32,6 +33,7 @@ class Command(BaseCommand): packages_group, install_group, dry_run_option, + venv_option, skip_option, ] diff --git a/src/pdm/cli/commands/info.py b/src/pdm/cli/commands/info.py index 3612e1867a..f8345a9767 100644 --- a/src/pdm/cli/commands/info.py +++ b/src/pdm/cli/commands/info.py @@ -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 @@ -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( @@ -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] diff --git a/src/pdm/cli/commands/install.py b/src/pdm/cli/commands/install.py index 085043085a..bbd34a137d 100644 --- a/src/pdm/cli/commands/install.py +++ b/src/pdm/cli/commands/install.py @@ -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( diff --git a/src/pdm/cli/commands/list.py b/src/pdm/cli/commands/list.py index 84dd02e7a5..19fbef8c35 100644 --- a/src/pdm/cli/commands/list.py +++ b/src/pdm/cli/commands/list.py @@ -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, @@ -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( diff --git a/src/pdm/cli/commands/remove.py b/src/pdm/cli/commands/remove.py index 8ab3e71bb5..31db901b55 100644 --- a/src/pdm/cli/commands/remove.py +++ b/src/pdm/cli/commands/remove.py @@ -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( diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py index 32856312d7..a0ceede97d 100644 --- a/src/pdm/cli/commands/run.py +++ b/src/pdm/cli/commands/run.py @@ -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 @@ -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.") @@ -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", diff --git a/src/pdm/cli/commands/show.py b/src/pdm/cli/commands/show.py index feb1eea9f3..c18d8e079b 100644 --- a/src/pdm/cli/commands/show.py +++ b/src/pdm/cli/commands/show.py @@ -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 @@ -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, diff --git a/src/pdm/cli/commands/sync.py b/src/pdm/cli/commands/sync.py index a97246bd22..5b7e6f5c53 100644 --- a/src/pdm/cli/commands/sync.py +++ b/src/pdm/cli/commands/sync.py @@ -11,6 +11,7 @@ install_group, lockfile_option, skip_option, + venv_option, ) from pdm.project import Project @@ -26,6 +27,7 @@ class Command(BaseCommand): skip_option, clean_group, install_group, + venv_option, ] def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/src/pdm/cli/commands/update.py b/src/pdm/cli/commands/update.py index 8c2e06da6a..564765dc62 100644 --- a/src/pdm/cli/commands/update.py +++ b/src/pdm/cli/commands/update.py @@ -13,6 +13,7 @@ skip_option, unconstrained_option, update_strategy_group, + venv_option, ) from pdm.project import Project @@ -30,6 +31,7 @@ class Command(BaseCommand): prerelease_option, unconstrained_option, skip_option, + venv_option, ] def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/src/pdm/cli/commands/use.py b/src/pdm/cli/commands/use.py index 7c7d6ad19f..f052f1730a 100644 --- a/src/pdm/cli/commands/use.py +++ b/src/pdm/cli/commands/use.py @@ -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: @@ -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), ) diff --git a/src/pdm/cli/commands/venv/__init__.py b/src/pdm/cli/commands/venv/__init__.py index a9060b2a0e..1a5c78bf51 100644 --- a/src/pdm/cli/commands/venv/__init__.py +++ b/src/pdm/cli/commands/venv/__init__.py @@ -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 @@ -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() diff --git a/src/pdm/cli/commands/venv/activate.py b/src/pdm/cli/commands/venv/activate.py index 2a68c9e33b..205ac5b99d 100644 --- a/src/pdm/cli/commands/venv/activate.py +++ b/src/pdm/cli/commands/venv/activate.py @@ -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 @@ -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 @@ -39,8 +32,8 @@ 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 [/]", @@ -48,6 +41,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: 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 diff --git a/src/pdm/cli/commands/venv/remove.py b/src/pdm/cli/commands/venv/remove.py index 4796c26569..2007b71c03 100644 --- a/src/pdm/cli/commands/venv/remove.py +++ b/src/pdm/cli/commands/venv/remove.py @@ -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 @@ -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!") diff --git a/src/pdm/cli/commands/venv/utils.py b/src/pdm/cli/commands/venv/utils.py index 900121b562..3679d9b531 100644 --- a/src/pdm/cli/commands/venv/utils.py +++ b/src/pdm/cli/commands/venv/utils.py @@ -8,6 +8,7 @@ from findpython import PythonVersion from findpython.providers import BaseProvider +from pdm.exceptions import PdmUsageError from pdm.project import Project @@ -80,3 +81,14 @@ def find_pythons(self) -> Iterable[PythonVersion]: python = get_venv_python(venv) if python.exists(): yield PythonVersion(python, _interpreter=python, keep_symlink=True) + + +def get_venv_with_name(project: Project, name: str) -> Path: + all_venvs = dict(iter_venvs(project)) + try: + return all_venvs[name] + except KeyError: + raise PdmUsageError( + f"No virtualenv with key '{name}' is found, must be one of {list(all_venvs)}.\n" + "You can create one with 'pdm venv create'.", + ) diff --git a/src/pdm/cli/completions/pdm.bash b/src/pdm/cli/completions/pdm.bash index dbf44d7893..647669af2a 100644 --- a/src/pdm/cli/completions/pdm.bash +++ b/src/pdm/cli/completions/pdm.bash @@ -29,7 +29,7 @@ _pdm_a919b69078acdf0a_complete() case "$com" in (add) - opts="--dev --dry-run --editable --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --prerelease --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --unconstrained --update-all --update-eager --update-reuse --verbose" + opts="--dev --dry-run --editable --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --prerelease --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --unconstrained --update-all --update-eager --update-reuse --venv --verbose" ;; (build) @@ -61,7 +61,7 @@ _pdm_a919b69078acdf0a_complete() ;; (info) - opts="--env --global --help --packages --project --python --verbose --where" + opts="--env --global --help --packages --project --python --venv --verbose --where" ;; (init) @@ -69,11 +69,11 @@ _pdm_a919b69078acdf0a_complete() ;; (install) - opts="--check --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-lock --no-self --production --project --skip --verbose" + opts="--check --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-lock --no-self --production --project --skip --venv --verbose" ;; (list) - opts="--csv --exclude --fields --freeze --global --graph --help --include --json --markdown --project --resolve --reverse --sort --verbose" + opts="--csv --exclude --fields --freeze --global --graph --help --include --json --markdown --project --resolve --reverse --sort --venv --verbose" ;; (lock) @@ -89,11 +89,11 @@ _pdm_a919b69078acdf0a_complete() ;; (remove) - opts="--dev --dry-run --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --project --skip --verbose" + opts="--dev --dry-run --fail-fast --global --group --help --lockfile --no-editable --no-isolation --no-self --no-sync --project --skip --venv --verbose" ;; (run) - opts="--global --help --list --project --site-packages --skip --verbose" + opts="--global --help --list --project --site-packages --skip --venv --verbose" ;; (search) @@ -105,19 +105,19 @@ _pdm_a919b69078acdf0a_complete() ;; (show) - opts="--global --help --keywords --license --name --platform --project --summary --verbose --version" + opts="--global --help --keywords --license --name --platform --project --summary --venv --verbose --version" ;; (sync) - opts="--clean --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --only-keep --production --project --reinstall --skip --verbose" + opts="--clean --dev --dry-run --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --only-keep --production --project --reinstall --skip --venv --verbose" ;; (update) - opts="--dev --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --no-sync --outdated --prerelease --production --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --top --unconstrained --update-all --update-eager --update-reuse --verbose" + opts="--dev --fail-fast --global --group --help --lockfile --no-default --no-editable --no-isolation --no-self --no-sync --outdated --prerelease --production --project --save-compatible --save-exact --save-minimum --save-wildcard --skip --top --unconstrained --update-all --update-eager --update-reuse --venv --verbose" ;; (use) - opts="--first --global --help --ignore-remembered --project --skip --verbose" + opts="--first --global --help --ignore-remembered --project --skip --venv --verbose" ;; (venv) diff --git a/src/pdm/cli/completions/pdm.fish b/src/pdm/cli/completions/pdm.fish index 987c87120b..4cf5d69f66 100644 --- a/src/pdm/cli/completions/pdm.fish +++ b/src/pdm/cli/completions/pdm.fish @@ -13,7 +13,7 @@ end # global options complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l config -d 'Specify another config file path(env var: PDM_CONFIG_FILE)' complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l help -d 'show this help message and exit' -complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l ignore-python -d 'Ignore the Python path saved in .pdm-python' +complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l ignore-python -d 'Ignore the Python path saved in .pdm-python. [env var: PDM_IGNORE_SAVED_PYTHON]' complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l pep582 -d 'Print the command line to be eval\'d by the shell' complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l verbose -d '-v for detailed output and -vv for more detailed' complete -c pdm -n '__fish_pdm_a919b69078acdf0a_complete_no_subcommand' -l version -d 'Show version' @@ -70,6 +70,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from add' -l unconstrained -d 'Ign complete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-all -d 'Update all dependencies and sub-dependencies' complete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-eager -d 'Try to update the packages and their dependencies recursively' complete -c pdm -A -n '__fish_seen_subcommand_from add' -l update-reuse -d 'Reuse pinned versions already present in lock file if possible' +complete -c pdm -A -n '__fish_seen_subcommand_from add' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from add' -l verbose -d '-v for detailed output and -vv for more detailed' # build @@ -134,9 +135,10 @@ complete -c pdm -A -n '__fish_seen_subcommand_from import' -l verbose -d '-v for complete -c pdm -A -n '__fish_seen_subcommand_from info' -l env -d 'Show PEP 508 environment markers' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l global -d 'Use the global project, supply the project root with `-p` option' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l help -d 'show this help message and exit' -complete -c pdm -A -n '__fish_seen_subcommand_from info' -l packages -d 'Show the packages root' +complete -c pdm -A -n '__fish_seen_subcommand_from info' -l packages -d 'Show the local packages root' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l python -d 'Show the interpreter path' +complete -c pdm -A -n '__fish_seen_subcommand_from info' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l verbose -d '-v for detailed output and -vv for more detailed' complete -c pdm -A -n '__fish_seen_subcommand_from info' -l where -d 'Show the project root path' @@ -168,6 +170,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-self -d 'Don\' complete -c pdm -A -n '__fish_seen_subcommand_from install' -l production -d 'Unselect dev dependencies' complete -c pdm -A -n '__fish_seen_subcommand_from install' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from install' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.' +complete -c pdm -A -n '__fish_seen_subcommand_from install' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from install' -l verbose -d '-v for detailed output and -vv for more detailed' # list @@ -185,6 +188,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from list' -l project -d 'Specify complete -c pdm -A -n '__fish_seen_subcommand_from list' -l resolve -d 'Resolve all requirements to output licenses (instead of just showing those currently installed)' complete -c pdm -A -n '__fish_seen_subcommand_from list' -l reverse -d 'Reverse the dependency graph' complete -c pdm -A -n '__fish_seen_subcommand_from list' -l sort -d 'Sort the output using a given field name. If nothing is set, no sort is applied. Multiple fields can be combined with \',\'.' +complete -c pdm -A -n '__fish_seen_subcommand_from list' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from list' -l verbose -d '-v for detailed output and -vv for more detailed' # lock @@ -234,6 +238,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-self -d 'Don\'t complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l no-sync -d 'Only write pyproject.toml and do not uninstall packages' complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.' +complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from remove' -l verbose -d '-v for detailed output and -vv for more detailed' # run @@ -243,6 +248,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from run' -l list -d 'Show all ava complete -c pdm -A -n '__fish_seen_subcommand_from run' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from run' -l site-packages -d 'Load site-packages from the selected interpreter' complete -c pdm -A -n '__fish_seen_subcommand_from run' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.' +complete -c pdm -A -n '__fish_seen_subcommand_from run' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from run' -l verbose -d '-v for detailed output and -vv for more detailed' # search @@ -262,6 +268,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from show' -l name -d 'Show name' complete -c pdm -A -n '__fish_seen_subcommand_from show' -l platform -d 'Show platform' complete -c pdm -A -n '__fish_seen_subcommand_from show' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from show' -l summary -d 'Show summary' +complete -c pdm -A -n '__fish_seen_subcommand_from show' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from show' -l verbose -d '-v for detailed output and -vv for more detailed' complete -c pdm -A -n '__fish_seen_subcommand_from show' -l version -d 'Show version' @@ -283,6 +290,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l production -d 'Unsel complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l reinstall -d 'Force reinstall existing dependencies' complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.' +complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from sync' -l verbose -d '-v for detailed output and -vv for more detailed' # update @@ -311,6 +319,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from update' -l unconstrained -d ' complete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-all -d 'Update all dependencies and sub-dependencies' complete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-eager -d 'Try to update the packages and their dependencies recursively' complete -c pdm -A -n '__fish_seen_subcommand_from update' -l update-reuse -d 'Reuse pinned versions already present in lock file if possible' +complete -c pdm -A -n '__fish_seen_subcommand_from update' -l venv -d 'Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]' complete -c pdm -A -n '__fish_seen_subcommand_from update' -l verbose -d '-v for detailed output and -vv for more detailed' # use @@ -320,6 +329,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from use' -l help -d 'show this he complete -c pdm -A -n '__fish_seen_subcommand_from use' -l ignore-remembered -d 'Ignore the remembered selection' complete -c pdm -A -n '__fish_seen_subcommand_from use' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__' complete -c pdm -A -n '__fish_seen_subcommand_from use' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.' +complete -c pdm -A -n '__fish_seen_subcommand_from use' -l venv -d 'Use the interpreter in the virtual environment with the given name' complete -c pdm -A -n '__fish_seen_subcommand_from use' -l verbose -d '-v for detailed output and -vv for more detailed' # venv diff --git a/src/pdm/cli/completions/pdm.ps1 b/src/pdm/cli/completions/pdm.ps1 index 33d545e130..8c5c9bd172 100644 --- a/src/pdm/cli/completions/pdm.ps1 +++ b/src/pdm/cli/completions/pdm.ps1 @@ -200,6 +200,7 @@ function TabExpansion($line, $lastWord) { $sectionOption = [Option]::new(@("-G", "--group")).WithValues(@(getSections)) $projectOption = [Option]::new(@("-p", "--project")).WithValues(@()) $skipOption = [Option]::new(@("-k", "--skip")).WithValues(@()) + $venvOption = [Option]::new(@("--venv")).WithValues(@()) $formatOption = [Option]::new(@("-f", "--format")).WithValues(@("setuppy", "requirements", "poetry", "flit")) Switch ($command) { @@ -214,6 +215,7 @@ function TabExpansion($line, $lastWord) { )), $sectionOption, $projectOption, + $venvOption, $skipOption [Option]::new(@("-e", "--editable")).WithValues(@(getPyPIPackages)) )) @@ -272,7 +274,8 @@ function TabExpansion($line, $lastWord) { $completer.AddOpts( @( [Option]::new(@("--env", "--global", "-g", "--python", "--where", "--packages")), - $projectOption + $projectOption, + $venvOption )) break } @@ -295,6 +298,7 @@ function TabExpansion($line, $lastWord) { )), $sectionOption, $skipOption, + $venvOption, $projectOption )) break @@ -303,6 +307,7 @@ function TabExpansion($line, $lastWord) { $completer.AddOpts( @( [Option]::new(@("--graph", "--global", "-g", "--reverse", "-r", "--freeze", "--json", "--csv", "--markdown", "--fields", "--sort", "--include", "--exclude", "--resolve")), + $venvOption, $projectOption )) break @@ -361,6 +366,7 @@ function TabExpansion($line, $lastWord) { )), $projectOption, $skipOption, + $venvOption, $sectionOption )) $completer.AddParams(@(getPdmPackages), $true) @@ -371,6 +377,7 @@ function TabExpansion($line, $lastWord) { @( [Option]::new(@("--global", "-g", "-l", "--list", "-s", "--site-packages")), $skipOption, + $venvOption, $projectOption )) $completer.AddParams(@(getScripts), $false) @@ -381,6 +388,7 @@ function TabExpansion($line, $lastWord) { $completer.AddOpts( @( [Option]::new(@("--global", "-g", "--name", "--version", "--summary", "--license", "--platform", "--keywords")), + $venvOption, $projectOption )) break @@ -393,6 +401,7 @@ function TabExpansion($line, $lastWord) { "-L", "--lockfile", "--fail-fast", "-x" )), $sectionOption, + $venvOption, $skipOption, $projectOption )) @@ -408,6 +417,7 @@ function TabExpansion($line, $lastWord) { )), $sectionOption, $skipOption, + $venvOption, $projectOption )) $completer.AddParams(@(getPdmPackages), $true) @@ -417,6 +427,7 @@ function TabExpansion($line, $lastWord) { $completer.AddOpts( @( [Option]::new(@("--global", "-g", "-f", "--first", "-i", "--ignore-remembered", "--skip")), + $venvOption, $projectOption )) break diff --git a/src/pdm/cli/completions/pdm.zsh b/src/pdm/cli/completions/pdm.zsh index 9eec685e73..d9d81406da 100644 --- a/src/pdm/cli/completions/pdm.zsh +++ b/src/pdm/cli/completions/pdm.zsh @@ -73,6 +73,7 @@ _pdm() { '--update-all[Update all dependencies and sub-dependencies]' '--no-editable[Install non-editable versions for all packages]' "--no-self[Don't install the project itself]" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]' {-u,--unconstrained}'[Ignore the version constraint of packages]' {--pre,--prerelease}'[Allow prereleases to be pinned]' @@ -173,6 +174,7 @@ _pdm() { '--where[Show the project root path]' '--env[Show PEP 508 environment markers]' '--packages[Show the packages root]' + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' ) ;; init) @@ -201,6 +203,7 @@ _pdm() { "--no-isolation[do not isolate the build in a clean environment]" "--dry-run[Show the difference only without modifying the lock file content]" "--check[Check if the lock file is up to date and fail otherwise]" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' ) ;; list) @@ -217,6 +220,7 @@ _pdm() { "--include[Dependency groups to include in the output. By default all are included]:include:" "--exclude[Dependency groups to exclude from the output]:exclude:" "--resolve[Resolve all requirements to output licenses (instead of just showing those currently installed)" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' ) ;; lock) @@ -308,6 +312,7 @@ _pdm() { {-x,--fail-fast}'[Abort on first installation error]' "--no-isolation[do not isolate the build in a clean environment]" "--dry-run[Show the difference only without modifying the lockfile content]" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' "*:packages:_pdm_packages" ) ;; @@ -317,6 +322,7 @@ _pdm() { {-l,--list}'[Show all available scripts defined in pyproject.toml]' \ {-k,--skip}'[Skip some tasks and/or hooks by their comma-separated names]' \ {-s,--site-packages}'[Load site-packages from the selected interpreter]' \ + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' \ '(-)1:command:->command' \ '*:arguments: _normal ' && return 0 if [[ $state == command ]]; then @@ -340,6 +346,7 @@ _pdm() { '--license[Show license]' '--platform[Show platform]' '--keywords[Show keywords]' + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' '1:package:' ) ;; @@ -360,6 +367,7 @@ _pdm() { '--no-editable[Install non-editable versions for all packages]' "--no-self[Don't install the project itself]" "--no-isolation[do not isolate the build in a clean environment]" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' ) ;; update) @@ -388,6 +396,7 @@ _pdm() { "--outdated[Show the difference only without modifying the lockfile content]" {-x,--fail-fast}'[Abort on first installation error]' "--no-isolation[do not isolate the build in a clean environment]" + '--venv[Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]]:venv:' "*:packages:_pdm_packages" ) ;; @@ -395,6 +404,7 @@ _pdm() { arguments+=( {-f,--first}'[Select the first matched interpreter]' {-i,--ignore-remembered}'[Ignore the remembered selection]' + '--venv[Use the interpreter in the virtual environment with the given name]:venv:' '*:python:_files' ) ;; diff --git a/src/pdm/cli/options.py b/src/pdm/cli/options.py index a635258b21..9fdffff8db 100644 --- a/src/pdm/cli/options.py +++ b/src/pdm/cli/options.py @@ -2,19 +2,17 @@ import argparse import os -from typing import Any, Sequence +import sys +from functools import partial +from typing import Any, Sequence, cast +from pdm.cli.actions import print_pep582_command from pdm.compat import Protocol +from pdm.project import Project class ActionCallback(Protocol): - def __call__( - self, - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: str | Sequence[Any] | None, - option_string: str | None, - ) -> None: + def __call__(self, project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None: ... @@ -33,6 +31,10 @@ def add_to_parser(self, parser: argparse._ActionsContainer) -> None: def add_to_group(self, group: argparse._ArgumentGroup) -> None: group.add_argument(*self.args, **self.kwargs) + def __call__(self, func: ActionCallback) -> Option: + self.kwargs.update(action=CallbackAction, callback=func) + return self + class CallbackAction(argparse.Action): def __init__(self, *args: Any, callback: ActionCallback, **kwargs: Any) -> None: @@ -46,7 +48,10 @@ def __call__( values: str | Sequence[Any] | None, option_string: str | None = None, ) -> None: - return self.callback(parser, namespace, values, option_string=option_string) + if not hasattr(namespace, "callbacks"): + namespace.callbacks = [] + callback = partial(self.callback, values=values) + namespace.callbacks.append(callback) class ArgumentGroup(Option): @@ -133,6 +138,7 @@ def from_splitted_env(name: str, separator: str) -> list[str] | None: help="Show the difference only and don't perform any action", ) + lockfile_option = Option( "-L", "--lockfile", @@ -140,13 +146,12 @@ def from_splitted_env(name: str, separator: str) -> list[str] | None: help="Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]", ) -pep582_option = Option( - "--pep582", - const="AUTO", - metavar="SHELL", - nargs="?", - help="Print the command line to be eval'd by the shell", -) + +@Option("--pep582", const="AUTO", metavar="SHELL", nargs="?", help="Print the command line to be eval'd by the shell") +def pep582_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None: + print_pep582_command(project, cast(str, values)) + sys.exit(0) + install_group = ArgumentGroup("Install options") install_group.add_argument( @@ -164,23 +169,11 @@ def from_splitted_env(name: str, separator: str) -> list[str] | None: install_group.add_argument("--fail-fast", "-x", action="store_true", help="Abort on first installation error") -def no_isolation_callback( - parser: argparse.ArgumentParser, - namespace: argparse.Namespace, - values: str | Sequence[Any] | None, - option_string: str | None, -) -> None: +@Option("--no-isolation", dest="build_isolation", nargs=0, help="Do not isolate the build in a clean environment") +def no_isolation_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None: os.environ["PDM_BUILD_ISOLATION"] = "no" -no_isolation_option = Option( - "--no-isolation", - dest="build_isolation", - action=CallbackAction, - nargs=0, - help="Do not isolate the build in a clean environment", - callback=no_isolation_callback, -) install_group.options.append(no_isolation_option) groups_group = ArgumentGroup("Dependencies selection") @@ -321,14 +314,16 @@ def no_isolation_callback( ) packages_group.add_argument("packages", nargs="*", help="Specify packages") -ignore_python_option = Option( + +@Option( "-I", "--ignore-python", - action=CallbackAction, nargs=0, - help="Ignore the Python path saved in .pdm-python", - callback=lambda *args, **kwargs: os.environ.update({"PDM_IGNORE_SAVED_PYTHON": "1"}), + help="Ignore the Python path saved in .pdm-python. [env var: PDM_IGNORE_SAVED_PYTHON]", ) +def ignore_python_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None: + os.environ.update({"PDM_IGNORE_SAVED_PYTHON": "1"}) + prerelease_option = Option( "--pre", @@ -344,3 +339,14 @@ def no_isolation_callback( default=False, help="Ignore the version constraint of packages", ) + + +venv_option = Option( + "--venv", + dest="use_venv", + metavar="NAME", + nargs="?", + const="in-project", + help="Run the command in the virtual environment with the given key. [env var: PDM_USE_VENV]", + default=os.getenv("PDM_USE_VENV"), +) diff --git a/src/pdm/cli/utils.py b/src/pdm/cli/utils.py index 16f1b619fd..ad08ad97ad 100644 --- a/src/pdm/cli/utils.py +++ b/src/pdm/cli/utils.py @@ -660,3 +660,14 @@ def get_pep582_path(project: Project) -> str: with resources_open_binary("pdm.pep582", "sitecustomize.py") as f: script_dir.joinpath("sitecustomize.py").write_bytes(f.read()) return str(script_dir) + + +def use_venv(project: Project, name: str) -> None: + from pdm.cli.commands.venv.utils import get_venv_python, get_venv_with_name + from pdm.environments import PythonEnvironment + + venv = get_venv_with_name(project, cast(str, name)) + project.core.ui.echo( + f"In virtual environment: [success]{venv}[/]", err=True, style="info", verbosity=termui.Verbosity.DETAIL + ) + project.environment = PythonEnvironment(project, python=str(get_venv_python(venv))) diff --git a/src/pdm/core.py b/src/pdm/core.py index 87ba0d069d..75710f7ae3 100644 --- a/src/pdm/core.py +++ b/src/pdm/core.py @@ -20,7 +20,7 @@ from pdm import termui from pdm.__version__ import __version__ -from pdm.cli.actions import check_update, print_pep582_command +from pdm.cli.actions import check_update from pdm.cli.commands.base import BaseCommand from pdm.cli.options import ignore_python_option, pep582_option, verbose_option from pdm.cli.utils import ErrorArgumentParser, PdmFormatter @@ -104,9 +104,6 @@ def ensure_project(self, options: argparse.Namespace, obj: Project | None) -> Pr is_global=global_project, global_config=options.config or os.getenv("PDM_CONFIG_FILE"), ) - - if getattr(options, "lockfile", None): - project.set_lockfile(options.lockfile) return project def create_project( @@ -131,18 +128,25 @@ def before_invoke(self, project: Project, command: BaseCommand | None, options: """Called before command invocation""" from pdm.cli.commands.fix import Command as FixCommand from pdm.cli.hooks import HookManager + from pdm.cli.utils import use_venv self.ui.set_verbosity(options.verbose) self.ui.set_theme(project.global_config.load_theme()) + hooks = HookManager(project, getattr(options, "skip", None)) hooks.try_emit("pre_invoke", command=command.name if command else None, options=options) if not isinstance(command, FixCommand): FixCommand.check_problems(project) - if options.pep582: - print_pep582_command(project, options.pep582) - sys.exit(0) + for callback in getattr(options, "callbacks", []): + callback(project, options) + + if getattr(options, "lockfile", None): + project.set_lockfile(cast(str, options.lockfile)) + + if getattr(options, "use_venv", None): + use_venv(project, cast(str, options.use_venv)) def main( self, @@ -173,16 +177,15 @@ def main( project = self.ensure_project(options, obj) command = getattr(options, "command", None) - self.before_invoke(project, command, options) if root_script and root_script not in project.scripts: self.parser.error(f"Script unknown: {root_script}") if command is None: - self.parser.print_help(sys.stderr) - sys.exit(1) + self.parser.error("No command given") assert isinstance(command, BaseCommand) try: + self.before_invoke(project, command, options) command.handle(project, options) except Exception: etype, err, traceback = sys.exc_info() diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py index a824acd5d7..fd02f54f72 100644 --- a/src/pdm/project/core.py +++ b/src/pdm/project/core.py @@ -191,7 +191,7 @@ def match_version(python: PythonInfo) -> bool: def note(message: str) -> None: if not self.is_global: - self.core.ui.echo(message, style="warning", err=True) + self.core.ui.echo(message, style="info", err=True) config = self.config saved_path = self._saved_python @@ -199,6 +199,11 @@ def note(message: str) -> None: python = PythonInfo.from_path(saved_path) if match_version(python): return python + else: + note( + "The saved Python interpreter doesn't match the project's requirement. " + "Trying to find another one." + ) self._saved_python = None # Clear the saved path if it doesn't match if config.get("python.use_venv") and not self.is_global and not os.getenv("PDM_IGNORE_ACTIVE_VENV"): diff --git a/tests/cli/test_list.py b/tests/cli/test_list.py index 0250f0cbdf..c0978df067 100644 --- a/tests/cli/test_list.py +++ b/tests/cli/test_list.py @@ -699,6 +699,28 @@ def test_list_csv_include_exclude_valid(project, pdm): assert ":sub" in result.outputs +@pytest.mark.usefixtures("local_finder") +def test_list_packages_in_given_venv(project, pdm): + project.pyproject.metadata["requires-python"] = ">=3.7" + project.pyproject.write() + project.global_config["python.use_venv"] = True + pdm(["venv", "create"], obj=project, strict=True) + pdm(["venv", "create", "--name", "second"], obj=project, strict=True) + project._saved_python = None + pdm(["add", "first", "--no-self"], obj=project, strict=True) + second_lockfile = str(project.root / "pdm.2.lock") + pdm( + ["add", "-G", "second", "--no-self", "-L", second_lockfile, "--venv", "second", "editables"], + obj=project, + strict=True, + ) + project.environment = None + result1 = pdm(["list", "--freeze"], obj=project, strict=True) + result2 = pdm(["list", "--freeze", "--venv", "second"], obj=project, strict=True) + assert result1.output.strip() == "first==2.0.2" + assert result2.output.strip() == "editables==0.2" + + @pytest.mark.usefixtures("working_set", "repository") def test_list_csv_include_exclude(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") diff --git a/tests/cli/test_others.py b/tests/cli/test_others.py index 6fc830fbea..4c6690033b 100644 --- a/tests/cli/test_others.py +++ b/tests/cli/test_others.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from pdm.cli import actions @@ -23,43 +25,60 @@ def test_project_no_init_error(project_no_init, pdm): assert "The pyproject.toml has not been initialized yet" in result.stderr -def test_help_option(invoke): - result = invoke(["--help"]) +def test_help_option(pdm): + result = pdm(["--help"]) assert "Usage: pdm [-h]" in result.output -def test_info_command(project, invoke): - result = invoke(["info"], obj=project) +def test_info_command(project, pdm): + result = pdm(["info"], obj=project) assert "Project Root:" in result.output assert project.root.as_posix() in result.output - result = invoke(["info", "--python"], obj=project) + result = pdm(["info", "--python"], obj=project) assert result.output.strip() == str(project.python.executable) - result = invoke(["info", "--where"], obj=project) + result = pdm(["info", "--where"], obj=project) assert result.output.strip() == str(project.root) - result = invoke(["info", "--env"], obj=project) + result = pdm(["info", "--env"], obj=project) assert result.exit_code == 0 -def test_info_global_project(invoke, tmp_path): +def test_info_global_project(pdm, tmp_path): with cd(tmp_path): - result = invoke(["info", "-g", "--where"]) + result = pdm(["info", "-g", "--where"]) assert "global-project" in result.output.strip() -def test_global_project_other_location(invoke, project): - result = invoke(["info", "-g", "-p", project.root.as_posix(), "--where"]) +def test_info_with_multiple_venvs(pdm, project): + project.global_config["python.use_venv"] = True + pdm(["venv", "create"], obj=project, strict=True) + pdm(["venv", "create", "--name", "test"], obj=project, strict=True) + project._saved_python = None + result = pdm(["info", "--python"], obj=project, strict=True) + assert Path(result.output.strip()).parent.parent == project.root / ".venv" + venv_location = project.config["venv.location"] + result = pdm(["info", "--python", "--venv", "test"], obj=project, strict=True) + assert Path(result.output.strip()).parent.parent.parent == project.root / venv_location + + result = pdm(["info", "--python", "--venv", "test"], obj=project, strict=True, env={"PDM_USE_VENV": "test"}) + assert Path(result.output.strip()).parent.parent.parent == project.root / venv_location + result = pdm(["info", "--python", "--venv", "default"], obj=project) + assert "No virtualenv with key 'default' is found" in result.stderr + + +def test_global_project_other_location(pdm, project): + result = pdm(["info", "-g", "-p", project.root.as_posix(), "--where"]) assert result.stdout.strip() == str(project.root) -def test_uncaught_error(invoke, mocker): +def test_uncaught_error(pdm, mocker): mocker.patch.object(actions, "do_lock", side_effect=RuntimeError("test error")) - result = invoke(["lock"]) + result = pdm(["lock"]) assert "[RuntimeError]: test error" in result.stderr - result = invoke(["lock", "-v"]) + result = pdm(["lock", "-v"]) assert isinstance(result.exception, RuntimeError) @@ -72,25 +91,25 @@ def test_uncaught_error(invoke, mocker): "projects/flit-demo/pyproject.toml", ], ) -def test_import_other_format_file(project, invoke, filename): +def test_import_other_format_file(project, pdm, filename): requirements_file = FIXTURES / filename - result = invoke(["import", str(requirements_file)], obj=project) + result = pdm(["import", str(requirements_file)], obj=project) assert result.exit_code == 0 -def test_import_requirement_no_overwrite(project, invoke, tmp_path): +def test_import_requirement_no_overwrite(project, pdm, tmp_path): project.add_dependencies({"requests": parse_requirement("requests")}) tmp_path.joinpath("reqs.txt").write_text("flask\nflask-login\n") - result = invoke(["import", "-dGweb", str(tmp_path.joinpath("reqs.txt"))], obj=project) + result = pdm(["import", "-dGweb", str(tmp_path.joinpath("reqs.txt"))], obj=project) assert result.exit_code == 0, result.stderr assert list(project.get_dependencies()) == ["requests"] assert list(project.get_dependencies("web")) == ["flask", "flask-login"] @pytest.mark.network -def test_search_package(invoke, tmp_path): +def test_search_package(pdm, tmp_path): with cd(tmp_path): - result = invoke(["search", "requests"]) + result = pdm(["search", "requests"]) assert result.exit_code == 0 assert len(result.output.splitlines()) > 0 assert not tmp_path.joinpath("__pypackages__").exists() @@ -98,78 +117,78 @@ def test_search_package(invoke, tmp_path): @pytest.mark.network -def test_show_package_on_pypi(invoke): - result = invoke(["show", "ipython"]) +def test_show_package_on_pypi(pdm): + result = pdm(["show", "ipython"]) assert result.exit_code == 0 assert "ipython" in result.output.splitlines()[0] - result = invoke(["show", "requests"]) + result = pdm(["show", "requests"]) assert result.exit_code == 0 assert "requests" in result.output.splitlines()[0] - result = invoke(["show", "--name", "requests"]) + result = pdm(["show", "--name", "requests"]) assert result.exit_code == 0 assert "requests" in result.output.splitlines()[0] - result = invoke(["show", "--name", "sphinx-data-viewer"]) + result = pdm(["show", "--name", "sphinx-data-viewer"]) assert result.exit_code == 0 assert "sphinx-data-viewer" in result.output.splitlines()[0] -def test_show_self_package(project, invoke): - result = invoke(["show"], obj=project) +def test_show_self_package(project, pdm): + result = pdm(["show"], obj=project) assert result.exit_code == 0, result.stderr - result = invoke(["show", "--name", "--version"], obj=project) + result = pdm(["show", "--name", "--version"], obj=project) assert result.exit_code == 0 assert "test_project\n0.0.0\n" == result.output -def test_export_to_requirements_txt(invoke, fixture_project): +def test_export_to_requirements_txt(pdm, fixture_project): project = fixture_project("demo-package") requirements_txt = project.root / "requirements.txt" requirements_no_hashes = project.root / "requirements_simple.txt" requirements_pyproject = project.root / "requirements.ini" - result = invoke(["export"], obj=project) + result = pdm(["export"], obj=project) assert result.exit_code == 0 assert result.output.strip() == requirements_txt.read_text().strip() - result = invoke(["export", "--without-hashes"], obj=project) + result = pdm(["export", "--without-hashes"], obj=project) assert result.exit_code == 0 assert result.output.strip() == requirements_no_hashes.read_text().strip() - result = invoke(["export", "--pyproject"], obj=project) + result = pdm(["export", "--pyproject"], obj=project) assert result.exit_code == 0 assert result.output.strip() == requirements_pyproject.read_text().strip() - result = invoke(["export", "-o", str(project.root / "requirements_output.txt")], obj=project) + result = pdm(["export", "-o", str(project.root / "requirements_output.txt")], obj=project) assert result.exit_code == 0 assert (project.root / "requirements_output.txt").read_text() == requirements_txt.read_text() -def test_export_doesnt_include_dep_with_extras(invoke, fixture_project): +def test_export_doesnt_include_dep_with_extras(pdm, fixture_project): project = fixture_project("demo-package-has-dep-with-extras") requirements_txt = project.root / "requirements.txt" - result = invoke(["export", "--without-hashes"], obj=project) + result = pdm(["export", "--without-hashes"], obj=project) assert result.exit_code == 0 assert result.output.strip() == requirements_txt.read_text().strip() -def test_completion_command(invoke): - result = invoke(["completion", "bash"]) +def test_completion_command(pdm): + result = pdm(["completion", "bash"]) assert result.exit_code == 0 assert "(completion)" in result.output @pytest.mark.network -def test_show_update_hint(invoke, project, monkeypatch): +def test_show_update_hint(pdm, project, monkeypatch): monkeypatch.delenv("PDM_CHECK_UPDATE", raising=False) prev_version = project.core.version try: project.core.version = "0.0.0" - r = invoke(["config"], obj=project) + r = pdm(["config"], obj=project) finally: project.core.version = prev_version assert "to upgrade." in r.stderr diff --git a/tests/cli/test_use.py b/tests/cli/test_use.py index 6b28db838f..0ce93e95bd 100644 --- a/tests/cli/test_use.py +++ b/tests/cli/test_use.py @@ -70,3 +70,16 @@ def test_use_remember_last_selection(project, mocker): mocker.patch.object(project, "find_interpreters") actions.do_use(project, "3") project.find_interpreters.assert_not_called() + + +def test_use_venv_python(project, pdm): + pdm(["venv", "create"], obj=project, strict=True) + pdm(["venv", "create", "--name", "test"], obj=project, strict=True) + project.global_config["python.use_venv"] = True + venv_location = project.config["venv.location"] + actions.do_use(project, venv="in-project") + assert project.python.executable.parent.parent == project.root.joinpath(".venv") + actions.do_use(project, venv="test") + assert project.python.executable.parent.parent.parent == Path(venv_location) + with pytest.raises(Exception, match="No virtualenv with key 'non-exists' is found"): + actions.do_use(project, venv="non-exists")