Skip to content

Commit 6392961

Browse files
Add support for extras in editable requirements (#1531)
## Summary If you're developing on a package like `attrs` locally, and it has a recursive extra like `attrs[dev]`, it turns out that we then try to find the `attrs` in `attrs[dev]` from the registry, rather than recognizing that it's part of the editable. This PR fixes the issue by making editables slightly more first-class throughout the resolver. Instead of mocking metadata, we explicitly check for extras in various places. Part of the problem here is that we treated editables as URL dependencies, but when we saw an _extra_ like `attrs[dev]`, we didn't map that back to the URL. So now, we treat them as registry dependencies, but with the appropriate guardrails throughout. Closes #1447. ## Test Plan - Cloned `attrs`. - Ran `cargo run venv && cargo run pip install -e ".[dev]" -v`.
1 parent 4e0b6f8 commit 6392961

File tree

4 files changed

+92
-37
lines changed

4 files changed

+92
-37
lines changed

crates/uv-resolver/src/resolution.rs

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ impl ResolutionGraph {
6565
match package {
6666
PubGrubPackage::Package(package_name, None, None) => {
6767
// Create the distribution.
68-
let pinned_package = pins
69-
.get(package_name, version)
70-
.expect("Every package should be pinned")
71-
.clone();
68+
let pinned_package = if let Some((editable, _)) = editables.get(package_name) {
69+
Dist::from_editable(package_name.clone(), editable.clone())?
70+
} else {
71+
pins.get(package_name, version)
72+
.expect("Every package should be pinned")
73+
.clone()
74+
};
7275

7376
// Add its hashes to the index.
7477
if let Some(versions_response) = packages.get(package_name) {
@@ -87,9 +90,7 @@ impl ResolutionGraph {
8790
}
8891
PubGrubPackage::Package(package_name, None, Some(url)) => {
8992
// Create the distribution.
90-
let pinned_package = if let Some((editable, _)) = editables.get(package_name) {
91-
Dist::from_editable(package_name.clone(), editable.clone())?
92-
} else {
93+
let pinned_package = {
9394
let url = redirects.get(url).map_or_else(
9495
|| url.clone(),
9596
|url| VerbatimUrl::unknown(url.value().clone()),
@@ -115,20 +116,35 @@ impl ResolutionGraph {
115116
PubGrubPackage::Package(package_name, Some(extra), None) => {
116117
// Validate that the `extra` exists.
117118
let dist = PubGrubDistribution::from_registry(package_name, version);
118-
let metadata = distributions
119-
.get(&dist.package_id())
120-
.expect("Every package should have metadata");
121119

122-
if !metadata.provides_extras.contains(extra) {
123-
let pinned_package = pins
124-
.get(package_name, version)
125-
.expect("Every package should be pinned")
126-
.clone();
120+
if let Some((_, metadata)) = editables.get(package_name) {
121+
if !metadata.provides_extras.contains(extra) {
122+
let pinned_package = pins
123+
.get(package_name, version)
124+
.expect("Every package should be pinned")
125+
.clone();
127126

128-
diagnostics.push(Diagnostic::MissingExtra {
129-
dist: pinned_package,
130-
extra: extra.clone(),
131-
});
127+
diagnostics.push(Diagnostic::MissingExtra {
128+
dist: pinned_package,
129+
extra: extra.clone(),
130+
});
131+
}
132+
} else {
133+
let metadata = distributions
134+
.get(&dist.package_id())
135+
.expect("Every package should have metadata");
136+
137+
if !metadata.provides_extras.contains(extra) {
138+
let pinned_package = pins
139+
.get(package_name, version)
140+
.expect("Every package should be pinned")
141+
.clone();
142+
143+
diagnostics.push(Diagnostic::MissingExtra {
144+
dist: pinned_package,
145+
extra: extra.clone(),
146+
});
147+
}
132148
}
133149
}
134150
PubGrubPackage::Package(package_name, Some(extra), Some(url)) => {

crates/uv-resolver/src/resolver/mod.rs

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,8 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
160160
// Determine all the editable requirements.
161161
let mut editables = FxHashMap::default();
162162
for (editable_requirement, metadata) in &manifest.editables {
163-
// Convert the editable requirement into a distribution.
164-
let dist = Dist::from_editable(metadata.name.clone(), editable_requirement.clone())
165-
.expect("This is a valid distribution");
166-
167-
// Mock editable responses.
168-
let package_id = dist.package_id();
169-
index.distributions.register(package_id.clone());
170-
index.distributions.done(package_id, metadata.clone());
171163
editables.insert(
172-
dist.name().clone(),
164+
metadata.name.clone(),
173165
(editable_requirement.clone(), metadata.clone()),
174166
);
175167
}
@@ -633,6 +625,16 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
633625
}
634626

635627
PubGrubPackage::Package(package_name, extra, None) => {
628+
// If the dist is an editable, return the version from the editable metadata.
629+
if let Some((_local, metadata)) = self.editables.get(package_name) {
630+
let version = metadata.version.clone();
631+
return if range.contains(&version) {
632+
Ok(Some(ResolverVersion::Available(version)))
633+
} else {
634+
Ok(None)
635+
};
636+
}
637+
636638
// Wait for the metadata to be available.
637639
let versions_response = self
638640
.index
@@ -798,19 +800,15 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
798800
// Add a dependency on each editable.
799801
for (editable, metadata) in self.editables.values() {
800802
constraints.insert(
801-
PubGrubPackage::Package(
802-
metadata.name.clone(),
803-
None,
804-
Some(editable.url().clone()),
805-
),
803+
PubGrubPackage::Package(metadata.name.clone(), None, None),
806804
Range::singleton(metadata.version.clone()),
807805
);
808806
for extra in &editable.extras {
809807
constraints.insert(
810808
PubGrubPackage::Package(
811809
metadata.name.clone(),
812810
Some(extra.clone()),
813-
Some(editable.url().clone()),
811+
None,
814812
),
815813
Range::singleton(metadata.version.clone()),
816814
);
@@ -830,7 +828,37 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
830828
return Ok(Dependencies::Available(DependencyConstraints::default()));
831829
}
832830

833-
// Determine the distribution to lookup
831+
// Determine if the distribution is editable.
832+
if let Some((_local, metadata)) = self.editables.get(package_name) {
833+
let mut constraints = PubGrubDependencies::from_requirements(
834+
&metadata.requires_dist,
835+
&self.constraints,
836+
&self.overrides,
837+
Some(package_name),
838+
extra.as_ref(),
839+
self.markers,
840+
)?;
841+
842+
for (package, version) in constraints.iter() {
843+
debug!("Adding transitive dependency: {package}{version}");
844+
845+
// Emit a request to fetch the metadata for this package.
846+
self.visit_package(package, priorities, request_sink)
847+
.await?;
848+
}
849+
850+
// If a package has an extra, insert a constraint on the base package.
851+
if extra.is_some() {
852+
constraints.insert(
853+
PubGrubPackage::Package(package_name.clone(), None, None),
854+
Range::singleton(version.clone()),
855+
);
856+
}
857+
858+
return Ok(Dependencies::Available(constraints.into()));
859+
}
860+
861+
// Determine the distribution to lookup.
834862
let dist = match url {
835863
Some(url) => PubGrubDistribution::from_url(package_name, url),
836864
None => PubGrubDistribution::from_registry(package_name, version),
@@ -984,6 +1012,11 @@ impl<'a, Provider: ResolverProvider> Resolver<'a, Provider> {
9841012

9851013
// Pre-fetch the package and distribution metadata.
9861014
Request::Prefetch(package_name, range) => {
1015+
// Ignore editables.
1016+
if self.editables.contains_key(&package_name) {
1017+
return Ok(None);
1018+
}
1019+
9871020
// Wait for the package metadata to become available.
9881021
let versions_response = self
9891022
.index

crates/uv/tests/pip_compile.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2075,7 +2075,7 @@ fn compile_editable() -> Result<()> {
20752075
requirements_in.write_str(indoc! {r"
20762076
-e ../../scripts/editable-installs/poetry_editable
20772077
-e ${PROJECT_ROOT}/../../scripts/editable-installs/maturin_editable
2078-
-e file://../../scripts/editable-installs/black_editable[d]
2078+
-e file://../../scripts/editable-installs/black_editable[dev]
20792079
boltons # normal dependency for comparison
20802080
"
20812081
})?;
@@ -2122,12 +2122,14 @@ fn compile_editable() -> Result<()> {
21222122
# yarl
21232123
numpy==1.26.2
21242124
# via poetry-editable
2125+
uvloop==0.19.0
2126+
# via black
21252127
yarl==1.9.2
21262128
# via aiohttp
21272129
21282130
----- stderr -----
21292131
Built 3 editables in [TIME]
2130-
Resolved 12 packages in [TIME]
2132+
Resolved 13 packages in [TIME]
21312133
"###);
21322134

21332135
Ok(())

scripts/editable-installs/black_editable/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ jupyter = [
2020
"ipython>=7.8.0",
2121
"tokenize-rt>=3.2.0",
2222
]
23+
dev = [
24+
"black[d]",
25+
"black[uvloop]",
26+
]
2327

2428
[build-system]
2529
requires = ["flit_core>=3.4,<4"]

0 commit comments

Comments
 (0)