Skip to content

Commit

Permalink
feat: Add lock option to resolve direct dependencies to the minimal v…
Browse files Browse the repository at this point in the history
…ersions available (#2327)
  • Loading branch information
frostming authored Oct 23, 2023
1 parent a7f4e37 commit 70f4985
Show file tree
Hide file tree
Showing 21 changed files with 295 additions and 88 deletions.
41 changes: 34 additions & 7 deletions docs/docs/usage/dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,30 +329,57 @@ pdm lock --prod -L pdm.prod.lock

Check the `metadata.groups` field in the lockfile to see which groups are included.

## Cross-platform lockfile
## Lock strategies

Currently, we support three flags to control the locking behavior: `cross_platform`, `static_urls` and `direct_minimal_versions`, with the meanings as follows.
You can pass one or more flags to `pdm lock` by `--strategy/-S` option, either by giving a comma-separated list or by passing the option multiple times.
Both of these commands function in the same way:

```bash
pdm lock -S cross_platform,static_urls
pdm lock -S cross_platform -S static_urls
```

The flags will be encoded in the lockfile and get read when you run `pdm lock` next time. But you can disable flags by prefixing the flag name with `no_`:

```bash
pdm lock -S no_cross_platform
```

This command makes the lockfile not cross-platform.

### Cross platform

_New in version 2.6.0_

By default, the generated lockfile is **cross-platform**, which means the current platform isn't taken into account when resolving the dependencies. The result lockfile will contain wheels and dependencies for all possible platforms and Python versions.
However, sometimes this will result in a wrong lockfile when a release doesn't contain all wheels. To avoid this, you can tell PDM
to create a lockfile that works for **this platform** only, trimming the wheels not relevant to the current platform. This can be done by passing the `--no-cross-platform` option to `pdm lock`:
to create a lockfile that works for **this platform** only, trimming the wheels not relevant to the current platform. This can be done by passing the `--strategy no_cross_platform` option to `pdm lock`:

```bash
pdm lock --no-cross-platform
pdm lock --strategy no_cross_platform
```

## Store static URLs or filenames in lockfile
### Static URLs

_New in version 2.8.0_

By default, PDM only stores the filenames of the packages in the lockfile, which benefits the reusability across different package indexes.
However, if you want to store the static URLs of the packages in the lockfile, you can pass the `--static-urls` option to `pdm lock`:
However, if you want to store the static URLs of the packages in the lockfile, you can pass the `--strategy static_urls` option to `pdm lock`:

```bash
pdm lock --static-urls
pdm lock --strategy static_urls
```

The settings will be saved and remembered for the same lockfile. You can also pass `--no-static-urls` to disable it.
The settings will be saved and remembered for the same lockfile. You can also pass `--strategy no_static_urls` to disable it.

### Direct minimal versions

_New in version 2.10.0_

When it is enabled by passing `--strategy direct_minimal_versions`, dependencies specified in the `pyproject.toml` will be resolved to the minimal versions available, rather than the latest versions. This is useful when you want to test the compatibility of your project within a range of dependency versions.

For example, if you specified `flask>=2.0` in the `pyproject.toml`, `flask` will be resolved to version `2.0.0` if there is no other compatibility issue.

## Show what packages are installed

Expand Down
1 change: 1 addition & 0 deletions news/2310.feature.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add lock option to resolve direct dependencies to the minimal versions available.
1 change: 1 addition & 0 deletions news/2310.feature.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Introduce a new `--strategy/-S` option for `lock` command, to specify one or more strategy flags for resolving dependencies. `--static-urls` and `--no-cross-platform` are deprecated at the same time.
25 changes: 13 additions & 12 deletions src/pdm/cli/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pdm.models.candidates import Candidate
from pdm.models.requirements import Requirement, parse_requirement
from pdm.project import Project
from pdm.project.lockfile import FLAG_CROSS_PLATFORM, FLAG_DIRECT_MINIMAL_VERSIONS
from pdm.resolver import resolve
from pdm.utils import deprecation_warning

Expand All @@ -42,15 +43,13 @@ def do_lock(
dry_run: bool = False,
refresh: bool = False,
groups: list[str] | None = None,
cross_platform: bool | None = None,
static_urls: bool | None = None,
strategy_change: list[str] | None = None,
hooks: HookManager | None = None,
) -> dict[str, Candidate]:
"""Performs the locking process and update lockfile."""
hooks = hooks or HookManager(project)
check_project_file(project)
if static_urls is None:
static_urls = project.lockfile.static_urls
lock_strategy = project.lockfile.apply_strategy_change(strategy_change or [])
if refresh:
locked_repo = project.locked_repository
repo = project.get_repository()
Expand All @@ -68,19 +67,23 @@ def do_lock(
c.hashes.clear()
fetch_hashes(repo, mapping)
lockfile = format_lockfile(
project, mapping, dependencies, groups=project.lockfile.groups, static_urls=static_urls
project, mapping, dependencies, groups=project.lockfile.groups, strategy=lock_strategy
)
project.write_lockfile(lockfile)
return mapping
# TODO: multiple dependency definitions for the same package.
if cross_platform is None:
cross_platform = project.lockfile.cross_platform
provider = project.get_provider(strategy, tracked_names, ignore_compatibility=cross_platform)

provider = project.get_provider(
strategy,
tracked_names,
ignore_compatibility=FLAG_CROSS_PLATFORM in lock_strategy,
direct_minimal_versions=FLAG_DIRECT_MINIMAL_VERSIONS in lock_strategy,
)
if not requirements:
requirements = [
r for g, deps in project.all_dependencies.items() if groups is None or g in groups for r in deps.values()
]
if not cross_platform:
if FLAG_CROSS_PLATFORM not in lock_strategy:
this_env = project.environment.marker_environment
requirements = [req for req in requirements if not req.marker or req.marker.evaluate(this_env)]
resolve_max_rounds = int(project.config["strategy.resolve_max_rounds"])
Expand Down Expand Up @@ -116,9 +119,7 @@ def do_lock(
ui.echo(format_resolution_impossible(err), err=True)
raise ResolutionImpossible("Unable to find a resolution") from None
else:
data = format_lockfile(
project, mapping, dependencies, groups=groups, cross_platform=cross_platform, static_urls=static_urls
)
data = format_lockfile(project, mapping, dependencies, groups=groups, strategy=lock_strategy)
if project.enable_write_lockfile:
ui.echo(f"{termui.Emoji.LOCK} Lock successful")
project.write_lockfile(data, write=not dry_run)
Expand Down
36 changes: 10 additions & 26 deletions src/pdm/cli/commands/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
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,
lockfile_option,
no_isolation_option,
skip_option,
)
from pdm.cli.options import groups_group, lock_strategy_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, groups_group)
arguments = (
*BaseCommand.arguments,
lockfile_option,
no_isolation_option,
skip_option,
groups_group,
lock_strategy_group,
)

def add_arguments(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
Expand All @@ -32,23 +34,6 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
action="store_true",
help="Check if the lock file is up to date and quit",
)
parser.add_argument(
"--no-cross-platform",
action="store_false",
default=True,
dest="cross_platform",
help="Only lock packages for the current platform",
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--static-urls", action="store_true", help="Store static file URLs in the lockfile", default=None
)
group.add_argument(
"--no-static-urls",
action="store_false",
dest="static_urls",
help="Do not store static file URLs in the lockfile",
)

def handle(self, project: Project, options: argparse.Namespace) -> None:
if options.check:
Expand All @@ -72,7 +57,6 @@ def handle(self, project: Project, options: argparse.Namespace) -> None:
project,
refresh=options.refresh,
groups=selection.all(),
cross_platform=options.cross_platform,
static_urls=options.static_urls,
strategy_change=options.strategy_change,
hooks=HookManager(project, options.skip),
)
2 changes: 1 addition & 1 deletion src/pdm/cli/completions/pdm.bash
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ _pdm_a919b69078acdf0a_complete()
;;

(lock)
opts="--check --dev --global --group --help --lockfile --no-cross-platform --no-default --no-isolation --no-static-urls --production --project --quiet --refresh --skip --static-urls --verbose"
opts="--check --dev --global --group --help --lockfile --no-cross-platform --no-default --no-isolation --no-static-urls --production --project --quiet --refresh --skip --static-urls --strategy --verbose"
;;

(plugin)
Expand Down
7 changes: 4 additions & 3 deletions src/pdm/cli/completions/pdm.fish
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,17 @@ complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l global -d 'Use the g
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l group -d 'Select group of optional-dependencies separated by comma or dev-dependencies (with `-d`). Can be supplied multiple times, use ":all" to include all groups under the same species.'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l help -d 'Show this help message and exit.'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l lockfile -d 'Specify another lockfile path. Default: pdm.lock. [env var: PDM_LOCKFILE]'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-cross-platform -d 'Only lock packages for the current platform'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-cross-platform -d '[DEPRECATED] Only lock packages for the current platform'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-default -d 'Don\'t include dependencies from the default group'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-isolation -d 'Disable isolation when building a source distribution that follows PEP 517, as in: build dependencies specified by PEP 518 must be already installed if this option is used.'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-static-urls -d 'Do not store static file URLs in the lockfile'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l no-static-urls -d '[DEPRECATED] Do not store static file URLs in the lockfile'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l production -d 'Unselect dev dependencies'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l project -d 'Specify another path as the project root, which changes the base of pyproject.toml and __pypackages__ [env var: PDM_PROJECT]'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l quiet -d 'Suppress output'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l refresh -d 'Don\'t update pinned versions, only refresh the lock file'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l skip -d 'Skip some tasks and/or hooks by their comma-separated names. Can be supplied multiple times. Use ":all" to skip all hooks. Use ":pre" and ":post" to skip all pre or post hooks.'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l static-urls -d 'Store static file URLs in the lockfile'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l static-urls -d '[DEPRECATED] Store static file URLs in the lockfile'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l strategy -d 'Specify lock strategy(cross_platform,static_urls,direct_minimal_versions). Add \'no_\' prefix to disable. Support given multiple times or split by comma.'
complete -c pdm -A -n '__fish_seen_subcommand_from lock' -l verbose -d 'Use `-v` for detailed output and `-vv` for more detailed'

# plugin
Expand Down
3 changes: 2 additions & 1 deletion src/pdm/cli/completions/pdm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ function TabExpansion($line, $lastWord) {
@(
[Option]::new(@(
"--global", "-g", "--no-isolation", "--refresh", "-L", "--lockfile", "--check", "--dev", "--prod",
"--production", "-d", "--no-default", "--no-cross-platform", "--static-urls", "--no-static-urls"
"--production", "-d", "--no-default", "--no-cross-platform", "--static-urls", "--no-static-urls",
"--strategy", "-S"
)),
$skipOption,
$sectionOption,
Expand Down
7 changes: 4 additions & 3 deletions src/pdm/cli/completions/pdm.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,11 @@ _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]"
"--static-urls[Store static file URLs in the lockfile]"
"--no-static-urls[Do not store static file URLs in the lockfile]"
"--static-urls[(DEPRECATED) Store static file URLs in the lockfile]"
"--no-static-urls[(DEPRECATED) Do not store static file URLs in the lockfile]"
"--no-default[Don\'t include dependencies from the default group]"
"--no-cross-platform[Only lock packages for the current platform]"
"--no-cross-platform[(DEPRECATED) Only lock packages for the current platform]"
{-S,--strategy}'Specify lock strategy(cross_platform,static_urls,direct_minimal_versions). Add no_ prefix to disable. Support given multiple times or split by comma.:strategy:'
)
;;
self)
Expand Down
33 changes: 33 additions & 0 deletions src/pdm/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,36 @@ def ignore_python_option(project: Project, namespace: argparse.Namespace, values
help="Run the command in the virtual environment with the given key. [env var: PDM_IN_VENV]",
default=os.getenv("PDM_IN_VENV"),
)


lock_strategy_group = ArgumentGroup("lock_strategy")
lock_strategy_group.add_argument(
"--strategy",
"-S",
dest="strategy_change",
metavar="STRATEGY",
action=split_lists(","),
help="Specify lock strategy(cross_platform,static_urls,direct_minimal_versions). Add 'no_' prefix to disable."
" Support given multiple times or split by comma.",
)
lock_strategy_group.add_argument(
"--no-cross-platform",
action="append_const",
dest="strategy_change",
const="no_cross_platform",
help="[DEPRECATED] Only lock packages for the current platform",
)
lock_strategy_group.add_argument(
"--static-urls",
action="append_const",
dest="strategy_change",
help="[DEPRECATED] Store static file URLs in the lockfile",
const="static_urls",
)
lock_strategy_group.add_argument(
"--no-static-urls",
action="append_const",
dest="strategy_change",
help="[DEPRECATED] Do not store static file URLs in the lockfile",
const="no_static_urls",
)
13 changes: 7 additions & 6 deletions src/pdm/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
strip_extras,
)
from pdm.models.specifiers import PySpecSet, get_specifier
from pdm.project.lockfile import FLAG_CROSS_PLATFORM, FLAG_STATIC_URLS
from pdm.utils import (
comparable_version,
is_path_relative_to,
Expand Down Expand Up @@ -514,9 +515,8 @@ def format_lockfile(
project: Project,
mapping: dict[str, Candidate],
fetched_dependencies: dict[tuple[str, str | None], list[Requirement]],
groups: list[str] | None = None,
cross_platform: bool | None = None,
static_urls: bool | None = None,
groups: list[str] | None,
strategy: set[str],
) -> dict:
"""Format lock file from a dict of resolved candidates, a mapping of dependencies
and a collection of package summaries.
Expand Down Expand Up @@ -544,7 +544,7 @@ def format_lockfile(
if v.hashes:
collected = {}
for item in v.hashes:
if static_urls:
if FLAG_STATIC_URLS in strategy:
row = {"url": item["url"], "hash": item["hash"]}
else:
row = {"file": item["file"], "hash": item["hash"]}
Expand All @@ -561,10 +561,11 @@ def format_lockfile(
metadata.update(
{
"groups": sorted(groups, key=lambda k: k != "default"),
"cross_platform": cross_platform if cross_platform is not None else project.lockfile.cross_platform,
"static_urls": static_urls if static_urls is not None else project.lockfile.static_urls,
"strategy": sorted(strategy),
}
)
metadata.pop(FLAG_STATIC_URLS, None)
metadata.pop(FLAG_CROSS_PLATFORM, None)
doc.add("metadata", metadata)
doc.add("package", packages)
return cast(dict, doc)
Expand Down
6 changes: 4 additions & 2 deletions src/pdm/environments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,15 @@ def get_finder(
self,
sources: list[RepositoryConfig] | None = None,
ignore_compatibility: bool = False,
minimal_version: bool = False,
) -> Generator[unearth.PackageFinder, None, None]:
"""Return the package finder of given index sources.
:param sources: a list of sources the finder should search in.
:param ignore_compatibility: whether to ignore the python version
and wheel tags.
"""
from unearth import PackageFinder
from pdm.models.finder import PDMPackageFinder

if sources is None:
sources = self.project.sources
Expand All @@ -142,7 +143,7 @@ def get_finder(

session = self._build_session(trusted_hosts)
with self._patch_target_python():
finder = PackageFinder(
finder = PDMPackageFinder(
session=session,
target_python=self.target_python,
ignore_compatibility=ignore_compatibility,
Expand All @@ -153,6 +154,7 @@ def get_finder(
"respect-source-order", False
),
verbosity=self.project.core.ui.verbosity,
minimal_version=minimal_version,
)
for source in sources:
assert source.url
Expand Down
Loading

0 comments on commit 70f4985

Please sign in to comment.