Skip to content

Commit

Permalink
refinements to update -- determine which packages may need upgrading …
Browse files Browse the repository at this point in the history
…and don't require lockfile to exist
  • Loading branch information
matteius committed Oct 30, 2024
1 parent 7984148 commit e5bc9c6
Showing 1 changed file with 110 additions and 42 deletions.
152 changes: 110 additions & 42 deletions pipenv/routines/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from pipenv.routines.outdated import do_outdated
from pipenv.routines.sync import do_sync
from pipenv.utils import err
from pipenv.utils.constants import VCS_LIST
from pipenv.utils.dependencies import (
expansive_install_req_from_line,
get_lockfile_section_using_pipfile_category,
get_pipfile_category_using_lockfile_section,
)
from pipenv.utils.processes import run_command
Expand Down Expand Up @@ -59,16 +61,17 @@ def do_update(

if not outdated:
# Pre-sync packages for pipdeptree resolution to avoid conflicts
do_sync(
project,
dev=dev,
categories=categories,
python=python,
bare=bare,
clear=clear,
pypi_mirror=pypi_mirror,
extra_pip_args=extra_pip_args,
)
if project.lockfile_exists:
do_sync(
project,
dev=dev,
categories=categories,
python=python,
bare=bare,
clear=clear,
pypi_mirror=pypi_mirror,
extra_pip_args=extra_pip_args,
)
upgrade(
project,
pre=pre,
Expand Down Expand Up @@ -173,6 +176,57 @@ def check_version_conflicts(
return conflicts


def get_modified_pipfile_entries(project, pipfile_categories):
"""
Detect Pipfile entries that have been modified since the last lock.
Returns a dict mapping categories to sets of modified package names.
"""
modified = defaultdict(set)

lockfile = project.lockfile()

for pipfile_category in pipfile_categories:
lockfile_category = get_lockfile_section_using_pipfile_category(pipfile_category)
pipfile_packages = project.parsed_pipfile.get(pipfile_category, {})
locked_packages = lockfile.get(lockfile_category, {})

for package_name, pipfile_entry in pipfile_packages.items():
# Check if package exists in lockfile
if package_name not in locked_packages:
modified[lockfile_category].add(package_name)
continue

locked_entry = locked_packages.get(package_name)
# Compare version specs
pipfile_version = (
str(pipfile_entry)
if isinstance(pipfile_entry, str)
else pipfile_entry.get("version")
)
if locked_entry:
locked_version = locked_entry.get("version", "")
# We might want to eventually improve this check to consider full constraints but for now ...
if pipfile_version == locked_version:
continue
modified[lockfile_category].add(package_name)
else: # Hasn't been added to lock yet
modified[lockfile_category].add(package_name)

# Handle VCS/path dependencies
vcs_keys = ["ref", "subdirectory"]
vcs_keys.extend(VCS_LIST)
has_vcs_changed = any(
pipfile_entry.get(key) != locked_entry.get(key)
for key in vcs_keys
if isinstance(pipfile_entry, dict) and key in pipfile_entry
)

if has_vcs_changed:
modified[lockfile_category].add(package_name)

return modified


def upgrade(
project,
pre=False,
Expand Down Expand Up @@ -245,10 +299,17 @@ def upgrade(
)
sys.exit(1)

has_package_args = False
if package_args:
has_package_args = True

requested_install_reqs = defaultdict(dict)
requested_packages = defaultdict(dict)
for category in categories:
pipfile_category = get_pipfile_category_using_lockfile_section(category)
if not package_args and project.lockfile_exists:
modified_entries = get_modified_pipfile_entries(project, [pipfile_category])
package_args = list(modified_entries[category])

for package in package_args[:]:
install_req, _ = expansive_install_req_from_line(package, expand_env=True)
Expand Down Expand Up @@ -293,28 +354,27 @@ def upgrade(
f"Unable to parse version specifier for {dependency}: {str(e)}"
)

if not package_args:
err.print("Nothing to upgrade!")
return
else:
# When packages are not provided we simply perform full resolution
upgrade_lock_data = None
if package_args:
err.print(
f"[bold][green]Upgrading[/bold][/green] {', '.join(package_args)} in [{category}] dependencies."
)

# Resolve package to generate constraints of new package data
upgrade_lock_data = venv_resolve_deps(
requested_packages[pipfile_category],
which=project._which,
project=project,
lockfile={},
pipfile_category=pipfile_category,
pre=pre,
allow_global=system,
pypi_mirror=pypi_mirror,
)
if not upgrade_lock_data:
err.print("Nothing to upgrade!")
return
# Resolve package to generate constraints of new package data
upgrade_lock_data = venv_resolve_deps(
requested_packages[pipfile_category],
which=project._which,
project=project,
lockfile={},
pipfile_category=pipfile_category,
pre=pre,
allow_global=system,
pypi_mirror=pypi_mirror,
)
if not upgrade_lock_data:
err.print("Nothing to upgrade!")
return

complete_packages = project.parsed_pipfile.get(pipfile_category, {})

Expand All @@ -329,21 +389,29 @@ def upgrade(
pypi_mirror=pypi_mirror,
)

# Verify no conflicts were introduced during resolution
for package_name, package_data in full_lock_resolution.items():
if package_name in upgrade_lock_data:
version = package_data.get("version", "").replace("==", "")
if not version:
# Either vcs or file package
continue
if upgrade_lock_data is None:
for package_name, package_data in full_lock_resolution.items():
lockfile[category][package_name] = package_data
else: # Upgrade a subset of packages
# Verify no conflicts were introduced during resolution
for package_name, package_data in full_lock_resolution.items():
if package_name in upgrade_lock_data:
version = package_data.get("version", "").replace("==", "")
if not version:
# Either vcs or file package
continue

# Update lockfile with verified resolution data
for package_name in upgrade_lock_data:
correct_package_lock = full_lock_resolution.get(package_name)
if correct_package_lock:
if category not in lockfile:
lockfile[category] = {}
lockfile[category][package_name] = correct_package_lock
# Update lockfile with verified resolution data
for package_name in upgrade_lock_data:
correct_package_lock = full_lock_resolution.get(package_name)
if correct_package_lock:
if category not in lockfile:
lockfile[category] = {}
lockfile[category][package_name] = correct_package_lock

# reset package args in case of multiple categories being processed
if has_package_args is False:
package_args = []

lockfile.update({"_meta": project.get_lockfile_meta()})
project.write_lockfile(lockfile)

0 comments on commit e5bc9c6

Please sign in to comment.