From cec8954581fb861271f43e825eb04edac03707ab Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Tue, 28 Feb 2023 11:33:04 +0800 Subject: [PATCH] feat: encode selected groups into lockfile (#1741) --- docs/docs/usage/dependency.md | 30 ++++- news/1704.feature.md | 1 + src/pdm/cli/actions.py | 124 ++++++++++--------- src/pdm/cli/commands/add.py | 4 +- src/pdm/cli/commands/export.py | 12 +- src/pdm/cli/commands/install.py | 11 +- src/pdm/cli/commands/lock.py | 13 +- src/pdm/cli/commands/remove.py | 4 +- src/pdm/cli/commands/sync.py | 5 +- src/pdm/cli/commands/update.py | 5 +- src/pdm/cli/filters.py | 102 ++++++++++++++++ src/pdm/cli/options.py | 2 +- src/pdm/cli/utils.py | 31 +---- src/pdm/project/lockfile.py | 7 +- src/pdm/project/project_file.py | 2 +- src/pdm/pytest.py | 2 + tests/cli/test_add.py | 207 +++++++++++++++---------------- tests/cli/test_install.py | 163 ++++++++++++++----------- tests/cli/test_list.py | 208 +++++++++++++++----------------- tests/cli/test_lock.py | 16 ++- tests/cli/test_others.py | 14 +-- tests/cli/test_remove.py | 89 +++++++------- tests/cli/test_update.py | 130 +++++++++++--------- tests/conftest.py | 4 +- tests/test_formats.py | 3 + tests/test_signals.py | 8 +- tests/test_utils.py | 36 ++++-- 27 files changed, 692 insertions(+), 541 deletions(-) create mode 100644 news/1704.feature.md create mode 100644 src/pdm/cli/filters.py diff --git a/docs/docs/usage/dependency.md b/docs/docs/usage/dependency.md index 27cf673e5e..43914fb931 100644 --- a/docs/docs/usage/dependency.md +++ b/docs/docs/usage/dependency.md @@ -256,7 +256,7 @@ There are a few similar commands to do this job with slight differences: You can specify another lockfile than the default [`pdm lock`](../reference/cli.md#exec-0--lock) by using the `-L/--lockfile ` option or the `PDM_LOCKFILE` environment variable. -### Select a subset of dependencies with CLI options +## Select a subset of dependency groups to be installed or locked Say we have a project with following dependencies: @@ -288,6 +288,34 @@ dev2 = ["mkdocs"] Besides, if you don't want the root project to be installed, add `--no-self` option, and `--no-editable` can be used when you want all packages to be installed in non-editable versions. With `--no-editable` turn on, you can safely archive the whole `__pypackages__` and copy it to the target environment for deployment. +You may also use the pdm lock command with these options to lock only the specified groups, which will be recorded in the `[metadata]` table of the lock file. If no `--group/--prod/--dev/--no-default` option is specified, `pdm sync` and `pdm update` will operate using the groups in the lockfile. However, if any groups that are not included in the lockfile are given as arguments to the commands, PDM will raise an error. + +This feature is especially valuable when managing multiple lockfiles, where each may have different versions of the same package pinned. To switch between lockfiles, you can use the `--lockfile/-L` option. + +For a realistic example, your project depends on a release version of `werkzeug` and you may want to work with a local in-development copy of it when developing. You can add the following to your `pyproject.toml`: + +```toml +[project] +requires-python = ">=3.7" +dependencies = ["werkzeug"] + +[tool.pdm.dev-dependencies] +dev = ["werkzeug @ file:///${PROJECT_ROOT}/dev/werkzeug"] +``` + +Then, run `pdm lock` with different options to generate lockfiles for different purposes: + +```bash +# Lock default + dev, write to pdm.lock +# with the local copy of werkzeug pinned. +pdm lock +# Lock default, write to pdm.prod.lock +# with the release version of werkzeug pinned. +pdm lock --prod -L pdm.prod.lock +``` + +Check the `metadata.groups` field in the lockfile to see which groups are included. + ## Show what packages are installed Similar to `pip list`, you can list all packages installed in the packages directory: diff --git a/news/1704.feature.md b/news/1704.feature.md new file mode 100644 index 0000000000..05b1674e87 --- /dev/null +++ b/news/1704.feature.md @@ -0,0 +1 @@ +Only lock selected groups into the lockfile. Modify other commands to honor the groups included in the lockfile. diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py index 95359a30f2..433c095ec7 100644 --- a/src/pdm/cli/actions.py +++ b/src/pdm/cli/actions.py @@ -12,7 +12,7 @@ from argparse import Namespace from collections import defaultdict from itertools import chain -from typing import Collection, Iterable, Mapping, Sequence, cast +from typing import Collection, Iterable, Mapping, cast import tomlkit from resolvelib.reporters import BaseReporter @@ -20,6 +20,7 @@ from tomlkit.items import Array from pdm import termui +from pdm.cli.filters import GroupSelection from pdm.cli.hooks import HookManager from pdm.cli.utils import ( check_project_file, @@ -31,7 +32,6 @@ merge_dictionary, save_version_specifiers, set_env_in_reg, - translate_groups, ) from pdm.exceptions import NoPythonVersion, PdmUsageError, ProjectError from pdm.formats import FORMATS @@ -55,6 +55,7 @@ def do_lock( requirements: list[Requirement] | None = None, dry_run: bool = False, refresh: bool = False, + groups: list[str] | None = None, hooks: HookManager | None = None, ) -> dict[str, Candidate]: """Performs the locking process and update lockfile.""" @@ -75,12 +76,14 @@ def do_lock( with project.core.ui.logging("lock"): fetch_hashes(repo, mapping) lockfile = format_lockfile(project, mapping, dependencies) - project.write_lockfile(lockfile) + project.write_lockfile(lockfile, groups=groups) return mapping # TODO: multiple dependency definitions for the same package. provider = project.get_provider(strategy, tracked_names) if not requirements: - requirements = [r for deps in project.all_dependencies.values() for r in deps.values()] + requirements = [ + r for g, deps in project.all_dependencies.items() if groups is None or g in groups for r in deps.values() + ] resolve_max_rounds = int(project.config["strategy.resolve_max_rounds"]) ui = project.core.ui with ui.logging("lock"): @@ -116,7 +119,7 @@ def do_lock( else: data = format_lockfile(project, mapping, dependencies) ui.echo(f"{termui.Emoji.LOCK} Lock successful") - project.write_lockfile(data, write=not dry_run) + project.write_lockfile(data, write=not dry_run, groups=groups) hooks.try_emit("post_lock", resolution=mapping, dry_run=dry_run) return mapping @@ -171,9 +174,7 @@ def check_lockfile(project: Project, raise_not_exist: bool = True) -> str | None def do_sync( project: Project, *, - groups: Collection[str] = (), - dev: bool = True, - default: bool = True, + selection: GroupSelection, dry_run: bool = False, clean: bool = False, requirements: list[Requirement] | None = None, @@ -188,9 +189,8 @@ def do_sync( """Synchronize project""" hooks = hooks or HookManager(project) if requirements is None: - groups = translate_groups(project, default, dev, groups or ()) requirements = [] - for group in groups: + for group in selection: requirements.extend(project.get_dependencies(group).values()) candidates = resolve_candidates_from_lockfile(project, requirements) if tracked_names and dry_run: @@ -201,7 +201,7 @@ def do_sync( clean=clean, dry_run=dry_run, no_editable=no_editable, - install_self=not no_self and "default" in groups and bool(project.name), + install_self=not no_self and "default" in selection and bool(project.name), use_install_cache=project.config["install.cache"], reinstall=reinstall, only_keep=only_keep, @@ -216,8 +216,7 @@ def do_sync( def do_add( project: Project, *, - dev: bool = False, - group: str | None = None, + selection: GroupSelection, sync: bool = True, save: str = "compatible", strategy: str = "reuse", @@ -234,15 +233,16 @@ def do_add( """Add packages and install""" hooks = hooks or HookManager(project) check_project_file(project) - if not editables and not packages: - raise PdmUsageError("Must specify at least one package or editable package.") if editables and no_editable: raise PdmUsageError("Cannot use --no-editable with editable packages given.") - if not group: - group = "dev" if dev else "default" + group = selection.one() tracked_names: set[str] = set() requirements: dict[str, Requirement] = {} - if group == "default" or not dev and group not in project.pyproject.settings.get("dev-dependencies", {}): + lock_groups = project.lockfile.groups + if lock_groups 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", {}): if editables: raise PdmUsageError("Cannot add editables to the default or optional dependency group") for r in [parse_requirement(line, True) for line in editables] + [parse_requirement(line) for line in packages]: @@ -259,35 +259,44 @@ def do_add( r.prerelease = prerelease tracked_names.add(key) requirements[key] = r - if not requirements: - return - project.core.ui.echo( - f"Adding packages to [primary]{group}[/] " - f"{'dev-' if dev else ''}dependencies: " + ", ".join(f"[req]{r.as_line()}[/]" for r in requirements.values()) - ) + if requirements: + project.core.ui.echo( + f"Adding packages to [primary]{group}[/] " + f"{'dev-' if selection.dev else ''}dependencies: " + + ", ".join(f"[req]{r.as_line()}[/]" for r in requirements.values()) + ) all_dependencies = project.all_dependencies group_deps = all_dependencies.setdefault(group, {}) if unconstrained: + if not requirements: + raise PdmUsageError("--unconstrained requires at least one package") for req in group_deps.values(): req.specifier = get_specifier("") group_deps.update(requirements) - reqs = [r for deps in all_dependencies.values() for r in deps.values()] + reqs = [r for g, deps in all_dependencies.items() if lock_groups is None or g in lock_groups for r in deps.values()] with hooks.skipping("post_lock"): - resolved = do_lock(project, strategy, tracked_names, reqs, dry_run=dry_run, hooks=hooks) + resolved = do_lock( + project, + strategy, + tracked_names, + reqs, + dry_run=True, + hooks=hooks, + groups=lock_groups, + ) # Update dependency specifiers and lockfile hash. deps_to_update = group_deps if unconstrained else requirements save_version_specifiers({group: deps_to_update}, resolved, save) if not dry_run: - project.add_dependencies(deps_to_update, group, dev) - project.write_lockfile(project.lockfile._data, False) + project.add_dependencies(deps_to_update, group, selection.dev or False) + project.write_lockfile(project.lockfile._data, False, groups=lock_groups) hooks.try_emit("post_lock", resolution=resolved, dry_run=dry_run) _populate_requirement_names(group_deps) if sync: do_sync( project, - groups=(group,), - default=False, + selection=GroupSelection(project, groups=[group], default=False), no_editable=no_editable and tracked_names, no_self=no_self, requirements=list(group_deps.values()), @@ -308,9 +317,7 @@ def _populate_requirement_names(req_mapping: dict[str, Requirement]) -> None: def do_update( project: Project, *, - dev: bool | None = None, - groups: Sequence[str] = (), - default: bool = True, + selection: GroupSelection, strategy: str = "reuse", save: str = "compatible", unconstrained: bool = False, @@ -327,19 +334,20 @@ def do_update( """Update specified packages or all packages""" hooks = hooks or HookManager(project) check_project_file(project) - if len(packages) > 0 and (top or len(groups) > 1 or not default): - raise PdmUsageError("packages argument can't be used together with multiple -G or --no-default and --top.") + if len(packages) > 0 and (top or selection.groups or not selection.default): + raise PdmUsageError("packages argument can't be used together with multiple -G or " "--no-default or --top.") all_dependencies = project.all_dependencies updated_deps: dict[str, dict[str, Requirement]] = defaultdict(dict) - install_dev = True if dev is None else dev + locked_groups = project.lockfile.groups if not packages: if prerelease: raise PdmUsageError("--prerelease must be used with packages given") - groups = translate_groups(project, default, install_dev, groups or ()) - for group in groups: + for group in selection: updated_deps[group] = all_dependencies[group] else: - group = groups[0] if groups else ("dev" if dev else "default") + group = selection.one() + if locked_groups and group not in locked_groups: + raise ProjectError(f"Requested group not in lockfile: {group}") dependencies = all_dependencies[group] for name in packages: matched_name = next( @@ -348,7 +356,8 @@ def do_update( ) if not matched_name: raise ProjectError( - f"[req]{name}[/] does not exist in [primary]{group}[/] {'dev-' if dev else ''}dependencies." + f"[req]{name}[/] does not exist in [primary]{group}[/] " + f"{'dev-' if selection.dev else ''}dependencies." ) dependencies[matched_name].prerelease = prerelease updated_deps[group][matched_name] = dependencies[matched_name] @@ -367,23 +376,23 @@ def do_update( strategy, chain.from_iterable(updated_deps.values()), reqs, - dry_run=dry_run, + dry_run=True, hooks=hooks, + groups=locked_groups, ) for deps in updated_deps.values(): _populate_requirement_names(deps) - if unconstrained and not dry_run: + if unconstrained: # Need to update version constraints save_version_specifiers(updated_deps, resolved, save) for group, deps in updated_deps.items(): - project.add_dependencies(deps, group, dev or False) - project.write_lockfile(project.lockfile._data, False) + project.add_dependencies(deps, group, selection.dev or False) + if not dry_run: + project.write_lockfile(project.lockfile._data, False, groups=locked_groups) if sync or dry_run: do_sync( project, - groups=groups, - dev=install_dev, - default=default, + selection=selection, clean=False, dry_run=dry_run, requirements=[r for deps in updated_deps.values() for r in deps.values()], @@ -397,8 +406,7 @@ def do_update( def do_remove( project: Project, - dev: bool = False, - group: str | None = None, + selection: GroupSelection, sync: bool = True, packages: Collection[str] = (), no_editable: bool = False, @@ -412,15 +420,13 @@ def do_remove( check_project_file(project) if not packages: raise PdmUsageError("Must specify at least one package to remove.") - if not group: - group = "dev" if dev else "default" - if group not in list(project.iter_groups()): - raise ProjectError(f"Non-exist group {group}") + group = selection.one() + lock_groups = project.lockfile.groups - deps, _ = project.get_pyproject_dependencies(group, dev) + deps, _ = project.get_pyproject_dependencies(group, selection.dev or False) project.core.ui.echo( f"Removing packages from [primary]{group}[/] " - f"{'dev-' if dev else ''}dependencies: " + ", ".join(f"[req]{name}[/]" for name in packages) + f"{'dev-' if selection.dev else ''}dependencies: " + ", ".join(f"[req]{name}[/]" for name in packages) ) with cd(project.root): for name in packages: @@ -434,12 +440,14 @@ def do_remove( if not dry_run: project.pyproject.write() - do_lock(project, "reuse", dry_run=dry_run, hooks=hooks) + if lock_groups and group not in lock_groups: + project.core.ui.echo(f"Group [success]{group}[/] isn't in lockfile, skipping lock.", style="warning", err=True) + return + do_lock(project, "reuse", dry_run=dry_run, hooks=hooks, groups=lock_groups) if sync: do_sync( project, - groups=(group,), - default=False, + selection=GroupSelection(project, default=False, groups=[group]), clean=True, no_editable=no_editable, no_self=no_self, diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py index b4170bff71..d21184f0ad 100644 --- a/src/pdm/cli/commands/add.py +++ b/src/pdm/cli/commands/add.py @@ -2,6 +2,7 @@ from pdm.cli import actions 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, @@ -56,8 +57,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: raise PdmUsageError("`--no-editable` cannot be used with `-e/--editable`") actions.do_add( project, - dev=options.dev, - group=options.group, + selection=GroupSelection.from_options(project, options), sync=options.sync, save=options.save_strategy or project.config["strategy.save"], strategy=options.update_strategy or project.config["strategy.update"], diff --git a/src/pdm/cli/commands/export.py b/src/pdm/cli/commands/export.py index 6afd408ce8..c6394f3706 100644 --- a/src/pdm/cli/commands/export.py +++ b/src/pdm/cli/commands/export.py @@ -6,8 +6,8 @@ from pdm.cli.actions import resolve_candidates_from_lockfile from pdm.cli.commands.base import BaseCommand +from pdm.cli.filters import GroupSelection from pdm.cli.options import groups_group, lockfile_option -from pdm.cli.utils import translate_groups from pdm.formats import FORMATS from pdm.models.candidates import Candidate from pdm.models.requirements import Requirement @@ -46,18 +46,12 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: ) def handle(self, project: Project, options: argparse.Namespace) -> None: - groups: list[str] = list(options.groups) if options.pyproject: options.hashes = False - groups = translate_groups( - project, - options.default, - options.dev, - options.groups or (), - ) + selection = GroupSelection.from_options(project, options) requirements: dict[str, Requirement] = {} packages: Iterable[Requirement] | Iterable[Candidate] - for group in groups: + for group in selection: requirements.update(project.get_dependencies(group)) if options.pyproject: packages = requirements.values() diff --git a/src/pdm/cli/commands/install.py b/src/pdm/cli/commands/install.py index 374951b4f9..085043085a 100644 --- a/src/pdm/cli/commands/install.py +++ b/src/pdm/cli/commands/install.py @@ -4,6 +4,7 @@ from pdm import termui from pdm.cli import actions 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, @@ -41,6 +42,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: hooks = HookManager(project, options.skip) strategy = actions.check_lockfile(project, False) + selection = GroupSelection.from_options(project, options) if strategy: if options.check: project.core.ui.echo( @@ -50,13 +52,14 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: sys.exit(1) if options.lock: project.core.ui.echo("Updating the lock file...", style="success", err=True) - actions.do_lock(project, strategy=strategy, dry_run=options.dry_run, hooks=hooks) + + actions.do_lock( + project, strategy=strategy, dry_run=options.dry_run, hooks=hooks, groups=selection.all() + ) actions.do_sync( project, - groups=options.groups, - dev=options.dev, - default=options.default, + selection=selection, no_editable=options.no_editable, no_self=options.no_self, dry_run=options.dry_run, diff --git a/src/pdm/cli/commands/lock.py b/src/pdm/cli/commands/lock.py index f2a9d5d102..4e4d210972 100644 --- a/src/pdm/cli/commands/lock.py +++ b/src/pdm/cli/commands/lock.py @@ -4,15 +4,21 @@ from pdm import termui from pdm.cli import actions from pdm.cli.commands.base import BaseCommand +from pdm.cli.filters import GroupSelection from pdm.cli.hooks import HookManager -from pdm.cli.options import lockfile_option, no_isolation_option, skip_option +from pdm.cli.options import ( + groups_group, + lockfile_option, + no_isolation_option, + skip_option, +) from pdm.project import Project class Command(BaseCommand): """Resolve and lock dependencies""" - arguments = [*BaseCommand.arguments, lockfile_option, no_isolation_option, skip_option] + arguments = [*BaseCommand.arguments, lockfile_option, no_isolation_option, skip_option, groups_group] def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( @@ -44,9 +50,10 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: verbosity=termui.Verbosity.DETAIL, ) sys.exit(0) - + selection = GroupSelection.from_options(project, options) actions.do_lock( project, refresh=options.refresh, + groups=selection.all(), hooks=HookManager(project, options.skip), ) diff --git a/src/pdm/cli/commands/remove.py b/src/pdm/cli/commands/remove.py index 698ccf1669..8ab3e71bb5 100644 --- a/src/pdm/cli/commands/remove.py +++ b/src/pdm/cli/commands/remove.py @@ -2,6 +2,7 @@ from pdm.cli import actions 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.project import Project @@ -33,8 +34,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: def handle(self, project: Project, options: argparse.Namespace) -> None: actions.do_remove( project, - dev=options.dev, - group=options.group, + selection=GroupSelection.from_options(project, options), sync=options.sync, packages=options.packages, no_editable=options.no_editable, diff --git a/src/pdm/cli/commands/sync.py b/src/pdm/cli/commands/sync.py index 5912cfb2eb..a97246bd22 100644 --- a/src/pdm/cli/commands/sync.py +++ b/src/pdm/cli/commands/sync.py @@ -2,6 +2,7 @@ from pdm.cli import actions from pdm.cli.commands.base import BaseCommand +from pdm.cli.filters import GroupSelection from pdm.cli.hooks import HookManager from pdm.cli.options import ( clean_group, @@ -39,9 +40,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None: actions.check_lockfile(project) actions.do_sync( project, - groups=options.groups, - dev=options.dev, - default=options.default, + selection=GroupSelection.from_options(project, options), dry_run=options.dry_run, clean=options.clean, no_editable=options.no_editable, diff --git a/src/pdm/cli/commands/update.py b/src/pdm/cli/commands/update.py index 23c62db7b0..8c2e06da6a 100644 --- a/src/pdm/cli/commands/update.py +++ b/src/pdm/cli/commands/update.py @@ -2,6 +2,7 @@ from pdm.cli import actions from pdm.cli.commands.base import BaseCommand +from pdm.cli.filters import GroupSelection from pdm.cli.hooks import HookManager from pdm.cli.options import ( groups_group, @@ -58,9 +59,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: def handle(self, project: Project, options: argparse.Namespace) -> None: actions.do_update( project, - dev=options.dev, - groups=options.groups, - default=options.default, + selection=GroupSelection.from_options(project, options), save=options.save_strategy or project.config["strategy.save"], strategy=options.update_strategy or project.config["strategy.update"], unconstrained=options.unconstrained, diff --git a/src/pdm/cli/filters.py b/src/pdm/cli/filters.py new file mode 100644 index 0000000000..4af4a9c901 --- /dev/null +++ b/src/pdm/cli/filters.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import argparse +from typing import TYPE_CHECKING + +from pdm.compat import cached_property +from pdm.exceptions import PdmUsageError + +if TYPE_CHECKING: + from typing import Iterator, Sequence + + from pdm.project import Project + + +class GroupSelection: + def __init__( + self, + project: Project, + *, + default: bool = True, + dev: bool | None = None, + groups: Sequence[str] = (), + group: str | None = None, + ): + self.project = project + self.groups = groups + self.group = group + self.default = default + self.dev = dev + + @classmethod + def from_options(cls, project: Project, options: argparse.Namespace) -> GroupSelection: + if "group" in options: + return cls(project, group=options.group, dev=options.dev) + return cls( + project, + default=options.default, + dev=options.dev, + groups=options.groups, + ) + + def one(self) -> str: + if self.group: + return self.group + if len(self.groups) == 1: + return self.groups[0] + return "dev" if self.dev else "default" + + @property + def is_unset(self) -> bool: + return self.default and self.dev is None and not self.groups + + def all(self) -> list[str] | None: + if self.is_unset: + if self.project.lockfile.exists: + return self.project.lockfile.groups + return list(self) + + @cached_property + def _translated_groups(self) -> set[str]: + """Translate default, dev and groups containing ":all" into a list of groups""" + if self.is_unset: + # Default case, return what is in the lock file + locked_groups = self.project.lockfile.groups + if locked_groups: + return set(locked_groups) + default, dev, groups = self.default, self.dev, self.groups + if dev is None: # --prod is not set, include dev-dependencies + dev = True + project = self.project + optional_groups = set(project.pyproject.metadata.get("optional-dependencies", {})) + dev_groups = set(project.pyproject.settings.get("dev-dependencies", {})) + groups_set = set(groups) + if groups_set & dev_groups: + if not dev: + raise PdmUsageError("--prod is not allowed with dev groups and should be left") + elif dev: + groups_set.update(dev_groups) + if ":all" in groups: + groups_set.discard(":all") + groups_set.update(optional_groups) + if default: + groups_set.add("default") + # Sorts the result in ascending order instead of in random order + # to make this function pure + invalid_groups = groups_set - set(project.iter_groups()) + if invalid_groups: + project.core.ui.echo( + "[d]Ignoring non-existing groups: [success]" f"{', '.join(invalid_groups)}[/]", + err=True, + ) + groups_set -= invalid_groups + extra_groups = project.lockfile.compare_groups(groups_set) + if extra_groups: + raise PdmUsageError(f"Requested groups not in lockfile: {','.join(extra_groups)}") + return groups_set + + def __iter__(self) -> Iterator[str]: + return iter(self._translated_groups) + + def __contains__(self, group: str) -> bool: + return group in self._translated_groups diff --git a/src/pdm/cli/options.py b/src/pdm/cli/options.py index 51de813587..b7a55ba4a2 100644 --- a/src/pdm/cli/options.py +++ b/src/pdm/cli/options.py @@ -207,7 +207,7 @@ def no_isolation_callback( dev_group.add_argument( "-d", "--dev", - default=True, + default=None, dest="dev", action="store_true", help="Select dev dependencies", diff --git a/src/pdm/cli/utils.py b/src/pdm/cli/utils.py index 593155691d..11c2a4b4ac 100644 --- a/src/pdm/cli/utils.py +++ b/src/pdm/cli/utils.py @@ -25,7 +25,7 @@ from rich.tree import Tree from pdm import termui -from pdm.exceptions import PdmArgumentError, PdmUsageError, ProjectError +from pdm.exceptions import PdmArgumentError, ProjectError from pdm.formats import FORMATS from pdm.formats.base import make_array, make_inline_table from pdm.models.requirements import ( @@ -597,35 +597,6 @@ def format_resolution_impossible(err: ResolutionImpossible) -> str: return "\n".join(result) -def translate_groups(project: Project, default: bool, dev: bool, groups: Iterable[str]) -> list[str]: - """Translate default, dev and groups containing ":all" into a list of groups""" - optional_groups = set(project.pyproject.metadata.get("optional-dependencies", {})) - dev_groups = set(project.pyproject.settings.get("dev-dependencies", {})) - groups_set = set(groups) - if dev is None: - dev = True - if groups_set & dev_groups: - if not dev: - raise PdmUsageError("--prod is not allowed with dev groups and should be left") - elif dev: - groups_set.update(dev_groups) - if ":all" in groups: - groups_set.discard(":all") - groups_set.update(optional_groups) - if default: - groups_set.add("default") - # Sorts the result in ascending order instead of in random order - # to make this function pure - invalid_groups = groups_set - set(project.iter_groups()) - if invalid_groups: - project.core.ui.echo( - f"[d]Ignoring non-existing groups: [success]{', '.join(invalid_groups)}[/]", - err=True, - ) - groups_set -= invalid_groups - return sorted(groups_set) - - def merge_dictionary(target: MutableMapping[Any, Any], input: Mapping[Any, Any]) -> None: """Merge the input dict with the target while preserving the existing values properly. This will update the target dictionary in place. diff --git a/src/pdm/project/lockfile.py b/src/pdm/project/lockfile.py index 805543867e..2149681c69 100644 --- a/src/pdm/project/lockfile.py +++ b/src/pdm/project/lockfile.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Mapping +from typing import Any, Iterable, Mapping import tomlkit @@ -27,6 +27,11 @@ def file_version(self) -> str: def groups(self) -> list[str] | None: return self._data.get("metadata", {}).get("groups") + def compare_groups(self, groups: Iterable[str]) -> list[str]: + if not self.groups: + return [] + return list(set(groups).difference(self.groups)) + def set_data(self, data: Mapping[str, Any]) -> None: self._data = tomlkit.document() for line in GENERATED_COMMENTS: diff --git a/src/pdm/project/project_file.py b/src/pdm/project/project_file.py index 4a11abf5c8..c84d326b5b 100644 --- a/src/pdm/project/project_file.py +++ b/src/pdm/project/project_file.py @@ -36,7 +36,7 @@ def write(self, show_message: bool = True) -> None: @property def is_valid(self) -> bool: - return "project" in self._data + return bool(self._data.get("project")) @property def metadata(self) -> items.Table: diff --git a/src/pdm/pytest.py b/src/pdm/pytest.py index f0496283a7..49f5c1ad39 100644 --- a/src/pdm/pytest.py +++ b/src/pdm/pytest.py @@ -617,6 +617,8 @@ def caller( result = RunResult(exit_code, stdout.getvalue(), stderr.getvalue(), exception) if strict and result.exit_code != 0: + if result.exception: + raise result.exception.with_traceback(result.exception.__traceback__) raise RuntimeError(f"Call command {args} failed({result.exit_code}): {result.stderr}") return result diff --git a/tests/cli/test_add.py b/tests/cli/test_add.py index a92b3ce938..0da6456e39 100644 --- a/tests/cli/test_add.py +++ b/tests/cli/test_add.py @@ -4,35 +4,35 @@ from unearth import Link from pdm.cli import actions -from pdm.exceptions import PdmUsageError +from pdm.models.requirements import parse_requirement from pdm.models.specifiers import PySpecSet from pdm.utils import path_to_url from tests import FIXTURES -@pytest.mark.usefixtures("repository") -def test_add_package(project, working_set, is_dev): - actions.do_add(project, dev=is_dev, packages=["requests"]) +def test_add_package(project, working_set, dev_option, pdm): + pdm(["add", *dev_option, "requests"], obj=project, strict=True) group = ( - project.pyproject.settings["dev-dependencies"]["dev"] if is_dev else project.pyproject.metadata["dependencies"] + project.pyproject.settings["dev-dependencies"]["dev"] + if dev_option + else project.pyproject.metadata["dependencies"] ) - assert group[0] == "requests~=2.19" + assert group[0] == "requests>=2.19.1" locked_candidates = project.locked_repository.all_candidates assert locked_candidates["idna"].version == "2.7" for package in ("requests", "idna", "chardet", "urllib3", "certifi"): assert package in working_set -def test_add_command(project, invoke, mocker): +def test_add_command(project, pdm, mocker): do_add = mocker.patch.object(actions, "do_add") - invoke(["add", "requests"], obj=project) + pdm(["add", "requests"], obj=project) do_add.assert_called_once() -@pytest.mark.usefixtures("repository") -def test_add_package_to_custom_group(project, working_set): - actions.do_add(project, group="test", packages=["requests"]) +def test_add_package_to_custom_group(project, working_set, pdm): + pdm(["add", "requests", "--group", "test"], obj=project, strict=True) assert "requests" in project.pyproject.metadata["optional-dependencies"]["test"][0] locked_candidates = project.locked_repository.all_candidates @@ -41,9 +41,8 @@ def test_add_package_to_custom_group(project, working_set): assert package in working_set -@pytest.mark.usefixtures("repository") -def test_add_package_to_custom_dev_group(project, working_set): - actions.do_add(project, dev=True, group="test", packages=["requests"]) +def test_add_package_to_custom_dev_group(project, working_set, pdm): + pdm(["add", "requests", "--group", "test", "--dev"], obj=project, strict=True) dependencies = project.pyproject.settings["dev-dependencies"]["test"] assert "requests" in dependencies[0] @@ -53,16 +52,13 @@ def test_add_package_to_custom_dev_group(project, working_set): assert package in working_set -@pytest.mark.usefixtures("repository", "vcs") -def test_add_editable_package(project, working_set): +@pytest.mark.usefixtures("vcs") +def test_add_editable_package(project, working_set, pdm): # Ensure that correct python version is used. project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add(project, dev=True, packages=["demo"]) - actions.do_add( - project, - dev=True, - editables=["git+https://github.com/test-root/demo.git#egg=demo"], - ) + pdm(["add", "--dev", "demo"], obj=project, strict=True) + pdm(["add", "-de", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project, strict=True) + group = project.pyproject.settings["dev-dependencies"]["dev"] assert group == ["-e git+https://github.com/test-root/demo.git#egg=demo"] locked_candidates = project.locked_repository.all_candidates @@ -71,82 +67,67 @@ def test_add_editable_package(project, working_set): assert locked_candidates["idna"].version == "2.7" assert "idna" in working_set - actions.do_sync(project, no_editable=True) + pdm(["sync", "--no-editable"], obj=project, strict=True) assert not working_set["demo"].link_file -@pytest.mark.usefixtures("repository", "vcs", "working_set") -def test_add_editable_package_to_metadata_forbidden(project): +@pytest.mark.usefixtures("vcs", "working_set") +def test_add_editable_package_to_metadata_forbidden(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") - with pytest.raises(PdmUsageError): - actions.do_add(project, editables=["git+https://github.com/test-root/demo.git#egg=demo"]) - with pytest.raises(PdmUsageError): - actions.do_add( - project, - group="foo", - editables=["git+https://github.com/test-root/demo.git#egg=demo"], - ) - - -@pytest.mark.usefixtures("repository", "vcs") -def test_non_editable_override_editable(project, working_set): + result = pdm(["add", "-v", "-e", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project) + assert "PdmUsageError" in result.stderr + result = pdm(["add", "-v", "-Gtest", "-e", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project) + assert "PdmUsageError" in result.stderr + + +@pytest.mark.usefixtures("working_set", "vcs") +def test_non_editable_override_editable(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add( - project, - dev=True, - editables=[ - "git+https://github.com/test-root/demo.git#egg=demo", - ], - ) - actions.do_add( - project, - dev=True, - packages=["git+https://github.com/test-root/demo.git#egg=demo"], - ) + url = "git+https://github.com/test-root/demo.git#egg=demo" + pdm(["add", "--dev", "-e", url], obj=project, strict=True) + pdm(["add", "--dev", url], obj=project, strict=True) assert not project.dev_dependencies["demo"].editable -@pytest.mark.usefixtures("repository", "working_set") -def test_add_remote_package_url(project, is_dev): +@pytest.mark.usefixtures("working_set") +def test_add_remote_package_url(project, dev_option, pdm): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add( - project, - dev=is_dev, - packages=["http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"], - ) + url = "http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl" + pdm(["add", *dev_option, url], obj=project, strict=True) group = ( - project.pyproject.settings["dev-dependencies"]["dev"] if is_dev else project.pyproject.metadata["dependencies"] + project.pyproject.settings["dev-dependencies"]["dev"] + if dev_option + else project.pyproject.metadata["dependencies"] ) - assert group[0] == "demo @ http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl" + assert group[0] == f"demo @ {url}" -@pytest.mark.usefixtures("repository") -def test_add_no_install(project, working_set): - actions.do_add(project, sync=False, packages=["requests"]) +def test_add_no_install(project, working_set, pdm): + pdm(["add", "--no-sync", "requests"], obj=project, strict=True) for package in ("requests", "idna", "chardet", "urllib3", "certifi"): assert package not in working_set @pytest.mark.usefixtures("repository") -def test_add_package_save_exact(project): - actions.do_add(project, sync=False, save="exact", packages=["requests"]) +def test_add_package_save_exact(project, pdm): + pdm(["add", "--save-exact", "--no-sync", "requests"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "requests==2.19.1" @pytest.mark.usefixtures("repository") -def test_add_package_save_wildcard(project): - actions.do_add(project, sync=False, save="wildcard", packages=["requests"]) +def test_add_package_save_wildcard(project, pdm): + pdm(["add", "--save-wildcard", "--no-sync", "requests"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "requests" @pytest.mark.usefixtures("repository") -def test_add_package_save_minimum(project): - actions.do_add(project, sync=False, save="minimum", packages=["requests"]) +def test_add_package_save_minimum(project, pdm): + pdm(["add", "--save-minimum", "--no-sync", "requests"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "requests>=2.19.1" -def test_add_package_update_reuse(project, repository): - actions.do_add(project, sync=False, save="wildcard", packages=["requests", "pytz"]) +def test_add_package_update_reuse(project, repository, pdm): + pdm(["add", "--no-sync", "--save-wildcard", "requests", "pytz"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.19.1" @@ -166,15 +147,15 @@ def test_add_package_update_reuse(project, repository): "urllib3<1.24,>=1.21.1", ], ) - actions.do_add(project, sync=False, save="wildcard", packages=["requests"], strategy="reuse") + pdm(["add", "--no-sync", "--save-wildcard", "requests"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.20.0" assert locked_candidates["chardet"].version == "3.0.4" assert locked_candidates["pytz"].version == "2019.3" -def test_add_package_update_eager(project, repository): - actions.do_add(project, sync=False, save="wildcard", packages=["requests", "pytz"]) +def test_add_package_update_eager(project, repository, pdm): + pdm(["add", "--no-sync", "--save-wildcard", "requests", "pytz"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.19.1" @@ -194,56 +175,53 @@ def test_add_package_update_eager(project, repository): "urllib3<1.24,>=1.21.1", ], ) - actions.do_add(project, sync=False, save="wildcard", packages=["requests"], strategy="eager") + pdm(["add", "--no-sync", "--save-wildcard", "--update-eager", "requests"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.20.0" assert locked_candidates["chardet"].version == "3.0.5" assert locked_candidates["pytz"].version == "2019.3" -@pytest.mark.usefixtures("repository") -def test_add_package_with_mismatch_marker(project, working_set, mocker): +def test_add_package_with_mismatch_marker(project, working_set, mocker, pdm): mocker.patch( "pdm.models.environment.get_pep508_environment", return_value={"platform_system": "Darwin"}, ) - actions.do_add(project, packages=["requests", "pytz; platform_system!='Darwin'"]) + pdm(["add", "requests", "pytz; platform_system!='Darwin'"], obj=project, strict=True) assert "pytz" not in working_set -@pytest.mark.usefixtures("repository") -def test_add_dependency_from_multiple_parents(project, working_set, mocker): +def test_add_dependency_from_multiple_parents(project, working_set, mocker, pdm): mocker.patch( "pdm.models.environment.get_pep508_environment", return_value={"platform_system": "Darwin"}, ) - actions.do_add(project, packages=["requests", "chardet; platform_system!='Darwin'"]) + pdm(["add", "requests", "chardet; platform_system!='Darwin'"], obj=project, strict=True) assert "chardet" in working_set -@pytest.mark.usefixtures("repository") -def test_add_packages_without_self(project, working_set): +def test_add_packages_without_self(project, working_set, pdm): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add(project, packages=["requests"], no_self=True) + pdm(["add", "--no-self", "requests"], obj=project, strict=True) assert project.name not in working_set -@pytest.mark.usefixtures("repository", "working_set") -def test_add_package_unconstrained_rewrite_specifier(project): +@pytest.mark.usefixtures("working_set") +def test_add_package_unconstrained_rewrite_specifier(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add(project, packages=["django"], no_self=True) + pdm(["add", "--no-self", "django"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["django"].version == "2.2.9" - assert project.pyproject.metadata["dependencies"][0] == "django~=2.2" + assert project.pyproject.metadata["dependencies"][0] == "django>=2.2.9" - actions.do_add(project, packages=["django-toolbar"], no_self=True, unconstrained=True) + pdm(["add", "--no-self", "--unconstrained", "django-toolbar"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["django"].version == "1.11.8" - assert project.pyproject.metadata["dependencies"][0] == "django~=1.11" + assert project.pyproject.metadata["dependencies"][0] == "django>=1.11.8" -@pytest.mark.usefixtures("repository", "working_set", "vcs") -def test_add_cached_vcs_requirement(project, mocker): +@pytest.mark.usefixtures("working_set", "vcs") +def test_add_cached_vcs_requirement(project, mocker, pdm): project.environment.python_requires = PySpecSet(">=3.6") url = "git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo" built_path = FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl" @@ -254,7 +232,7 @@ def test_add_cached_vcs_requirement(project, mocker): shutil.copy2(built_path, cache_path) downloader = mocker.patch("unearth.finder.unpack_link") builder = mocker.patch("pdm.builders.WheelBuilder.build") - actions.do_add(project, packages=[url], no_self=True) + pdm(["add", "--no-self", url], obj=project, strict=True) lockfile_entry = next(p for p in project.lockfile["package"] if p["name"] == "demo") assert lockfile_entry["revision"] == "1234567890abcdef" downloader.assert_not_called() @@ -262,40 +240,51 @@ def test_add_cached_vcs_requirement(project, mocker): @pytest.mark.usefixtures("repository") -def test_add_with_dry_run(project, capsys): - actions.do_add(project, dry_run=True, packages=["requests"]) - out, _ = capsys.readouterr() +def test_add_with_dry_run(project, pdm): + result = pdm(["add", "--dry-run", "requests"], obj=project, strict=True) assert not project.get_dependencies() - assert "requests 2.19.1" in out - assert "urllib3 1.22" in out + assert "requests 2.19.1" in result.stdout + assert "urllib3 1.22" in result.stdout -@pytest.mark.usefixtures("repository") -def test_add_with_prerelease(project, working_set): - actions.do_add(project, packages=["urllib3"], prerelease=True) +def test_add_with_prerelease(project, working_set, pdm): + pdm(["add", "--prerelease", "--save-compatible", "urllib3"], obj=project, strict=True) assert working_set["urllib3"].version == "1.23b0" assert project.pyproject.metadata["dependencies"][0] == "urllib3<2,>=1.23b0" -@pytest.mark.usefixtures("repository") -def test_add_editable_package_with_extras(project, working_set): +def test_add_editable_package_with_extras(project, working_set, pdm): project.environment.python_requires = PySpecSet(">=3.6") dep_path = FIXTURES.joinpath("projects/demo").as_posix() - actions.do_add( - project, - dev=True, - group="dev", - editables=[f"{dep_path}[security]"], - ) + pdm(["add", "-dGdev", "-e", f"{dep_path}[security]"], obj=project, strict=True) assert f"-e {path_to_url(dep_path)}#egg=demo[security]" in project.get_pyproject_dependencies("dev", True)[0] assert "demo" in working_set assert "requests" in working_set assert "urllib3" in working_set -def test_add_package_with_local_version(project, repository, working_set): +def test_add_package_with_local_version(project, repository, working_set, pdm): repository.add_candidate("foo", "1.0-alpha.0+local") - actions.do_add(project, packages=["foo"], save="minimum") + pdm(["add", "foo"], obj=project, strict=True) assert working_set["foo"].version == "1.0-alpha.0+local" dependencies, _ = project.get_pyproject_dependencies("default") assert dependencies[0] == "foo>=1.0a0" + + +def test_add_group_to_lockfile(project, working_set, pdm): + pdm(["add", "requests"], obj=project, strict=True) + assert project.lockfile.groups == ["default"] + pdm(["add", "--group", "tz", "pytz"], obj=project, strict=True) + assert project.lockfile.groups == ["default", "tz"] + assert "pytz" in working_set + + +def test_add_group_to_lockfile_without_package(project, working_set, pdm): + project.add_dependencies({"requests": parse_requirement("requests")}) + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") + pdm(["install"], obj=project, strict=True) + assert "pytz" not in working_set + assert project.lockfile.groups == ["default"] + pdm(["add", "--group", "tz"], obj=project, strict=True) + assert project.lockfile.groups == ["default", "tz"] + assert "pytz" in working_set diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index c508ae3419..eb9c27733d 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -6,73 +6,66 @@ from pdm.utils import cd -@pytest.mark.usefixtures("repository") -def test_sync_packages_with_group_all(project, working_set): +def test_sync_packages_with_group_all(project, working_set, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) project.add_dependencies({"pytz": parse_requirement("pytz")}, "date") project.add_dependencies({"pyopenssl": parse_requirement("pyopenssl")}, "ssl") - actions.do_lock(project) - actions.do_sync(project, groups=[":all"]) + pdm(["install", "-G:all"], obj=project, strict=True) assert "pytz" in working_set assert "requests" in working_set assert "idna" in working_set assert "pyopenssl" in working_set -@pytest.mark.usefixtures("repository") -def test_sync_packages_with_all_dev(project, working_set): +def test_sync_packages_with_all_dev(project, working_set, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) project.add_dependencies({"pytz": parse_requirement("pytz")}, "date", True) project.add_dependencies({"pyopenssl": parse_requirement("pyopenssl")}, "ssl", True) - actions.do_lock(project) - actions.do_sync(project, dev=True, default=False) + pdm(["install", "-d", "--no-default"], obj=project, strict=True) assert "requests" not in working_set assert "idna" not in working_set assert "pytz" in working_set assert "pyopenssl" in working_set -def test_sync_no_lockfile(project, invoke): +def test_sync_no_lockfile(project, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) - result = invoke(["sync"], obj=project) + result = pdm(["sync"], obj=project) assert result.exit_code == 1 -@pytest.mark.usefixtures("repository") -def test_sync_clean_packages(project, working_set): +def test_sync_clean_packages(project, working_set, pdm): for candidate in [ Distribution("foo", "0.1.0"), Distribution("chardet", "3.0.1"), Distribution("idna", "2.7"), ]: working_set.add_distribution(candidate) - actions.do_add(project, packages=["requests"], sync=False) - actions.do_sync(project, clean=True) + pdm(["add", "--no-sync", "requests"], obj=project, strict=True) + pdm(["sync", "--clean"], obj=project, strict=True) assert "foo" not in working_set -@pytest.mark.usefixtures("repository") -def test_sync_dry_run(project, working_set): +def test_sync_dry_run(project, working_set, pdm): for candidate in [ Distribution("foo", "0.1.0"), Distribution("chardet", "3.0.1"), Distribution("idna", "2.7"), ]: working_set.add_distribution(candidate) - actions.do_add(project, packages=["requests"], sync=False) - actions.do_sync(project, clean=True, dry_run=True) + pdm(["add", "--no-sync", "requests"], obj=project, strict=True) + pdm(["sync", "--clean", "--dry-run"], obj=project, strict=True) assert "foo" in working_set assert "requests" not in working_set assert working_set["chardet"].version == "3.0.1" -@pytest.mark.usefixtures("repository") -def test_sync_only_different(project, working_set, capsys): +def test_sync_only_different(project, working_set, pdm): working_set.add_distribution(Distribution("foo", "0.1.0")) working_set.add_distribution(Distribution("chardet", "3.0.1")) working_set.add_distribution(Distribution("idna", "2.7")) - actions.do_add(project, packages=["requests"]) - out, _ = capsys.readouterr() + result = pdm(["add", "requests"], obj=project, strict=True) + out = result.stdout assert "3 to add" in out, out assert "1 to update" in out assert "foo" in working_set @@ -80,46 +73,39 @@ def test_sync_only_different(project, working_set, capsys): assert working_set["chardet"].version == "3.0.4" -@pytest.mark.usefixtures("repository") -def test_sync_in_sequential_mode(project, working_set, capsys): +def test_sync_in_sequential_mode(project, working_set, pdm): project.project_config["install.parallel"] = False - actions.do_add(project, packages=["requests"]) - out, _ = capsys.readouterr() - assert "5 to add" in out + result = pdm(["add", "requests"], obj=project, strict=True) + assert "5 to add" in result.stdout assert "test-project" in working_set assert working_set["chardet"].version == "3.0.4" -@pytest.mark.usefixtures("repository") -def test_sync_packages_with_groups(project, working_set): +def test_sync_packages_with_groups(project, working_set, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) project.add_dependencies({"pytz": parse_requirement("pytz")}, "date") - actions.do_lock(project) - actions.do_sync(project, groups=["date"]) + pdm(["install", "-Gdate"], obj=project, strict=True) assert "pytz" in working_set assert "requests" in working_set assert "idna" in working_set -@pytest.mark.usefixtures("repository") -def test_sync_production_packages(project, working_set, is_dev): +@pytest.mark.parametrize("prod_option", [("--prod",), ()]) +def test_sync_production_packages(project, working_set, prod_option, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) project.add_dependencies({"pytz": parse_requirement("pytz")}, "dev", dev=True) - actions.do_lock(project) - actions.do_sync(project, dev=is_dev) + pdm(["install", *prod_option], obj=project, strict=True) assert "requests" in working_set - assert ("pytz" in working_set) == is_dev + assert ("pytz" in working_set) == (not prod_option) -@pytest.mark.usefixtures("repository") -def test_sync_without_self(project, working_set): +def test_sync_without_self(project, working_set, pdm): project.add_dependencies({"requests": parse_requirement("requests")}) - actions.do_lock(project) - actions.do_sync(project, no_self=True) + pdm(["install", "--no-self"], obj=project, strict=True) assert project.name not in working_set, list(working_set) -def test_sync_with_index_change(project, index): +def test_sync_with_index_change(project, index, pdm): project.project_config["pypi.url"] = "https://my.pypi.org/simple" project.pyproject.metadata["requires-python"] = ">=3.6" project.pyproject.metadata["dependencies"] = ["future-fstrings"] @@ -137,99 +123,99 @@ def test_sync_with_index_change(project, index): """ - actions.do_lock(project) + pdm(["lock"], obj=project, strict=True) file_hashes = project.lockfile["metadata"]["files"]["future-fstrings 1.2.0"] assert [e["hash"] for e in file_hashes] == [ "sha256:90e49598b553d8746c4dc7d9442e0359d038c3039d802c91c0a55505da318c63" ] # Mimic the CDN inconsistences of PyPI simple index. See issues/596. del index["/simple/future-fstrings/"] - actions.do_sync(project, no_self=True) + pdm(["sync", "--no-self"], obj=project, strict=True) -def test_install_command(project, invoke, mocker): +def test_install_command(project, pdm, mocker): do_lock = mocker.patch.object(actions, "do_lock") do_sync = mocker.patch.object(actions, "do_sync") - invoke(["install"], obj=project) + pdm(["install"], obj=project) do_lock.assert_called_once() do_sync.assert_called_once() -def test_sync_command(project, invoke, mocker): - invoke(["lock"], obj=project) +def test_sync_command(project, pdm, mocker): + pdm(["lock"], obj=project) do_sync = mocker.patch.object(actions, "do_sync") - invoke(["sync"], obj=project) + pdm(["sync"], obj=project) do_sync.assert_called_once() -def test_install_with_lockfile(project, invoke, working_set, repository): - result = invoke(["lock", "-v"], obj=project) +@pytest.mark.usefixtures("working_set") +def test_install_with_lockfile(project, pdm): + result = pdm(["lock", "-v"], obj=project) assert result.exit_code == 0 - result = invoke(["install"], obj=project) + result = pdm(["install"], obj=project) assert "Lock file" not in result.stderr project.add_dependencies({"pytz": parse_requirement("pytz")}, "default") - result = invoke(["install"], obj=project) + result = pdm(["install"], obj=project) assert "Lock file hash doesn't match" in result.stderr assert "pytz" in project.locked_repository.all_candidates assert project.is_lockfile_hash_match() -def test_install_with_dry_run(project, invoke, repository): +def test_install_with_dry_run(project, pdm, repository): project.add_dependencies({"pytz": parse_requirement("pytz")}, "default") - result = invoke(["install", "--dry-run"], obj=project) + result = pdm(["install", "--dry-run"], obj=project) project.lockfile.reload() assert "pytz" not in project.locked_repository.all_candidates assert "pytz 2019.3" in result.output -def test_install_check(invoke, project, repository): - result = invoke(["install", "--check"], obj=project) +def test_install_check(pdm, project, repository): + result = pdm(["install", "--check"], obj=project) assert result.exit_code == 1 - result = invoke(["add", "requests", "--no-sync"], obj=project) + result = pdm(["add", "requests", "--no-sync"], obj=project) project.add_dependencies({"requests": parse_requirement("requests>=2.0")}) - result = invoke(["install", "--check"], obj=project) + result = pdm(["install", "--check"], obj=project) assert result.exit_code == 1 -@pytest.mark.usefixtures("repository") -def test_sync_with_pure_option(project, working_set, invoke): +def test_sync_with_only_keep_option(project, working_set, pdm): project.add_dependencies({"requests": parse_requirement("requests>=2.0")}) project.add_dependencies({"django": parse_requirement("django")}, "web", True) - invoke(["install"], obj=project, strict=True) + pdm(["install"], obj=project, strict=True) assert all(p in working_set for p in ("requests", "urllib3", "django", "pytz")), list(working_set) - actions.do_sync(project, dev=False, only_keep=True) + pdm(["sync", "--prod", "--only-keep"], obj=project, strict=True) assert "requests" in working_set assert "urllib3" in working_set assert "django" not in working_set -def test_install_referencing_self_package(project, working_set, invoke): +def test_install_referencing_self_package(project, working_set, pdm): project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") project.add_dependencies({"urllib3": parse_requirement("urllib3")}, to_group="web") project.add_dependencies({"test-project": parse_requirement("test-project[tz,web]")}, to_group="all") - invoke(["install", "-Gall"], obj=project, strict=True) + pdm(["install", "-Gall"], obj=project, strict=True) assert "pytz" in working_set assert "urllib3" in working_set -def test_install_monorepo_with_rel_paths(fixture_project, invoke, working_set): +def test_install_monorepo_with_rel_paths(fixture_project, pdm, working_set): project = fixture_project("test-monorepo") with cd(project.root): - invoke(["install"], obj=project, strict=True) + pdm(["install"], obj=project, strict=True) for package in ("package-a", "package-b", "core"): assert package in working_set @pytest.mark.usefixtures("repository") -def test_install_retry(project, invoke, mocker): - invoke(["add", "certifi", "chardet", "--no-sync"], obj=project) +def test_install_retry(project, pdm, mocker): + pdm(["add", "certifi", "chardet", "--no-sync"], obj=project) handler = mocker.patch( "pdm.installers.synchronizers.Synchronizer.install_candidate", side_effect=RuntimeError, ) - result = invoke(["install"], obj=project) + result = pdm(["install"], obj=project) assert result.exit_code == 1 handler.assert_has_calls( [ @@ -243,16 +229,16 @@ def test_install_retry(project, invoke, mocker): @pytest.mark.usefixtures("repository") -def test_install_fail_fast(project, invoke, mocker): +def test_install_fail_fast(project, pdm, mocker): project.project_config["install.parallel"] = True - invoke(["add", "certifi", "chardet", "pytz", "--no-sync"], obj=project) + pdm(["add", "certifi", "chardet", "pytz", "--no-sync"], obj=project) handler = mocker.patch( "pdm.installers.synchronizers.Synchronizer.install_candidate", side_effect=RuntimeError, ) mocker.patch("multiprocessing.cpu_count", return_value=1) - result = invoke(["install", "--fail-fast"], obj=project) + result = pdm(["install", "--fail-fast"], obj=project) assert result.exit_code == 1 handler.assert_has_calls( [ @@ -261,3 +247,36 @@ def test_install_fail_fast(project, invoke, mocker): ], any_order=True, ) + + +@pytest.mark.usefixtures("working_set") +def test_install_groups_not_in_lockfile(project, pdm): + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") + project.add_dependencies({"urllib3": parse_requirement("urllib3")}, to_group="web") + pdm(["install", "-vv"], obj=project, strict=True) + assert project.lockfile.groups == ["default"] + all_locked_packages = project.locked_repository.all_candidates + for package in ["pytz", "urllib3"]: + assert package not in all_locked_packages + with pytest.raises(RuntimeError, match="Requested groups not in lockfile"): + pdm(["install", "-Gtz"], obj=project, strict=True) + + +def test_install_locked_groups(project, pdm, working_set): + project.add_dependencies({"urllib3": parse_requirement("urllib3")}) + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") + pdm(["lock", "-Gtz", "--no-default"], obj=project, strict=True) + pdm(["sync"], obj=project, strict=True) + assert "pytz" in working_set + assert "urllib3" not in working_set + + +def test_install_groups_and_lock(project, pdm, working_set): + project.add_dependencies({"urllib3": parse_requirement("urllib3")}) + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") + pdm(["install", "-Gtz", "--no-default"], obj=project, strict=True) + assert "pytz" in working_set + assert "urllib3" not in working_set + assert project.lockfile.groups == ["tz"] + assert "pytz" in project.locked_repository.all_candidates + assert "urllib3" not in project.locked_repository.all_candidates diff --git a/tests/cli/test_list.py b/tests/cli/test_list.py index 4e04840c37..0250f0cbdf 100644 --- a/tests/cli/test_list.py +++ b/tests/cli/test_list.py @@ -6,53 +6,47 @@ import pytest from rich.box import ASCII -from pdm.cli import actions from pdm.cli.commands.list import Command from pdm.models.specifiers import PySpecSet from pdm.pytest import Distribution from tests import FIXTURES -def test_list_command(project, invoke, mocker): +def test_list_command(project, pdm, mocker): # Calls the correct handler within the Command m = mocker.patch.object(Command, "handle_list") - invoke(["list"], obj=project) + pdm(["list"], obj=project) m.assert_called_once() @pytest.mark.usefixtures("working_set") -def test_list_graph_command(project, invoke, mocker): +def test_list_graph_command(project, pdm, mocker): # Calls the correct handler within the list command m = mocker.patch.object(Command, "handle_graph") - invoke(["list", "--graph"], obj=project) + pdm(["list", "--graph"], obj=project) m.assert_called_once() @mock.patch("rich.console.ConsoleOptions.ascii_only", lambda: True) @pytest.mark.usefixtures("working_set") -def test_list_dependency_graph(project, invoke): +def test_list_dependency_graph(project, pdm): # Shows a line that contains a sub requirement (any order). - actions.do_add(project, packages=["requests"]) - result = invoke(["list", "--graph"], obj=project) + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list", "--graph"], obj=project) expected = "-- urllib3 1.22 [ required: <1.24,>=1.21.1 ]" in result.outputs assert expected @mock.patch("rich.console.ConsoleOptions.ascii_only", lambda: True) @pytest.mark.usefixtures("working_set") -def test_list_dependency_graph_include_exclude(project, invoke): +def test_list_dependency_graph_include_exclude(project, pdm): # Just include dev packages in the graph project.environment.python_requires = PySpecSet(">=3.6") dep_path = FIXTURES.joinpath("projects/demo").as_posix() - actions.do_add( - project, - dev=True, - group="dev", - editables=[f"{dep_path}[security]"], - ) + pdm(["add", "-de", f"{dep_path}[security]"], obj=project, strict=True) # Output full graph - result = invoke(["list", "--graph"], obj=project) + result = pdm(["list", "--graph"], obj=project) expects = ( "demo 0.0.1 [ Not required ]\n", "+-- chardet 3.0.4 [ required: Any ]\n" if os.name == "nt" else "", @@ -67,88 +61,88 @@ def test_list_dependency_graph_include_exclude(project, invoke): assert expects == result.outputs # Now exclude the dev dep. - result = invoke(["list", "--graph", "--exclude", "dev"], obj=project) + result = pdm(["list", "--graph", "--exclude", "dev"], obj=project) expects = "" assert expects == result.outputs # Only include the dev dep - result = invoke(["list", "--graph", "--include", "dev", "--exclude", "*"], obj=project) + result = pdm(["list", "--graph", "--include", "dev", "--exclude", "*"], obj=project) expects = "demo[security] 0.0.1 [ required: Any ]\n" expects = "".join(expects) assert expects == result.outputs @pytest.mark.usefixtures("working_set") -def test_list_dependency_graph_with_circular_forward(project, invoke, repository): +def test_list_dependency_graph_with_circular_forward(project, pdm, repository): # shows a circular dependency repository.add_candidate("foo", "0.1.0") repository.add_candidate("foo-bar", "0.1.0") repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) repository.add_dependencies("foo-bar", "0.1.0", ["foo"]) - actions.do_add(project, packages=["foo"]) - result = invoke(["list", "--graph"], obj=project) + pdm(["add", "foo"], obj=project, strict=True) + result = pdm(["list", "--graph"], obj=project) circular_found = "foo [circular]" in result.outputs assert circular_found @mock.patch("rich.console.ConsoleOptions.ascii_only", lambda: True) @pytest.mark.usefixtures("working_set") -def test_list_dependency_graph_with_circular_reverse(project, invoke, repository): +def test_list_dependency_graph_with_circular_reverse(project, pdm, repository): repository.add_candidate("foo", "0.1.0") repository.add_candidate("foo-bar", "0.1.0") repository.add_candidate("baz", "0.1.0") repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) repository.add_dependencies("foo-bar", "0.1.0", ["foo", "baz"]) repository.add_dependencies("baz", "0.1.0", []) - actions.do_add(project, packages=["foo"]) + pdm(["add", "foo"], obj=project, strict=True) # --reverse flag shows packages reversed and with [circular] - result = invoke(["list", "--graph", "--reverse"], obj=project) + result = pdm(["list", "--graph", "--reverse"], obj=project) expected = ( "baz 0.1.0 \n" "`-- foo-bar 0.1.0 [ requires: Any ]\n" " `-- foo 0.1.0 [ requires: Any ]\n" " +-- foo-bar [circular] [ requires: Any ]\n" - " `-- test-project 0.0.0 [ requires: ~=0.1 ]\n" + " `-- test-project 0.0.0 [ requires: >=0.1.0 ]\n" ) assert expected in result.outputs # -r flag shows packages reversed and with [circular] - result = invoke(["list", "--graph", "-r"], obj=project) + result = pdm(["list", "--graph", "-r"], obj=project) assert expected in result.outputs -def test_list_reverse_without_graph_flag(project, invoke): +def test_list_reverse_without_graph_flag(project, pdm): # results in PDMUsageError since --reverse needs --graph - result = invoke(["list", "--reverse"], obj=project) + result = pdm(["list", "--reverse"], obj=project) assert "[PdmUsageError]" in result.stderr assert "--reverse cannot be used without --graph" in result.stderr - result = invoke(["list", "-r"], obj=project) + result = pdm(["list", "-r"], obj=project) assert "[PdmUsageError]" in result.stderr assert "--reverse cannot be used without --graph" in result.stderr @mock.patch("rich.console.ConsoleOptions.ascii_only", lambda: True) @pytest.mark.usefixtures("working_set") -def test_list_reverse_dependency_graph(project, invoke): +def test_list_reverse_dependency_graph(project, pdm): # requests visible on leaf node - actions.do_add(project, packages=["requests"]) - result = invoke(["list", "--graph", "--reverse"], obj=project) + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list", "--graph", "--reverse"], obj=project) assert "`-- requests 2.19.1 [ requires: <1.24,>=1.21.1 ]" in result.outputs @pytest.mark.usefixtures("working_set") -def test_list_json(project, invoke): +def test_list_json(project, pdm): # check json output matches graph output - actions.do_add(project, packages=["requests"], no_self=True) - result = invoke(["list", "--graph", "--json"], obj=project) + pdm(["add", "requests", "--no-self"], obj=project, strict=True) + result = pdm(["list", "--graph", "--json"], obj=project) expected = [ { "package": "requests", "version": "2.19.1", - "required": "~=2.19", + "required": ">=2.19.1", "dependencies": [ { "package": "certifi", @@ -181,10 +175,10 @@ def test_list_json(project, invoke): @pytest.mark.usefixtures("working_set") -def test_list_json_reverse(project, invoke): +def test_list_json_reverse(project, pdm): # check json output matches reversed graph - actions.do_add(project, packages=["requests"], no_self=True) - result = invoke(["list", "--graph", "--reverse", "--json"], obj=project) + pdm(["add", "requests", "--no-self"], obj=project, strict=True) + result = pdm(["list", "--graph", "--reverse", "--json"], obj=project) expected = [ { "package": "certifi", @@ -244,7 +238,7 @@ def test_list_json_reverse(project, invoke): @pytest.mark.usefixtures("working_set") -def test_list_json_with_circular_forward(project, invoke, repository): +def test_list_json_with_circular_forward(project, pdm, repository): # circulars are handled in json exports repository.add_candidate("foo", "0.1.0") repository.add_candidate("foo-bar", "0.1.0") @@ -252,13 +246,13 @@ def test_list_json_with_circular_forward(project, invoke, repository): repository.add_dependencies("baz", "0.1.0", ["foo"]) repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) repository.add_dependencies("foo-bar", "0.1.0", ["foo"]) - actions.do_add(project, packages=["baz"], no_self=True) - result = invoke(["list", "--graph", "--json"], obj=project) + pdm(["add", "baz", "--no-self"], obj=project, strict=True) + result = pdm(["list", "--graph", "--json"], obj=project) expected = [ { "package": "baz", "version": "0.1.0", - "required": "~=0.1", + "required": ">=0.1.0", "dependencies": [ { "package": "foo", @@ -287,7 +281,7 @@ def test_list_json_with_circular_forward(project, invoke, repository): @pytest.mark.usefixtures("working_set") -def test_list_json_with_circular_reverse(project, invoke, repository): +def test_list_json_with_circular_reverse(project, pdm, repository): # circulars are handled in reversed json exports repository.add_candidate("foo", "0.1.0") repository.add_candidate("foo-bar", "0.1.0") @@ -295,8 +289,8 @@ def test_list_json_with_circular_reverse(project, invoke, repository): repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) repository.add_dependencies("foo-bar", "0.1.0", ["foo", "baz"]) repository.add_dependencies("baz", "0.1.0", []) - actions.do_add(project, packages=["foo"], no_self=True) - result = invoke(["list", "--graph", "--reverse", "--json"], obj=project) + pdm(["add", "foo", "--no-self"], obj=project, strict=True) + result = pdm(["list", "--graph", "--reverse", "--json"], obj=project) expected = [ { "package": "baz", @@ -329,99 +323,99 @@ def test_list_json_with_circular_reverse(project, invoke, repository): assert expected == json.loads(result.outputs) -def test_list_field_unknown(project, invoke): +def test_list_field_unknown(project, pdm): # unknown list fields flagged to user - result = invoke(["list", "--fields", "notvalid"], obj=project) + result = pdm(["list", "--fields", "notvalid"], obj=project) assert "[PdmUsageError]" in result.stderr assert "--fields must specify one or more of:" in result.stderr -def test_list_sort_unknown(project, invoke): +def test_list_sort_unknown(project, pdm): # unknown sort fields flagged to user - result = invoke(["list", "--sort", "notvalid"], obj=project) + result = pdm(["list", "--sort", "notvalid"], obj=project) assert "[PdmUsageError]" in result.stderr assert "--sort key must be one of:" in result.stderr -def test_list_freeze_banned_options(project, invoke): +def test_list_freeze_banned_options(project, pdm): # other flags cannot be used with --freeze - result = invoke(["list", "--freeze", "--graph"], obj=project) + result = pdm(["list", "--freeze", "--graph"], obj=project) expected = "--graph cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--reverse"], obj=project) + result = pdm(["list", "--freeze", "--reverse"], obj=project) expected = "--reverse cannot be used without --graph" assert expected in result.outputs - result = invoke(["list", "--freeze", "-r"], obj=project) + result = pdm(["list", "--freeze", "-r"], obj=project) expected = "--reverse cannot be used without --graph" assert expected in result.outputs - result = invoke(["list", "--freeze", "--fields", "name"], obj=project) + result = pdm(["list", "--freeze", "--fields", "name"], obj=project) expected = "--fields cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--resolve"], obj=project) + result = pdm(["list", "--freeze", "--resolve"], obj=project) expected = "--resolve cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--sort", "name"], obj=project) + result = pdm(["list", "--freeze", "--sort", "name"], obj=project) expected = "--sort cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--csv"], obj=project) + result = pdm(["list", "--freeze", "--csv"], obj=project) expected = "--csv cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--json"], obj=project) + result = pdm(["list", "--freeze", "--json"], obj=project) expected = "--json cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--markdown"], obj=project) + result = pdm(["list", "--freeze", "--markdown"], obj=project) expected = "--markdown cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--include", "dev"], obj=project) + result = pdm(["list", "--freeze", "--include", "dev"], obj=project) expected = "--include/--exclude cannot be used with --freeze" assert expected in result.outputs - result = invoke(["list", "--freeze", "--exclude", "dev"], obj=project) + result = pdm(["list", "--freeze", "--exclude", "dev"], obj=project) expected = "--include/--exclude cannot be used with --freeze" assert expected in result.outputs -def test_list_multiple_export_formats(project, invoke): +def test_list_multiple_export_formats(project, pdm): # export formats cannot be used with each other - result = invoke(["list", "--csv", "--markdown"], obj=project) + result = pdm(["list", "--csv", "--markdown"], obj=project) expected = "--markdown: not allowed with argument --csv" assert expected in result.outputs - result = invoke(["list", "--csv", "--json"], obj=project) + result = pdm(["list", "--csv", "--json"], obj=project) expected = "--json: not allowed with argument --csv" assert expected in result.outputs - result = invoke(["list", "--markdown", "--csv"], obj=project) + result = pdm(["list", "--markdown", "--csv"], obj=project) expected = "--csv: not allowed with argument --markdown" assert expected in result.outputs - result = invoke(["list", "--markdown", "--json"], obj=project) + result = pdm(["list", "--markdown", "--json"], obj=project) expected = "--json: not allowed with argument --markdown" assert expected in result.outputs - result = invoke(["list", "--json", "--markdown"], obj=project) + result = pdm(["list", "--json", "--markdown"], obj=project) expected = "--markdown: not allowed with argument --json" assert expected in result.outputs - result = invoke(["list", "--json", "--csv"], obj=project) + result = pdm(["list", "--json", "--csv"], obj=project) expected = "--csv: not allowed with argument --json" assert expected in result.outputs @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("working_set") -def test_list_bare(project, invoke): - actions.do_add(project, packages=["requests"]) - result = invoke(["list"], obj=project) +def test_list_bare(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list"], obj=project) # Ordering can be different on different platforms # and python versions. assert "| name | version | location |\n" in result.output @@ -435,9 +429,9 @@ def test_list_bare(project, invoke): @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("working_set") -def test_list_bare_sorted_name(project, invoke): - actions.do_add(project, packages=["requests"]) - result = invoke(["list", "--sort", "name"], obj=project) +def test_list_bare_sorted_name(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list", "--sort", "name"], obj=project) expected = ( "+--------------------------------------+\n" "| name | version | location |\n" @@ -524,9 +518,9 @@ def prepare_metadata(self): @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("working_set") -def test_list_freeze(project, invoke): - actions.do_add(project, packages=["requests"]) - result = invoke(["list", "--freeze"], obj=project) +def test_list_freeze(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list", "--freeze"], obj=project) expected = ( "certifi==2018.11.17\n" "chardet==3.0.4\n" @@ -540,9 +534,9 @@ def test_list_freeze(project, invoke): @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("working_set") -def test_list_bare_sorted_version(project, invoke): - actions.do_add(project, packages=["requests"]) - result = invoke(["list", "--sort", "version"], obj=project) +def test_list_bare_sorted_version(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["list", "--sort", "version"], obj=project) expected = ( "+--------------------------------------+\n" "| name | version | location |\n" @@ -560,11 +554,11 @@ def test_list_bare_sorted_version(project, invoke): @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("fake_metadata") -def test_list_bare_sorted_version_resolve(project, invoke, working_set): +def test_list_bare_sorted_version_resolve(project, pdm, working_set): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add(project, packages=["requests"], sync=False) + pdm(["add", "requests", "--no-sync"], obj=project, strict=True) - result = invoke(["list", "--sort", "version", "--resolve"], obj=project, strict=True) + result = pdm(["list", "--sort", "version", "--resolve"], obj=project, strict=True) assert "requests" not in working_set expected = ( "+----------------------------------+\n" @@ -582,8 +576,8 @@ def test_list_bare_sorted_version_resolve(project, invoke, working_set): @mock.patch("pdm.termui.ROUNDED", ASCII) @pytest.mark.usefixtures("fake_working_set") -def test_list_bare_fields_licences(project, invoke): - result = invoke(["list", "--fields", "name,version,groups,licenses"], obj=project) +def test_list_bare_fields_licences(project, pdm): + result = pdm(["list", "--fields", "name,version,groups,licenses"], obj=project) expected = ( "+---------------------------------------------------------+\n" "| name | version | groups | licenses |\n" @@ -599,8 +593,8 @@ def test_list_bare_fields_licences(project, invoke): @pytest.mark.usefixtures("fake_working_set") -def test_list_csv_fields_licences(project, invoke): - result = invoke(["list", "--csv", "--fields", "name,version,licenses"], obj=project) +def test_list_csv_fields_licences(project, pdm): + result = pdm(["list", "--csv", "--fields", "name,version,licenses"], obj=project) expected = ( "name,version,licenses\n" "foo,0.1.0,A License\n" @@ -613,8 +607,8 @@ def test_list_csv_fields_licences(project, invoke): @pytest.mark.usefixtures("fake_working_set") -def test_list_json_fields_licences(project, invoke): - result = invoke(["list", "--json", "--fields", "name,version,licenses"], obj=project) +def test_list_json_fields_licences(project, pdm): + result = pdm(["list", "--json", "--fields", "name,version,licenses"], obj=project) expected = [ {"name": "foo", "version": "0.1.0", "licenses": "A License"}, {"name": "bar", "version": "3.0.1", "licenses": "B License"}, @@ -627,8 +621,8 @@ def test_list_json_fields_licences(project, invoke): @pytest.mark.usefixtures("fake_working_set") -def test_list_markdown_fields_licences(project, invoke): - result = invoke(["list", "--markdown", "--fields", "name,version,licenses"], obj=project) +def test_list_markdown_fields_licences(project, pdm): + result = pdm(["list", "--markdown", "--fields", "name,version,licenses"], obj=project) expected = ( "# test_project licenses\n" "## foo\n\n" @@ -681,16 +675,11 @@ def test_list_markdown_fields_licences(project, invoke): @pytest.mark.usefixtures("working_set", "repository") -def test_list_csv_include_exclude_valid(project, invoke): +def test_list_csv_include_exclude_valid(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") dep_path = FIXTURES.joinpath("projects/demo").as_posix() - actions.do_add( - project, - dev=True, - group="dev", - editables=[f"{dep_path}[security]"], - ) - result = invoke( + pdm(["add", "-de", f"{dep_path}[security]"], obj=project, strict=True) + result = pdm( [ "list", "--csv", @@ -711,18 +700,13 @@ def test_list_csv_include_exclude_valid(project, invoke): @pytest.mark.usefixtures("working_set", "repository") -def test_list_csv_include_exclude(project, invoke): +def test_list_csv_include_exclude(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") dep_path = FIXTURES.joinpath("projects/demo").as_posix() - actions.do_add( - project, - dev=True, - group="dev", - editables=[f"{dep_path}[security]"], - ) + pdm(["add", "-de", f"{dep_path}[security]"], obj=project, strict=True) # Show all groups. - result = invoke( + result = pdm( ["list", "--csv", "--fields", "name,version,groups", "--sort", "name"], obj=project, ) @@ -738,7 +722,7 @@ def test_list_csv_include_exclude(project, invoke): assert expected == result.output # Sub always included. - result = invoke( + result = pdm( [ "list", "--csv", @@ -755,7 +739,7 @@ def test_list_csv_include_exclude(project, invoke): assert expected == result.output # Include all (default) except sub - result = invoke( + result = pdm( [ "list", "--csv", @@ -772,7 +756,7 @@ def test_list_csv_include_exclude(project, invoke): assert expected == result.output # Show just the dev group - result = invoke( + result = pdm( [ "list", "--csv", @@ -791,7 +775,7 @@ def test_list_csv_include_exclude(project, invoke): assert expected == result.output # Exclude the dev group. - result = invoke( + result = pdm( [ "list", "--csv", diff --git a/tests/cli/test_lock.py b/tests/cli/test_lock.py index 87a2133add..3726f56340 100644 --- a/tests/cli/test_lock.py +++ b/tests/cli/test_lock.py @@ -7,10 +7,10 @@ from pdm.models.requirements import parse_requirement -def test_lock_command(project, invoke, mocker): +def test_lock_command(project, pdm, mocker): m = mocker.patch.object(actions, "do_lock") - invoke(["lock"], obj=project) - m.assert_called_with(project, refresh=False, hooks=ANY) + pdm(["lock"], obj=project) + m.assert_called_with(project, refresh=False, groups=["default"], hooks=ANY) @pytest.mark.usefixtures("repository") @@ -101,3 +101,13 @@ def test_skip_editable_dependencies_in_metadata(project, capsys): _, err = capsys.readouterr() assert "WARNING: Skipping editable dependency" in err assert not project.locked_repository.all_candidates + + +@pytest.mark.usefixtures("repository") +def test_lock_selected_groups(project, pdm): + project.add_dependencies({"requests": parse_requirement("requests")}, to_group="http") + project.add_dependencies({"pytz": parse_requirement("pytz")}) + pdm(["lock", "-G", "http", "--no-default"], obj=project, strict=True) + assert project.lockfile.groups == ["http"] + assert "requests" in project.locked_repository.all_candidates + assert "pytz" not in project.locked_repository.all_candidates diff --git a/tests/cli/test_others.py b/tests/cli/test_others.py index c5153a1704..efdd771e7d 100644 --- a/tests/cli/test_others.py +++ b/tests/cli/test_others.py @@ -1,7 +1,6 @@ import pytest from pdm.cli import actions -from pdm.exceptions import PdmException from pdm.models.requirements import parse_requirement from pdm.utils import cd from tests import FIXTURES @@ -17,14 +16,11 @@ def test_build_distributions(tmp_path, core): assert tarball.exists() -def test_project_no_init_error(project_no_init): - for handler in ( - actions.do_add, - actions.do_lock, - actions.do_update, - ): - with pytest.raises(PdmException, match="The pyproject.toml has not been initialized yet"): - handler(project_no_init) +def test_project_no_init_error(project_no_init, pdm): + for command in ("add", "lock", "update"): + result = pdm([command], obj=project_no_init) + assert result.exit_code != 0 + assert "The pyproject.toml has not been initialized yet" in result.stderr def test_help_option(invoke): diff --git a/tests/cli/test_remove.py b/tests/cli/test_remove.py index 993fed1e1c..6959dbf9ff 100644 --- a/tests/cli/test_remove.py +++ b/tests/cli/test_remove.py @@ -1,91 +1,79 @@ import pytest from pdm.cli import actions -from pdm.exceptions import PdmException, PdmUsageError +from pdm.models.requirements import parse_requirement from pdm.models.specifiers import PySpecSet -def test_remove_command(project, invoke, mocker): +def test_remove_command(project, pdm, mocker): do_remove = mocker.patch.object(actions, "do_remove") - invoke(["remove", "demo"], obj=project) + pdm(["remove", "demo"], obj=project) do_remove.assert_called_once() -@pytest.mark.usefixtures("repository", "working_set", "vcs") -def test_remove_editable_packages_while_keeping_normal(project): +@pytest.mark.usefixtures("working_set", "vcs") +def test_remove_editable_packages_while_keeping_normal(project, pdm): project.environment.python_requires = PySpecSet(">=3.6") - actions.do_add(project, packages=["demo"]) - actions.do_add( - project, - dev=True, - editables=["git+https://github.com/test-root/demo.git#egg=demo"], - ) + pdm(["add", "demo"], obj=project, strict=True) + pdm(["add", "-d", "-e", "git+https://github.com/test-root/demo.git#egg=demo"], obj=project, strict=True) dev_group = project.pyproject.settings["dev-dependencies"]["dev"] default_group = project.pyproject.metadata["dependencies"] - actions.do_remove(project, dev=True, packages=["demo"]) + pdm(["remove", "-d", "demo"], obj=project, strict=True) assert not dev_group assert len(default_group) == 1 assert not project.locked_repository.all_candidates["demo"].req.editable -@pytest.mark.usefixtures("repository") -def test_remove_package(project, working_set, is_dev): - actions.do_add(project, dev=is_dev, packages=["requests", "pytz"]) - actions.do_remove(project, dev=is_dev, packages=["pytz"]) +def test_remove_package(project, working_set, dev_option, pdm): + pdm(["add", *dev_option, "requests", "pytz"], obj=project, strict=True) + pdm(["remove", *dev_option, "pytz"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert "pytz" not in locked_candidates assert "pytz" not in working_set -@pytest.mark.usefixtures("repository") -def test_remove_package_with_dry_run(project, working_set, capsys): - actions.do_add(project, packages=["requests"]) - actions.do_remove(project, packages=["requests"], dry_run=True) - out, _ = capsys.readouterr() +def test_remove_package_with_dry_run(project, working_set, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["remove", "requests", "--dry-run"], obj=project, strict=True) project._lockfile = None locked_candidates = project.locked_repository.all_candidates assert "urllib3" in locked_candidates assert "urllib3" in working_set - assert "- urllib3 1.22" in out + assert "- urllib3 1.22" in result.output -@pytest.mark.usefixtures("repository") -def test_remove_package_no_sync(project, working_set): - actions.do_add(project, packages=["requests", "pytz"]) - actions.do_remove(project, sync=False, packages=["pytz"]) +def test_remove_package_no_sync(project, working_set, pdm): + pdm(["add", "requests", "pytz"], obj=project, strict=True) + pdm(["remove", "pytz", "--no-sync"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert "pytz" not in locked_candidates assert "pytz" in working_set -@pytest.mark.usefixtures("repository", "working_set") -def test_remove_package_not_exist(project): - actions.do_add(project, packages=["requests", "pytz"]) - with pytest.raises(PdmException): - actions.do_remove(project, sync=False, packages=["django"]) +@pytest.mark.usefixtures("working_set") +def test_remove_package_not_exist(project, pdm): + pdm(["add", "requests", "pytz"], obj=project, strict=True) + result = pdm(["remove", "django"], obj=project) + assert result.exit_code == 1 -@pytest.mark.usefixtures("repository") -def test_remove_package_exist_in_multi_groups(project, working_set): - actions.do_add(project, packages=["requests"]) - actions.do_add(project, dev=True, packages=["urllib3"]) - actions.do_remove(project, dev=True, packages=["urllib3"]) +def test_remove_package_exist_in_multi_groups(project, working_set, pdm): + pdm(["add", "requests"], obj=project, strict=True) + pdm(["add", "--dev", "urllib3"], obj=project, strict=True) + pdm(["remove", "--dev", "urllib3"], obj=project, strict=True) assert all("urllib3" not in line for line in project.pyproject.settings["dev-dependencies"]["dev"]) assert "urllib3" in working_set assert "requests" in working_set @pytest.mark.usefixtures("repository") -def test_add_remove_no_package(project): - with pytest.raises(PdmUsageError): - actions.do_add(project, packages=()) - - with pytest.raises(PdmUsageError): - actions.do_remove(project, packages=()) +def test_remove_no_package(project, pdm): + result = pdm(["remove"], obj=project) + assert result.exit_code != 0 -@pytest.mark.usefixtures("repository", "working_set") -def test_remove_package_wont_break_toml(project_no_init): +@pytest.mark.usefixtures("working_set") +def test_remove_package_wont_break_toml(project_no_init, pdm): project_no_init.pyproject._path.write_text( """ [project] @@ -96,5 +84,16 @@ def test_remove_package_wont_break_toml(project_no_init): """ ) project_no_init.pyproject.reload() - actions.do_remove(project_no_init, packages=["requests"]) + pdm(["remove", "requests"], obj=project_no_init, strict=True) assert project_no_init.pyproject.metadata["dependencies"] == [] + + +@pytest.mark.usefixtures("working_set") +def test_remove_group_not_in_lockfile(project, pdm, mocker): + pdm(["add", "requests"], obj=project, strict=True) + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="tz") + assert project.lockfile.groups == ["default"] + locker = mocker.patch.object(actions, "do_lock") + pdm(["remove", "--group", "tz", "pytz"], obj=project, strict=True) + assert not project.pyproject.metadata["optional-dependencies"].get("tz") + locker.assert_not_called() diff --git a/tests/cli/test_update.py b/tests/cli/test_update.py index 5493f6e8bf..2f475bfe87 100644 --- a/tests/cli/test_update.py +++ b/tests/cli/test_update.py @@ -1,41 +1,42 @@ import pytest from pdm.cli import actions -from pdm.exceptions import PdmUsageError +from pdm.models.requirements import parse_requirement -@pytest.mark.usefixtures("repository", "working_set") -def test_update_packages_with_top(project): - actions.do_add(project, packages=("requests",)) - with pytest.raises(PdmUsageError): - actions.do_update(project, packages=("requests",), top=True) +@pytest.mark.usefixtures("working_set") +def test_update_packages_with_top(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["update", "--top", "requests"], obj=project) + assert "PdmUsageError" in result.stderr -def test_update_command(project, invoke, mocker): +def test_update_command(project, pdm, mocker): do_update = mocker.patch.object(actions, "do_update") - invoke(["update"], obj=project) + pdm(["update"], obj=project) do_update.assert_called_once() @pytest.mark.usefixtures("working_set") -def test_update_ignore_constraints(project, repository): - actions.do_add(project, packages=("pytz",)) +def test_update_ignore_constraints(project, repository, pdm): + project.project_config["strategy.save"] = "compatible" + pdm(["add", "pytz"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"] == ["pytz~=2019.3"] repository.add_candidate("pytz", "2020.2") - actions.do_update(project, unconstrained=False, packages=("pytz",)) + pdm(["update", "pytz"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"] == ["pytz~=2019.3"] assert project.locked_repository.all_candidates["pytz"].version == "2019.3" - actions.do_update(project, unconstrained=True, packages=("pytz",)) + pdm(["update", "pytz", "--unconstrained"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"] == ["pytz~=2020.2"] assert project.locked_repository.all_candidates["pytz"].version == "2020.2" @pytest.mark.usefixtures("working_set") @pytest.mark.parametrize("strategy", ["reuse", "all"]) -def test_update_all_packages(project, repository, capsys, strategy): - actions.do_add(project, packages=["requests", "pytz"]) +def test_update_all_packages(project, repository, pdm, strategy): + pdm(["add", "requests", "pytz"], obj=project, strict=True) repository.add_candidate("pytz", "2019.6") repository.add_candidate("chardet", "3.0.5") repository.add_candidate("requests", "2.20.0") @@ -49,23 +50,21 @@ def test_update_all_packages(project, repository, capsys, strategy): "urllib3<1.24,>=1.21.1", ], ) - actions.do_update(project, strategy=strategy) + result = pdm(["update", f"--update-{strategy}"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.20.0" assert locked_candidates["chardet"].version == ("3.0.5" if strategy == "all" else "3.0.4") assert locked_candidates["pytz"].version == "2019.6" - out, _ = capsys.readouterr() update_num = 3 if strategy == "all" else 2 - assert f"{update_num} to update" in out, out + assert f"{update_num} to update" in result.stdout, result.stdout - actions.do_sync(project) - out, _ = capsys.readouterr() - assert "All packages are synced to date" in out + result = pdm(["sync"], obj=project, strict=True) + assert "All packages are synced to date" in result.stdout @pytest.mark.usefixtures("working_set") -def test_update_dry_run(project, repository, capsys): - actions.do_add(project, packages=["requests", "pytz"]) +def test_update_dry_run(project, repository, pdm): + pdm(["add", "requests", "pytz"], obj=project, strict=True) repository.add_candidate("pytz", "2019.6") repository.add_candidate("chardet", "3.0.5") repository.add_candidate("requests", "2.20.0") @@ -79,19 +78,18 @@ def test_update_dry_run(project, repository, capsys): "urllib3<1.24,>=1.21.1", ], ) - actions.do_update(project, dry_run=True) - out, _ = capsys.readouterr() + result = pdm(["update", "--dry-run"], obj=project, strict=True) project.lockfile.reload() locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.19.1" assert locked_candidates["chardet"].version == "3.0.4" assert locked_candidates["pytz"].version == "2019.3" - assert "requests 2.19.1 -> 2.20.0" in out + assert "requests 2.19.1 -> 2.20.0" in result.stdout @pytest.mark.usefixtures("working_set") -def test_update_top_packages_dry_run(project, repository, capsys): - actions.do_add(project, packages=["requests", "pytz"]) +def test_update_top_packages_dry_run(project, repository, pdm): + pdm(["add", "requests", "pytz"], obj=project, strict=True) repository.add_candidate("pytz", "2019.6") repository.add_candidate("chardet", "3.0.5") repository.add_candidate("requests", "2.20.0") @@ -105,15 +103,14 @@ def test_update_top_packages_dry_run(project, repository, capsys): "urllib3<1.24,>=1.21.1", ], ) - actions.do_update(project, top=True, dry_run=True) - out, _ = capsys.readouterr() - assert "requests 2.19.1 -> 2.20.0" in out - assert "- chardet 3.0.4 -> 3.0.5" not in out + result = pdm(["update", "--dry-run", "--top"], obj=project, strict=True) + assert "requests 2.19.1 -> 2.20.0" in result.stdout + assert "- chardet 3.0.4 -> 3.0.5" not in result.stdout @pytest.mark.usefixtures("working_set") -def test_update_specified_packages(project, repository): - actions.do_add(project, sync=False, packages=["requests", "pytz"]) +def test_update_specified_packages(project, repository, pdm): + pdm(["add", "requests", "pytz", "--no-sync"], obj=project, strict=True) repository.add_candidate("pytz", "2019.6") repository.add_candidate("chardet", "3.0.5") repository.add_candidate("requests", "2.20.0") @@ -127,15 +124,15 @@ def test_update_specified_packages(project, repository): "urllib3<1.24,>=1.21.1", ], ) - actions.do_update(project, packages=["requests"]) + pdm(["update", "requests"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.20.0" assert locked_candidates["chardet"].version == "3.0.4" @pytest.mark.usefixtures("working_set") -def test_update_specified_packages_eager_mode(project, repository): - actions.do_add(project, sync=False, packages=["requests", "pytz"]) +def test_update_specified_packages_eager_mode(project, repository, pdm): + pdm(["add", "requests", "pytz", "--no-sync"], obj=project, strict=True) repository.add_candidate("pytz", "2019.6") repository.add_candidate("chardet", "3.0.5") repository.add_candidate("requests", "2.20.0") @@ -149,54 +146,73 @@ def test_update_specified_packages_eager_mode(project, repository): "urllib3<1.24,>=1.21.1", ], ) - actions.do_update(project, strategy="eager", packages=["requests"]) + pdm(["update", "requests", "--update-eager"], obj=project, strict=True) locked_candidates = project.locked_repository.all_candidates assert locked_candidates["requests"].version == "2.20.0" assert locked_candidates["chardet"].version == "3.0.5" assert locked_candidates["pytz"].version == "2019.3" -@pytest.mark.usefixtures("repository", "working_set") -def test_update_with_package_and_groups_argument(project): - actions.do_add(project, packages=["requests", "pytz"]) - with pytest.raises(PdmUsageError): - actions.do_update(project, groups=("default", "dev"), packages=("requests",)) +@pytest.mark.usefixtures("working_set") +def test_update_with_package_and_groups_argument(project, pdm): + pdm(["add", "requests", "pytz"], obj=project, strict=True) + result = pdm(["update", "requests", "--group", "dev"], obj=project) + assert "PdmUsageError" in result.stderr - with pytest.raises(PdmUsageError): - actions.do_update(project, default=False, packages=("requests",)) + result = pdm(["update", "requests", "--no-default"], obj=project) + assert "PdmUsageError" in result.stderr -@pytest.mark.usefixtures("repository", "working_set") -def test_update_with_prerelease_without_package_argument(project): - actions.do_add(project, packages=["requests"]) - with pytest.raises(PdmUsageError, match="--prerelease must be used with packages given"): - actions.do_update(project, prerelease=True) +@pytest.mark.usefixtures("working_set") +def test_update_with_prerelease_without_package_argument(project, pdm): + pdm(["add", "requests"], obj=project, strict=True) + result = pdm(["update", "--prerelease"], obj=project) + assert "--prerelease must be used with packages given" in result.stderr -@pytest.mark.usefixtures("repository") -def test_update_existing_package_with_prerelease(project, working_set): - actions.do_add(project, packages=["urllib3"]) +def test_update_existing_package_with_prerelease(project, working_set, pdm): + project.project_config["strategy.save"] = "compatible" + pdm(["add", "urllib3"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "urllib3~=1.22" assert working_set["urllib3"].version == "1.22" - actions.do_update(project, packages=["urllib3"], prerelease=True) + pdm(["update", "urllib3", "--prerelease"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "urllib3~=1.22" assert working_set["urllib3"].version == "1.23b0" - actions.do_update(project, packages=["urllib3"], prerelease=True, unconstrained=True) + pdm(["update", "urllib3", "--prerelease", "--unconstrained"], obj=project, strict=True) assert project.pyproject.metadata["dependencies"][0] == "urllib3<2,>=1.23b0" assert working_set["urllib3"].version == "1.23b0" -def test_update_package_with_extras(project, repository, working_set): +def test_update_package_with_extras(project, repository, working_set, pdm): repository.add_candidate("foo", "0.1") foo_deps = ["urllib3; extra == 'req'"] repository.add_dependencies("foo", "0.1", foo_deps) - actions.do_add(project, packages=["foo[req]"]) + pdm(["add", "foo[req]"], obj=project, strict=True) assert working_set["foo"].version == "0.1" repository.add_candidate("foo", "0.2") repository.add_dependencies("foo", "0.2", foo_deps) - actions.do_update(project) + pdm(["update"], obj=project, strict=True) assert working_set["foo"].version == "0.2" assert project.locked_repository.all_candidates["foo"].version == "0.2" + + +def test_update_groups_in_lockfile(project, working_set, pdm, repository): + pdm(["add", "requests"], obj=project, strict=True) + repository.add_candidate("foo", "0.1") + pdm(["add", "foo", "--group", "extra"], obj=project, strict=True) + assert project.lockfile.groups == ["default", "extra"] + repository.add_candidate("foo", "0.2") + pdm(["update"], obj=project, strict=True) + assert project.locked_repository.all_candidates["foo"].version == "0.2" + assert working_set["foo"].version == "0.2" + + +def test_update_group_not_in_lockfile(project, working_set, pdm): + pdm(["add", "requests"], obj=project, strict=True) + project.add_dependencies({"pytz": parse_requirement("pytz")}, to_group="extra") + result = pdm(["update", "--group", "extra"], obj=project) + assert result.exit_code != 0 + assert "Requested groups not in lockfile: extra" in result.stderr diff --git a/tests/conftest.py b/tests/conftest.py index 1ccb5a4449..99caca2757 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -121,8 +121,8 @@ def is_editable(request): @pytest.fixture(params=[False, True]) -def is_dev(request): - return request.param +def dev_option(request) -> Iterable[str]: + return ("--dev",) if request.param else () @pytest.fixture diff --git a/tests/test_formats.py b/tests/test_formats.py index 898314391f..17ec5edde5 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -1,6 +1,8 @@ import shutil from argparse import Namespace +import pytest + from pdm.formats import flit, pipfile, poetry, requirements, setup_py from pdm.models.requirements import parse_requirement from pdm.utils import cd @@ -23,6 +25,7 @@ def test_convert_pipfile(project): assert settings["source"][0]["url"] == "https://pypi.python.org/simple" +@pytest.mark.parametrize("is_dev", [True, False]) def test_convert_requirements_file(project, is_dev): golden_file = FIXTURES / "requirements.txt" assert requirements.check_fingerprint(project, golden_file) diff --git a/tests/test_signals.py b/tests/test_signals.py index 2bfd29b8df..0e7b8eba3e 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,7 +1,8 @@ from unittest import mock +import pytest + from pdm import signals -from pdm.cli import actions def test_post_init_signal(project_no_init, invoke): @@ -12,12 +13,13 @@ def test_post_init_signal(project_no_init, invoke): mock_handler.assert_called_once_with(project_no_init, hooks=mock.ANY) -def test_post_lock_and_install_signals(project, working_set, repository): +@pytest.mark.usefixtures("working_set") +def test_post_lock_and_install_signals(project, pdm): pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False) post_lock = signals.post_lock.connect(mock.Mock(), weak=False) pre_install = signals.pre_install.connect(mock.Mock(), weak=False) post_install = signals.post_install.connect(mock.Mock(), weak=False) - actions.do_add(project, packages=["urllib3"]) + pdm(["add", "requests"], obj=project, strict=True) signals.pre_lock.disconnect(pre_lock) signals.post_lock.disconnect(post_lock) signals.pre_install.disconnect(pre_install) diff --git a/tests/test_utils.py b/tests/test_utils.py index 74677c02a1..901e7badb1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ from pdm import utils from pdm.cli import utils as cli_utils +from pdm.cli.filters import GroupSelection from pdm.exceptions import PdmUsageError @@ -98,26 +99,39 @@ def setup_dependencies(project): @pytest.mark.parametrize( "args,golden", [ - ((True, None, ()), ["default", "test", "doc"]), - ((True, None, [":all"]), ["default", "web", "auth", "test", "doc"]), - ((True, True, ["web"]), ["default", "web", "test", "doc"]), - ((True, None, ["web"]), ["default", "web", "test", "doc"]), - ((True, None, ["test"]), ["default", "test"]), - ((True, None, ["test", "web"]), ["default", "test", "web"]), - ((True, False, ["web"]), ["default", "web"]), - ((False, None, ()), ["test", "doc"]), + ({"default": True, "dev": None, "groups": ()}, ["default", "test", "doc"]), + ( + {"default": True, "dev": None, "groups": [":all"]}, + ["default", "web", "auth", "test", "doc"], + ), + ( + {"default": True, "dev": True, "groups": ["web"]}, + ["default", "web", "test", "doc"], + ), + ( + {"default": True, "dev": None, "groups": ["web"]}, + ["default", "web", "test", "doc"], + ), + ({"default": True, "dev": None, "groups": ["test"]}, ["default", "test"]), + ( + {"default": True, "dev": None, "groups": ["test", "web"]}, + ["default", "test", "web"], + ), + ({"default": True, "dev": False, "groups": ["web"]}, ["default", "web"]), + ({"default": False, "dev": None, "groups": ()}, ["test", "doc"]), ], ) def test_dependency_group_selection(project, args, golden): setup_dependencies(project) - target = cli_utils.translate_groups(project, *args) - assert sorted(golden) == sorted(target) + selection = GroupSelection(project, **args) + assert sorted(golden) == sorted(selection) def test_prod_should_not_be_with_dev(project): setup_dependencies(project) + selection = GroupSelection(project, default=True, dev=False, groups=["test"]) with pytest.raises(PdmUsageError): - cli_utils.translate_groups(project, True, False, ("test",)) + list(selection) def test_deprecation_warning():