Skip to content

Commit

Permalink
Support updates to lock configuration.
Browse files Browse the repository at this point in the history
This is needed to pick up IC changes in universal locks amongst many other
configuration changes that can affect what versions are chosen and which
artifacts are locked.
  • Loading branch information
jsirois committed Feb 21, 2024
1 parent 879ab4b commit 120fe0d
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 39 deletions.
64 changes: 43 additions & 21 deletions pex/cli/commands/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def sync(
self,
requirement_configuration, # type: RequirementConfiguration
pin=False, # type: bool
lock_config_updated=False, # type: bool
):
# type: (...) -> Union[LockUpdate, Result]
if not self._update_requests:
Expand All @@ -338,6 +339,7 @@ def sync(
update_requests=self._update_requests,
requirement_configuration=requirement_configuration,
pin=pin,
lock_config_updated=lock_config_updated,
)

def _no_updates(self):
Expand Down Expand Up @@ -1342,41 +1344,61 @@ def _sync(self):
production_assert(isinstance(resolver_configuration, LockRepositoryConfiguration))
pip_configuration = resolver_configuration.pip_configuration

target_configuration = target_options.configure(self.options)
if self.options.style == LockStyle.UNIVERSAL:
lock_configuration = LockConfiguration(
style=LockStyle.UNIVERSAL,
requires_python=tuple(
str(interpreter_constraint.requires_python)
for interpreter_constraint in target_configuration.interpreter_constraints
),
target_systems=tuple(self.options.target_systems),
)
elif self.options.target_systems:
return Error(
"The --target-system option only applies to --style {universal} locks.".format(
universal=LockStyle.UNIVERSAL.value
)
)
else:
lock_configuration = LockConfiguration(style=self.options.style)

lock_file_path = self.options.lock
if os.path.exists(lock_file_path):
lock_file = try_(parse_lockfile(self.options, lock_file_path=lock_file_path))
build_configuration = pip_configuration.build_configuration
original_lock_file = try_(parse_lockfile(self.options, lock_file_path=lock_file_path))
lock_file = attr.evolve(
original_lock_file,
style=lock_configuration.style,
requires_python=SortedTuple(lock_configuration.requires_python),
target_systems=SortedTuple(lock_configuration.target_systems),
pip_version=pip_configuration.version,
resolver_version=pip_configuration.resolver_version,
allow_prereleases=pip_configuration.allow_prereleases,
allow_wheels=build_configuration.allow_wheels,
only_wheels=SortedTuple(build_configuration.only_wheels),
allow_builds=build_configuration.allow_builds,
only_builds=SortedTuple(build_configuration.only_builds),
prefer_older_binary=build_configuration.prefer_older_binary,
use_pep517=build_configuration.use_pep517,
build_isolation=build_configuration.build_isolation,
transitive=pip_configuration.transitive,
)
lock_update_request = try_(
self._create_lock_update_request(lock_file_path=lock_file_path, lock_file=lock_file)
)

pin = getattr(self.options, "pin", False)
lock_update = lock_update_request.sync(
requirement_configuration=requirement_configuration, pin=pin
requirement_configuration=requirement_configuration,
pin=pin,
lock_config_updated=lock_file != original_lock_file,
)
if isinstance(lock_update, Result):
return lock_update

try_(self._process_lock_update(lock_update, lock_file, lock_file_path))
else:
target_configuration = target_options.configure(self.options)
if self.options.style == LockStyle.UNIVERSAL:
lock_configuration = LockConfiguration(
style=LockStyle.UNIVERSAL,
requires_python=tuple(
str(interpreter_constraint.requires_python)
for interpreter_constraint in target_configuration.interpreter_constraints
),
target_systems=tuple(self.options.target_systems),
)
elif self.options.target_systems:
return Error(
"The --target-system option only applies to --style {universal} locks.".format(
universal=LockStyle.UNIVERSAL.value
)
)
else:
lock_configuration = LockConfiguration(style=self.options.style)

targets = try_(
self._resolve_targets(
action="creating",
Expand Down
42 changes: 32 additions & 10 deletions pex/resolve/lockfile/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ def update_resolve(
locked_resolve, # type: LockedResolve
target, # type: Target
pin_all=False, # type: bool
artifacts_can_change=False, # type: bool
):
# type: (...) -> Union[ResolveUpdate, Error]

Expand Down Expand Up @@ -469,6 +470,21 @@ def update_resolve(
updated_pin = updated_requirement.pin
original_artifacts = tuple(locked_requirement.iter_artifacts())
updated_artifacts = tuple(updated_requirement.iter_artifacts())

# N.B.: We use a custom key for artifact equality comparison since `Artifact`
# contains a `verified` attribute that can both vary based on Pex's current
# knowledge about the trustworthiness of an artifact hash and is not relevant to
# whether the artifact refers to the same artifact.
def artifact_key(artifact):
# type: (Artifact) -> Any
return artifact.url, artifact.fingerprint

def artifacts_differ():
# type: () -> bool
return tuple(map(artifact_key, original_artifacts)) != tuple(
map(artifact_key, updated_artifacts)
)

if (
self.update_constraints_by_project_name
and project_name not in self.update_constraints_by_project_name
Expand All @@ -480,13 +496,13 @@ def update_resolve(
)
)

# N.B.: We use a custom key for artifact equality comparison since `Artifact`
# contains a `verified` attribute that can both vary based on Pex's current
# knowledge about the trustworthiness of an artifact hash and is not relevant to
# whether the artifact refers to the same artifact.
def artifact_key(artifact):
# type: (Artifact) -> Any
return artifact.url, artifact.fingerprint
if artifacts_can_change and artifacts_differ():
updates[project_name] = ArtifactsUpdate.calculate(
version=original_pin.version,
original=original_artifacts,
updated=updated_artifacts,
)
continue

assert artifact_key(updated_requirement.artifact) == artifact_key(
locked_requirement.artifact
Expand Down Expand Up @@ -528,7 +544,7 @@ def artifact_key(artifact):
updates[project_name] = VersionUpdate(
original=original_pin.version, updated=updated_pin.version
)
elif original_artifacts != updated_artifacts:
elif artifacts_differ():
updates[project_name] = ArtifactsUpdate.calculate(
version=original_pin.version,
original=original_artifacts,
Expand Down Expand Up @@ -611,6 +627,7 @@ def sync(
update_requests, # type: Iterable[ResolveUpdateRequest]
requirement_configuration, # type: RequirementConfiguration
pin=False, # type: bool
lock_config_updated=False, # type: bool
):
# type: (...) -> Union[LockUpdate, Error]

Expand All @@ -624,7 +641,7 @@ def sync(
self.pip_configuration.network_configuration
)
)
if not any((pin, requirements, constraints)):
if not any((pin, lock_config_updated, requirements, constraints)):
return LockUpdate(
requirements=self.lock_file.requirements,
resolves=tuple(
Expand All @@ -643,7 +660,10 @@ def sync(
)
)
return self._perform_update(
update_requests=update_requests, resolve_updater=resolve_updater, pin=pin
update_requests=update_requests,
resolve_updater=resolve_updater,
pin=pin,
artifacts_can_change=lock_config_updated,
)

def update(
Expand Down Expand Up @@ -682,6 +702,7 @@ def _perform_update(
update_requests, # type: Iterable[ResolveUpdateRequest]
resolve_updater, # type: ResolveUpdater
pin=False, # type: bool
artifacts_can_change=False, # type: bool
):
# type: (...) -> Union[LockUpdate, Error]

Expand All @@ -702,6 +723,7 @@ def _perform_update(
locked_resolve=update_request.locked_resolve,
target=update_request.target,
pin_all=pin,
artifacts_can_change=artifacts_can_change,
)
if isinstance(result, Error):
error_by_target[update_request.target] = result
Expand Down
66 changes: 58 additions & 8 deletions tests/integration/cli/commands/test_lock_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pex.pep_440 import Version
from pex.pep_503 import ProjectName
from pex.pip.version import PipVersion
from pex.resolve.locked_resolve import LockedResolve
from pex.resolve.locked_resolve import LockedResolve, LockStyle
from pex.resolve.lockfile import json_codec
from pex.resolve.lockfile.model import Lockfile
from pex.resolve.path_mappings import PathMapping, PathMappings
Expand Down Expand Up @@ -71,9 +71,10 @@ def host_requirements(*requirements):
pip_version.setuptools_requirement,
pip_version.wheel_requirement,
)
find_links_repo.make_wheel("foo", version="1")
find_links_repo.make_wheel("bar", version="1")
find_links_repo.make_wheel("baz", version="1")
for project_name in "foo", "bar", "baz":
find_links_repo.make_sdist(project_name, version="1")
find_links_repo.make_wheel(project_name, version="1")

run_pex3(
"lock",
"create",
Expand All @@ -92,9 +93,9 @@ def host_requirements(*requirements):
).assert_success()

host_requirements("cowsay<6.1")
find_links_repo.make_wheel("foo", version="2")
find_links_repo.make_wheel("bar", version="2")
find_links_repo.make_wheel("baz", version="2")
for project_name in "foo", "bar", "baz":
find_links_repo.make_sdist(project_name, version="2")
find_links_repo.make_wheel(project_name, version="2")

return SessionFixtures(
find_links=os.path.join(test_lock_sync_chroot, "find_links"),
Expand Down Expand Up @@ -452,6 +453,56 @@ def test_sync_complex(
)


def test_sync_configuration(
tmpdir, # type: Any
repo_args, # type: List[str]
initial_lock, # type: str
path_mappings, # type: PathMappings
path_mapping_args, # type: List[str]
):
# type: (...) -> None

lock_file = json_codec.load(initial_lock, path_mappings=path_mappings)
assert_lock(
lock_file,
path_mappings,
expected_pins=[pin("cowsay", "5.0"), pin("foo", "1"), pin("bar", "1")],
)
assert lock_file.style is LockStyle.STRICT

run_sync(
"cowsay",
"foo>1",
"bar",
"--style",
"sources",
"--lock",
initial_lock,
*(repo_args + path_mapping_args)
).assert_success(
expected_output_re=NO_OUTPUT,
expected_error_re=re_exact(
dedent(
"""\
Updates for lock generated by {platform}:
Updated bar 1 artifacts:
+ file://${{FL}}/bar-1.tar.gz
Updated foo from 1 to 2
Updates to lock input requirements:
Updated 'foo' to 'foo>1'
"""
).format(platform=lock_file.locked_resolves[0].platform_tag)
),
)
lock_file = json_codec.load(initial_lock, path_mappings=path_mappings)
assert_lock(
lock_file,
path_mappings,
expected_pins=[pin("cowsay", "5.0"), pin("foo", "2"), pin("bar", "1")],
)
assert lock_file.style is LockStyle.SOURCES


@pytest.fixture
def initial_venv(
tmpdir, # type: Any
Expand Down Expand Up @@ -850,7 +901,6 @@ def test_sync_venv_run_retain_user_pip(
requirements,
"--constraints",
constraints,
"cowsay<6.1",
"pip",
"--lock",
lock,
Expand Down

0 comments on commit 120fe0d

Please sign in to comment.