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

Consider installed packages during resolution #2596

Merged
merged 47 commits into from
Mar 28, 2024
Merged

Conversation

zanieb
Copy link
Member

@zanieb zanieb commented Mar 21, 2024

Previously, we did not consider installed distributions as candidates while performing resolution. Here, we update the resolver to use installed distributions that satisfy requirements instead of pulling new distributions from the registry.

The implementation details are as follows:

  • We now provide SitePackages to the CandidateSelector
  • If an installed distribution satisfies the requirement, we prefer it over remote distributions
  • We do not want to allow installed distributions in some cases, i.e., upgrade and reinstall
    • We address this by introducing an Exclusions type which tracks installed packages to ignore during selection
  • There's a new ResolvedDist wrapper with Installed(InstalledDist) and Installable(Dist) variants
    • This lets us pass already installed distributions throughout the resolver

The user-facing behavior is thoroughly covered in the tests, but briefly:

  • Installing a package that depends on an already-installed package prefers the local version over the index
  • Installing a package with a name that matches an already-installed URL package does not reinstall from the index
  • Reinstalling (--reinstall) a package by name will pull from the index even if an already-installed URL package is present
    • To reinstall the URL package, you must specify the URL in the request

Closes #1661

Addresses:

Test plan

  • Reproduction at charlesnicholson/uv-pep420-bug passes
  • Unit test for editable package (#1476)
  • Unit test for previously installed package with empty registry
  • Unit test for local non-editable package
  • Unit test for new version available locally but not in registry (#2093)
  • [ ] Unit test for wheel not available in registry but already installed locally (#2282) (seems complicated and not worthwhile)
  • Unit test for install from URL dependency then with matching version (#2383)
  • Unit test for install of new package that depends on installed package does not change version (#2560)
  • Unit test that pip compile does not consider installed packages

@zanieb zanieb force-pushed the zb/installed-resolve branch 2 times, most recently from 4bd3f16 to dbe5cf9 Compare March 25, 2024 20:43
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this generally looks good!

crates/distribution-types/src/resolution.rs Outdated Show resolved Hide resolved
crates/distribution-types/src/resolved.rs Outdated Show resolved Hide resolved
crates/uv-resolver/src/resolver/mod.rs Show resolved Hide resolved
zanieb added a commit that referenced this pull request Mar 26, 2024
This is driving me a little crazy and is becoming a larger problem in
#2596 where I need to move more types (like `Upgrade` and `Reinstall`)
into this crate. Anything that's shared across our core resolver,
install, and build crates needs to be defined in this crate to avoid
cyclic dependencies. We've outgrown it being a single file with some
shared traits.

There are no behavioral changes here.
@zanieb zanieb force-pushed the zb/installed-resolve branch 5 times, most recently from 48e4bf0 to 18f36d6 Compare March 27, 2024 16:02
@zanieb zanieb marked this pull request as ready for review March 27, 2024 16:38
crates/uv-dispatch/src/lib.rs Outdated Show resolved Hide resolved
@@ -613,26 +634,23 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
.ok_or(ResolveError::Unregistered)?;
self.visited.insert(package_name.clone());

let empty_version_map = VersionMap::default();
Copy link
Member Author

@zanieb zanieb Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to initialize this for lifetimes since version_map is a reference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could have CandidateSelector.select take an Option<&VersionMap> but I opted for an empty iterable to avoid nesting in select instead. I'd consider revisiting this in the future as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do let empty_version_map; here and then empty_version_map = VersionMap::default() in each branch if you want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that could be nicer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually idk if that makes sense we're trying to return &VersionMap::default() in the match which is the problem?

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

- Displays missing packages as single-line warnings.
- Adds support for `Editable project location` and `Required-by` fields
in `pip show`.

Part of #2526.

# Conflicts:
#	crates/uv-dispatch/src/lib.rs
#	crates/uv-resolver/src/resolver/mod.rs
#	crates/uv-resolver/tests/resolver.rs
#	crates/uv-traits/src/lib.rs
#	crates/uv/src/commands/pip_compile.rs

# Conflicts:
#	crates/uv/src/commands/pip_sync.rs
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally looks great to me.

crates/uv-dispatch/src/lib.rs Outdated Show resolved Hide resolved
crates/uv-installer/src/plan.rs Outdated Show resolved Hide resolved
crates/uv-resolver/src/candidate_selector.rs Outdated Show resolved Hide resolved
crates/uv-resolver/src/exclusions.rs Show resolved Hide resolved
crates/uv-resolver/src/version_map.rs Outdated Show resolved Hide resolved
crates/uv-resolver/src/resolver/mod.rs Show resolved Hide resolved
crates/uv-resolver/src/resolver/mod.rs Outdated Show resolved Hide resolved
@@ -613,26 +634,23 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
.ok_or(ResolveError::Unregistered)?;
self.visited.insert(package_name.clone());

let empty_version_map = VersionMap::default();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do let empty_version_map; here and then empty_version_map = VersionMap::default() in each branch if you want.

&& self
.installed_packages
.get_packages(package_name)
.is_empty()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so but want to confirm: do you have any tests in which a package is already installed, but doesn't exist on the registry? I.e., exercising true for self.unavailable_packages.get(package_name).is_some() and false for the latter condition.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes definitely.

.installed_packages
.get_packages(package_name)
.is_empty()
{
debug_assert!(
false,
"Dependencies were requested for a package that is not available"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we even get here btw?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a dumb question since there's a debug_assert! here anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah if a version map cannot be retrieved for a package we track the package in unavailable_packages but that's no longer the sole way to have a candidate for a package so this is kind of a lazy way of allowing installed packages dependencies to be retrieved even if they were not available in the index.

# Conflicts:
#	crates/distribution-types/src/lib.rs
#	crates/uv-dev/src/install_many.rs
#	crates/uv-resolver/src/finder.rs
#	crates/uv-resolver/src/lib.rs
#	crates/uv-resolver/src/manifest.rs
#	crates/uv-resolver/tests/resolver.rs
#	crates/uv/src/commands/pip_compile.rs
#	crates/uv/src/commands/pip_install.rs
#	crates/uv/src/commands/pip_sync.rs
#	crates/uv/src/commands/reporters.rs
#	crates/uv/tests/pip_sync.rs
@zanieb
Copy link
Member Author

zanieb commented Mar 28, 2024

I've addressed all the feedback from the review.

@zanieb zanieb added the enhancement New feature or improvement to existing functionality label Mar 28, 2024
@zanieb zanieb merged commit e1878c8 into main Mar 28, 2024
31 checks passed
@zanieb zanieb deleted the zb/installed-resolve branch March 28, 2024 18:49
zanieb added a commit that referenced this pull request Apr 2, 2024
…tion in the resolver (#2779)

Addresses panic introduced in #2596 and reported in
#2763 (comment)

When there are multiple versions of a package available, we remove the
existing packages before installing the resolved version to "fix" the
environment. We must remove all of the package versions and reinstall
because removing _any_ of the package versions could break the others.
Since reinstalls require a pull from the remote, this broke a contract
between the resolver and the planner which must always agree on which
packages should come from the remote. This further demonstrates that we
should be constructing the install plan with more concrete knowledge
from the resolver (i.e. `ResolvedDist` instead of `Requirement`) to
avoid having to manually ensure logic matches.

## Test plan

Fails on `main` with panic succeeds on branch

```
uv venv --seed
source .venv/bin/activate
pip install anyio==3.7.0 --ignore-installed
pip install anyio==4.0.0 --ignore-installed
cargo run -- pip install anyio black -v
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improvement to existing functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

uv pip install fails for dependencies on already-installed local packages
3 participants