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

Support update --dry-run to list outdated packages #366

Merged
merged 2 commits into from
Mar 31, 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/358.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support `--dry-run` option for `update` command to display packages that need update, install or removal. Add `--top` option to limit to top level packages only.
169 changes: 81 additions & 88 deletions pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
format_resolution_impossible,
save_version_specifiers,
set_env_in_reg,
translate_sections,
)
from pdm.exceptions import NoPythonVersion, PdmUsageError, ProjectError
from pdm.formats import FORMATS
Expand All @@ -41,15 +42,9 @@ def do_lock(
strategy: str = "all",
tracked_names: Optional[Iterable[str]] = None,
requirements: Optional[List[Requirement]] = None,
dry_run: bool = False,
) -> Dict[str, Candidate]:
"""Performs the locking process and update lockfile.

:param project: the project instance
:param strategy: update strategy: reuse/eager/all
:param tracked_names: required when using eager strategy
:param requirements: An optional dictionary of requirements, read from pyproject
if not given.
"""
"""Performs the locking process and update lockfile."""
check_project_file(project)
# TODO: multiple dependency definitions for the same package.
provider = project.get_provider(strategy, tracked_names)
Expand Down Expand Up @@ -89,7 +84,10 @@ def do_lock(
else:
data = format_lockfile(mapping, dependencies, summaries)
spin.succeed(f"{termui.Emoji.LOCK} Lock successful")
project.write_lockfile(data)
if not dry_run:
project.write_lockfile(data)
else:
project.lockfile = data

return mapping

Expand All @@ -101,39 +99,33 @@ def do_sync(
default: bool = True,
dry_run: bool = False,
clean: Optional[bool] = None,
tracked_names: Optional[Sequence[str]] = None,
) -> None:
"""Synchronize project

:param project: The project instance.
:param sections: A tuple of optional sections to be synced.
:param dev: whether to include dev-dependencies.
:param default: whether to include default dependencies.
:param dry_run: Print actions without actually running them.
:param clean: whether to remove unneeded packages.
"""
"""Synchronize project"""
if not project.lockfile_file.exists():
raise ProjectError("Lock file does not exist, nothing to sync")
clean = default if clean is None else clean
candidates = {}
sections = list(sections)
if dev and not sections:
sections.append(":all")
if ":all" in sections:
if dev:
sections = list(project.tool_settings.get("dev-dependencies", []))
else:
sections = list(project.meta.optional_dependencies or [])
if default:
sections.append("default")
for section in sections:
if section not in list(project.iter_sections()):
raise PdmUsageError(
f"Section {termui.green(repr(section))} doesn't exist "
"in the pyproject.toml"
)
candidates.update(project.get_locked_candidates(section))
handler = project.core.synchronizer_class(candidates, project.environment)
handler.synchronize(clean=clean, dry_run=dry_run)
if tracked_names and dry_run:
candidates = {
name: c
for name, c in project.get_locked_candidates("__all__").items()
if name in tracked_names
}
else:
candidates = {}
sections = translate_sections(project, default, dev, sections or ())
valid_sections = list(project.iter_sections())
for section in sections:
if section not in valid_sections:
raise PdmUsageError(
f"Section {termui.green(repr(section))} doesn't exist "
"in the pyproject.toml"
)
candidates.update(project.get_locked_candidates(section))
handler = project.core.synchronizer_class(
candidates, project.environment, clean, dry_run
)
handler.synchronize()


def do_add(
Expand Down Expand Up @@ -205,67 +197,68 @@ def do_update(
strategy: str = "reuse",
save: str = "compatible",
unconstrained: bool = False,
top: bool = False,
dry_run: bool = False,
packages: Sequence[str] = (),
) -> None:
"""Update specified packages or all packages

:param project: The project instance
:param dev: whether to update dev dependencies
:param sections: update specified sections
:param default: update default
:param strategy: update strategy (reuse/eager)
:param save: save strategy (compatible/exact/wildcard)
:param unconstrained: ignore version constraint
:param packages: specified packages to update
:return: None
"""
"""Update specified packages or all packages"""
check_project_file(project)
if len(packages) > 0 and (len(sections) > 1 or not default):
if len(packages) > 0 and (top or len(sections) > 1 or not default):
raise PdmUsageError(
"packages argument can't be used together with multiple -s or --no-default."
"packages argument can't be used together with multiple -s or "
"--no-default and --top."
)
if not packages:
if unconstrained:
raise PdmUsageError(
"--unconstrained must be used with package names given."
)
# pdm update with no packages given, same as 'lock' + 'sync'
do_lock(project)
do_sync(project, sections, dev, default, clean=False)
return
section = sections[0] if sections else ("dev" if dev else "default")
all_dependencies = project.all_dependencies
dependencies = all_dependencies[section]
updated_deps = {}
tracked_names = set()
for name in packages:
matched_name = next(
filter(
lambda k: safe_name(strip_extras(k)[0]).lower()
== safe_name(name).lower(),
dependencies.keys(),
),
None,
)
if not matched_name:
raise ProjectError(
"{} does not exist in {} {}dependencies.".format(
termui.green(name, bold=True), section, "dev-" if dev else ""
if not packages:
sections = translate_sections(project, default, dev, sections or ())
for section in sections:
updated_deps.update(all_dependencies[section])
else:
section = sections[0] if sections else ("dev" if dev else "default")
dependencies = all_dependencies[section]
for name in packages:
matched_name = next(
filter(
lambda k: safe_name(strip_extras(k)[0]).lower()
== safe_name(name).lower(),
dependencies.keys(),
),
None,
)
if not matched_name:
raise ProjectError(
"{} does not exist in {} {}dependencies.".format(
termui.green(name, bold=True), section, "dev-" if dev else ""
)
)
updated_deps[matched_name] = dependencies[matched_name]
project.core.ui.echo(
"Updating packages: {}.".format(
", ".join(termui.green(v, bold=True) for v in updated_deps)
)
if unconstrained:
dependencies[matched_name].specifier = get_specifier("")
tracked_names.add(matched_name)
updated_deps[matched_name] = dependencies[matched_name]
project.core.ui.echo(
"Updating packages: {}.".format(
", ".join(termui.green(v, bold=True) for v in tracked_names)
)
)
reqs = [r for deps in all_dependencies.values() for r in deps.values()]
resolved = do_lock(project, strategy, tracked_names, reqs)
do_sync(project, sections=sections, dev=dev, default=False, clean=False)
if unconstrained:
for _, dep in updated_deps.items():
dep.specifier = get_specifier("")
reqs = [r for deps in all_dependencies.values() for r in deps.values()]
resolved = do_lock(
project,
strategy if top or packages else "all",
updated_deps.keys(),
reqs,
dry_run=dry_run,
)
do_sync(
project,
sections=sections,
dev=dev,
default=default,
clean=False,
dry_run=dry_run,
tracked_names=updated_deps.keys() if top else None,
)
if unconstrained and not dry_run:
# Need to update version constraints
save_version_specifiers(updated_deps, resolved, save)
project.add_dependencies(updated_deps, section, dev)
Expand Down
2 changes: 2 additions & 0 deletions pdm/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.list:
return self._show_list(project)
global_env_options = project.scripts.get("_", {}) if project.scripts else {}
if not options.command:
raise PdmUsageError("No command given")
if project.scripts and options.command in project.scripts:
self._run_script(project, options.command, options.args, global_env_options)
else:
Expand Down
15 changes: 15 additions & 0 deletions pdm/cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
default=False,
help="Ignore the version constraint of packages",
)
parser.add_argument(
"-t",
"--top",
action="store_true",
help="Only update those list in pyproject.toml",
)
parser.add_argument(
"--dry-run",
"--outdated",
action="store_true",
dest="dry_run",
help="Show the difference only without modifying the lockfile content",
)
parser.add_argument(
"packages", nargs="*", help="If packages are given, only update them"
)
Expand All @@ -33,5 +46,7 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
save=options.save_strategy or project.config["strategy.save"],
strategy=options.update_strategy or project.config["strategy.update"],
unconstrained=options.unconstrained,
top=options.top,
dry_run=options.dry_run,
packages=options.packages,
)
8 changes: 4 additions & 4 deletions pdm/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def add_to_group(self, group: argparse._ArgumentGroup) -> None:
"--dry-run",
action="store_true",
default=False,
help="Only prints actions without actually running them",
help="Show the difference only and don't perform any action",
)


Expand All @@ -79,7 +79,7 @@ def add_to_group(self, group: argparse._ArgumentGroup) -> None:
help="Print the command line to be eval'd by the shell",
)

sections_group = ArgumentGroup()
sections_group = ArgumentGroup("Dependencies selection")
sections_group.add_argument(
"-s",
"--section",
Expand Down Expand Up @@ -159,7 +159,7 @@ def add_to_group(self, group: argparse._ArgumentGroup) -> None:
"--global",
dest="global_project",
action="store_true",
help="Use the global project, supply the project root with `-p` option.",
help="Use the global project, supply the project root with `-p` option",
)

clean_group = ArgumentGroup("clean", is_mutually_exclusive=True)
Expand All @@ -172,7 +172,7 @@ def add_to_group(self, group: argparse._ArgumentGroup) -> None:
sync_group.add_argument("--sync", action="store_true", help="sync packages")
sync_group.add_argument("--no-sync", action="store_false", help="don't sync packages")

packages_group = ArgumentGroup("packages")
packages_group = ArgumentGroup("Packages")
packages_group.add_argument(
"-e",
"--editable",
Expand Down
19 changes: 18 additions & 1 deletion pdm/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from argparse import Action
from collections import ChainMap
from pathlib import Path
from typing import TYPE_CHECKING, Set
from typing import TYPE_CHECKING, Sequence, Set

import cfonts
import tomlkit
Expand Down Expand Up @@ -445,3 +445,20 @@ def format_resolution_impossible(err: ResolutionImpossible) -> str:
"set a narrower `requires-python` range in the pyproject.toml."
)
return "\n".join(result)


def translate_sections(
project: Project, default: bool, dev: bool, sections: Sequence[str]
) -> Sequence[str]:
"""Translate default, dev and sections containing ":all" into a list of sections"""
sections = set(sections)
if dev and not sections:
sections.add(":all")
if ":all" in sections:
if dev:
sections = set(project.tool_settings.get("dev-dependencies", []))
else:
sections = set(project.meta.optional_dependencies or [])
if default:
sections.add("default")
return sections
Loading