Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: pdm install --check #813

Merged
merged 4 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/810.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `install --check` to check if the lock file is up to date.
37 changes: 24 additions & 13 deletions pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,30 @@ def resolve_candidates_from_lockfile(
return mapping


def check_lockfile(project: Project, raise_not_exist: bool = True) -> str | None:
"""Check if the lock file exists and is up to date. Return the update strategy."""
if not project.lockfile_file.exists():
if raise_not_exist:
raise ProjectError("Lock file does not exist, nothing to install")
project.core.ui.echo("Lock file does not exist", fg="yellow", err=True)
return "all"
elif not project.is_lockfile_compatible():
project.core.ui.echo(
"Lock file version is not compatible with PDM, installation may fail",
fg="yellow",
err=True,
)
return "all"
elif not project.is_lockfile_hash_match():
project.core.ui.echo(
"Lock file hash doesn't match pyproject.toml, packages may be outdated",
fg="yellow",
err=True,
)
return "reuse"
return None


def do_sync(
project: Project,
*,
Expand All @@ -143,19 +167,6 @@ def do_sync(
) -> None:
"""Synchronize project"""
if requirements is None:
if not project.lockfile_file.exists():
raise ProjectError("Lock file does not exist, nothing to sync")
elif not project.is_lockfile_compatible():
project.core.ui.echo(
"Lock file version is not compatible with PDM, "
"install may fail, please regenerate the pdm.lock",
err=True,
)
elif not project.is_lockfile_hash_match():
project.core.ui.echo(
"Lock file hash doesn't match pyproject.toml, packages may be outdated",
err=True,
)
groups = translate_groups(project, default, dev, groups or ())
requirements = []
for group in groups:
Expand Down
27 changes: 14 additions & 13 deletions pdm/cli/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,28 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
dest="lock",
action="store_false",
default=True,
help="Don't do lock if lockfile is not found or outdated.",
help="Don't do lock if the lock file is not found or outdated",
)
parser.add_argument(
"--check",
action="store_true",
help="Check if the lock file is up to date and fail otherwise",
)

def handle(self, project: Project, options: argparse.Namespace) -> None:
if not project.meta and click._compat.isatty(sys.stdout):
actions.ask_for_import(project)

if options.lock:
if not (
project.lockfile_file.exists() and project.is_lockfile_compatible()
):
project.core.ui.echo(
"Lock file does not exist or is incompatible, "
"trying to generate one..."
)
actions.do_lock(project, strategy="all", dry_run=options.dry_run)
elif not project.is_lockfile_hash_match():
strategy = actions.check_lockfile(project, False)
if strategy:
if options.check:
project.core.ui.echo(
"Lock file hash doesn't match pyproject.toml, regenerating..."
"Please run `pdm lock` to update the lock file", err=True
)
actions.do_lock(project, strategy="reuse", dry_run=options.dry_run)
sys.exit(1)
if options.lock:
project.core.ui.echo("Updating the lock file...", fg="green", err=True)
actions.do_lock(project, strategy=strategy, dry_run=options.dry_run)

actions.do_sync(
project,
Expand Down
1 change: 1 addition & 0 deletions pdm/cli/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
install_group.add_to_parser(parser)

def handle(self, project: Project, options: argparse.Namespace) -> None:
actions.check_lockfile(project)
actions.do_sync(
project,
groups=options.groups,
Expand Down
2 changes: 1 addition & 1 deletion pdm/cli/completions/pdm.bash
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ _pdm_a919b69078acdf0a_complete()
;;

(install)
opts="--dev --dry-run --global --group --help --no-default --no-editable --no-isolation --no-lock --no-self --production --project --verbose"
opts="--check --dev --dry-run --global --group --help --no-default --no-editable --no-isolation --no-lock --no-self --production --project --verbose"
;;

(list)
Expand Down
3 changes: 2 additions & 1 deletion pdm/cli/completions/pdm.fish
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from init' -l project -d 'Specify
complete -c pdm -A -n '__fish_seen_subcommand_from init' -l verbose -d '-v for detailed output and -vv for more detailed'

# install
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l check -d 'Check if the lock file is up to date and fail otherwise'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l dev -d 'Select dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l dry-run -d 'Show the difference only and don\'t perform any action'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l global -d 'Use the global project, supply the project root with `-p` option'
Expand All @@ -138,7 +139,7 @@ complete -c pdm -A -n '__fish_seen_subcommand_from install' -l help -d 'show thi
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-default -d 'Don\'t include dependencies from the default group'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-editable -d 'Install non-editable versions for all packages'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-isolation -d 'Do not isolate the build in a clean environment'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-lock -d 'Don\'t do lock if lockfile is not found or outdated.'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-lock -d 'Don\'t do lock if the lock file is not found or outdated'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l no-self -d 'Don\'t install the project itself'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l production -d 'Unselect dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from install' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__'
Expand Down
2 changes: 1 addition & 1 deletion pdm/cli/completions/pdm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ function TabExpansion($line, $lastWord) {
}
"install" {
$completer.AddOpts(@(
[Option]::new(("-d", "--dev", "-g", "--global", "--dry-run", "--no-default", "--no-lock", "--prod", "--production", "--no-editable", "--no-self", "--no-isolation")),
[Option]::new(("-d", "--dev", "-g", "--global", "--dry-run", "--no-default", "--no-lock", "--prod", "--production", "--no-editable", "--no-self", "--no-isolation", "--check")),
$sectionOption,
$projectOption
))
Expand Down
5 changes: 3 additions & 2 deletions pdm/cli/completions/pdm.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,13 @@ _pdm() {
{-G+,--group+}'[Select group of optional-dependencies or dev-dependencies(with -d). Can be supplied multiple times, use ":all" to include all groups under the same species]:group:_pdm_groups'
{-d,--dev}"[Select dev dependencies]"
{--prod,--production}"[Unselect dev dependencies]"
"--no-lock[Don't do lock if lockfile is not found or outdated]"
"--no-lock[Don't do lock if lock file is not found or outdated]"
"--no-default[Don\'t include dependencies from the default group]"
'--no-editable[Install non-editable versions for all packages]'
"--no-self[Don't install the project itself]"
"--no-isolation[do not isolate the build in a clean environment]"
"--dry-run[Show the difference only without modifying the lockfile content]"
"--dry-run[Show the difference only without modifying the lock file content]"
"--check[Check if the lock file is up to date and fail otherwise]"
)
;;
list)
Expand Down
6 changes: 6 additions & 0 deletions tests/cli/test_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ def test_add_package(project, working_set, is_dev):
assert package in working_set


def test_add_command(project, invoke, mocker):
do_add = mocker.patch.object(actions, "do_add")
invoke(["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"])
Expand Down
11 changes: 11 additions & 0 deletions tests/cli/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ def get_wheel_names(path):
return zf.namelist()


def test_build_command(project, invoke, mocker):
do_build = mocker.patch.object(actions, "do_build")
invoke(["build"], obj=project)
do_build.assert_called_once()


def test_build_global_project_forbidden(invoke):
result = invoke(["build", "-g"])
assert result.exit_code != 0


def test_build_single_module(fixture_project):
project = fixture_project("demo-module")
assert project.meta.version == "0.1.0"
Expand Down
53 changes: 49 additions & 4 deletions tests/cli/test_sync.py → tests/cli/test_install.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pytest

from pdm.cli import actions
from pdm.exceptions import PdmException
from pdm.models.requirements import parse_requirement
from tests.conftest import Distribution

Expand Down Expand Up @@ -32,10 +31,10 @@ def test_sync_packages_with_all_dev(project, working_set):
assert "pyopenssl" in working_set


def test_sync_no_lockfile(project):
def test_sync_no_lockfile(project, invoke):
project.add_dependencies({"requests": parse_requirement("requests")})
with pytest.raises(PdmException):
actions.do_sync(project)
result = invoke(["sync"], obj=project)
assert result.exit_code == 1


@pytest.mark.usefixtures("repository")
Expand Down Expand Up @@ -144,3 +143,49 @@ def test_sync_with_index_change(project, index):
# Mimic the CDN inconsistences of PyPI simple index. See issues/596.
del index["future-fstrings"]
actions.do_sync(project, no_self=True)


def test_install_command(project, invoke, mocker):
do_lock = mocker.patch.object(actions, "do_lock")
do_sync = mocker.patch.object(actions, "do_sync")
invoke(["install"], obj=project)
do_lock.assert_called_once()
do_sync.assert_called_once()


def test_sync_command(project, invoke, mocker):
invoke(["lock"], obj=project)
do_sync = mocker.patch.object(actions, "do_sync")
invoke(["sync"], obj=project)
do_sync.assert_called_once()


def test_install_with_lockfile(project, invoke, working_set, repository):
result = invoke(["lock", "-v"], obj=project)
assert result.exit_code == 0
result = invoke(["install"], obj=project)
assert "Lock file" not in result.stderr

project.add_dependencies({"pytz": parse_requirement("pytz")}, "default")
result = invoke(["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):
project.add_dependencies({"pytz": parse_requirement("pytz")}, "default")
result = invoke(["install", "--dry-run"], obj=project)
project._lockfile = None
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)
assert result.exit_code == 1

result = invoke(["add", "requests", "--no-sync"], obj=project)
project.add_dependencies({"requests": parse_requirement("requests>=2.0")})
result = invoke(["install", "--check"], obj=project)
assert result.exit_code == 1
Loading