Skip to content

Commit

Permalink
Full support for standard CLI reqs / constraints forms.
Browse files Browse the repository at this point in the history
  • Loading branch information
jsirois committed Feb 20, 2024
1 parent 9ca9141 commit bab2f79
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 100 deletions.
182 changes: 98 additions & 84 deletions pex/cli/commands/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from pex.pep_427 import InstallableType
from pex.pep_440 import Version
from pex.pep_503 import ProjectName
from pex.requirements import PyPIRequirement, URLRequirement, VCSRequirement
from pex.resolve import requirement_options, resolver_options, target_options
from pex.resolve.config import finalize as finalize_resolve_config
from pex.resolve.configured_resolver import ConfiguredResolver
Expand All @@ -39,6 +38,7 @@
ArtifactUpdate,
DeleteUpdate,
FingerprintUpdate,
LockUpdate,
LockUpdater,
ResolveUpdateRequest,
VersionUpdate,
Expand Down Expand Up @@ -300,6 +300,66 @@ def sync(
return Ok()


@attr.s(frozen=True)
class LockUpdateRequest(object):
_lock_file_path = attr.ib() # type: str
_lock_updater = attr.ib() # type: LockUpdater
_update_requests = attr.ib() # type: Iterable[ResolveUpdateRequest]

def update(
self,
updates=(), # type: Iterable[Requirement]
replacements=(), # type: Iterable[Requirement]
deletes=(), # type: Iterable[ProjectName]
pin=False, # type: bool
):
# type: (...) -> Union[LockUpdate, Result]
if not self._update_requests:
return self._no_updates()

return self._lock_updater.update(
update_requests=self._update_requests,
updates=updates,
replacements=replacements,
deletes=deletes,
pin=pin,
)

def sync(
self,
requirement_configuration, # type: RequirementConfiguration
pin=False, # type: bool
):
# type: (...) -> Union[LockUpdate, Result]
if not self._update_requests:
return self._no_updates()

return self._lock_updater.sync(
update_requests=self._update_requests,
requirement_configuration=requirement_configuration,
pin=pin,
)

def _no_updates(self):
# type: () -> Ok
return Ok(
"No lock update was performed.\n"
"The following platforms present in {lock_file} were not found on the local "
"machine:\n"
"{missing_platforms}\n"
"You might still be able to update the lock by adjusting target options like "
"--python-path.".format(
lock_file=self._lock_file_path,
missing_platforms="\n".join(
sorted(
"+ {platform}".format(platform=locked_resolve.platform_tag)
for locked_resolve in self._lock_updater.lock_file.locked_resolves
)
),
)
)


class Lock(OutputMixin, JsonMixin, BuildTimeCommand):
"""Operate on PEX lock files."""

Expand Down Expand Up @@ -890,21 +950,12 @@ def _export_subset(self):
requirement_configuration = requirement_options.configure(self.options)
return self._export(requirement_configuration=requirement_configuration)

def _update_lock(
def _create_lock_update_request(
self,
lock_file_path, # type: str
lock_file, # type: Lockfile
update_requirements=(), # type: Iterable[Requirement]
replace_requirements=(), # type: Iterable[Requirement]
delete_projects=(), # type: Iterable[ProjectName]
pin=False, # type: bool
):
# type: (...) -> Result

update_requirements_by_project_name = OrderedDict(
(update_requirement.project_name, update_requirement)
for update_requirement in update_requirements
) # type: OrderedDict[ProjectName, Requirement]
# type: (...) -> Union[LockUpdateRequest, Error]

network_configuration = resolver_options.create_network_configuration(self.options)
lock_updater = LockUpdater.create(
Expand Down Expand Up @@ -972,33 +1023,15 @@ def _update_lock(
)
)

if not update_requests:
return Ok(
"No lock update was performed.\n"
"The following platforms present in {lock_file} were not found on the local "
"machine:\n"
"{missing_platforms}\n"
"You might still be able to update the lock by adjusting target options like "
"--python-path.".format(
lock_file=lock_file_path,
missing_platforms="\n".join(
sorted(
"+ {platform}".format(platform=locked_resolve.platform_tag)
for locked_resolve in lock_file.locked_resolves
)
),
)
)
return LockUpdateRequest(lock_file_path, lock_updater, update_requests)

lock_update = try_(
lock_updater.update(
update_requests=update_requests,
updates=update_requirements_by_project_name.values(),
replacements=replace_requirements,
deletes=delete_projects,
pin=pin,
)
)
def _process_lock_update(
self,
lock_update, # type: LockUpdate
lock_file, # type: Lockfile
lock_file_path, # type: str
):
# type: (...) -> Result

original_requirements_by_project_name = OrderedDict(
(requirement.project_name, requirement) for requirement in lock_file.requirements
Expand Down Expand Up @@ -1062,7 +1095,7 @@ def _update_lock(
)
constraints_by_project_name.pop(project_name, None)
elif isinstance(update, VersionUpdate):
update_req = update_requirements_by_project_name.get(project_name)
update_req = lock_update.update_requirements_by_project_name.get(project_name)
if update.original:
print(
" {lead_in} {project_name} from {original_version} to "
Expand Down Expand Up @@ -1285,14 +1318,19 @@ def _update(self):
return Error("Failed to parse project name to delete: {err}".format(err=e))

lock_file_path, lock_file = self._load_lockfile()
return self._update_lock(
lock_file_path=lock_file_path,
lock_file=lock_file,
update_requirements=update_requirements,
replace_requirements=replace_requirements,
delete_projects=delete_projects,
lock_update_request = try_(
self._create_lock_update_request(lock_file_path=lock_file_path, lock_file=lock_file)
)
lock_update = lock_update_request.update(
updates=update_requirements,
replacements=replace_requirements,
deletes=delete_projects,
pin=pin,
)
if isinstance(lock_update, Result):
return lock_update

return self._process_lock_update(lock_update, lock_file, lock_file_path)

def _sync(self):
# type: () -> Result
Expand All @@ -1308,45 +1346,21 @@ def _sync(self):
production_assert(isinstance(resolver_configuration, LockRepositoryConfiguration))
pip_configuration = resolver_configuration.pip_configuration

if os.path.exists(self.options.lock):
lock_file = try_(parse_lockfile(self.options, lock_file_path=self.options.lock))
original_requirements_by_project_name = {
requirement.project_name: requirement for requirement in lock_file.requirements
}

replace_requirements = [] # type: List[Requirement]
parsed_requirements = requirement_configuration.parse_requirements(
network_configuration=pip_configuration.network_configuration
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))
lock_update_request = try_(
self._create_lock_update_request(lock_file_path=lock_file_path, lock_file=lock_file)
)
for parsed_requirement in parsed_requirements:
if isinstance(
parsed_requirement, (PyPIRequirement, URLRequirement, VCSRequirement)
):
original_requirement = original_requirements_by_project_name.pop(
parsed_requirement.requirement.project_name, None
)
if parsed_requirement.requirement != original_requirement:
replace_requirements.append(parsed_requirement.requirement)
else:
return Error(
"Cannot update a bare local project directory requirement on {path}.\n"
"Try re-phrasing as a PEP-508 direct reference with a file:// URL.\n"
"See: https://peps.python.org/pep-0508/".format(
path=parsed_requirement.path
)
)
delete_projects = (
tuple(original_requirements_by_project_name) if parsed_requirements else ()

pin = getattr(self.options, "pin", False)
lock_update = lock_update_request.sync(
requirement_configuration=requirement_configuration, pin=pin
)
if any((replace_requirements, delete_projects)):
try_(
self._update_lock(
lock_file_path=self.options.lock,
lock_file=lock_file,
replace_requirements=replace_requirements,
delete_projects=delete_projects,
)
)
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:
Expand Down Expand Up @@ -1382,7 +1396,7 @@ def _sync(self):
pip_configuration=pip_configuration,
)
)
with safe_open(self.options.lock, "w") as fp:
with safe_open(lock_file_path, "w") as fp:
self._dump_lockfile(lockfile, output=fp)

sync_target = None # type: Optional[SyncTarget]
Expand Down Expand Up @@ -1424,7 +1438,7 @@ def _sync(self):
if not sync_target:
return Ok()

lock_file = try_(parse_lockfile(self.options, lock_file_path=self.options.lock))
lock_file = try_(parse_lockfile(self.options, lock_file_path=lock_file_path))
target = LocalInterpreter.create(sync_target.venv.interpreter)
resolve_result = try_(
resolve_from_lock(
Expand Down
Loading

0 comments on commit bab2f79

Please sign in to comment.