Skip to content

Commit 8eea470

Browse files
Bias towards local directories for bare editable requirements (#3995)
## Summary Given `install -e dagster`, we need to assume that the user meant `install -e ./dagster`, even though `install dagster` should _not_ be treated as `install ./dagster`. I suspect pip will change this in the future (since `pip install dagster` does _not_ meant `pip install ./dagster`) but for now it's what users expect. Closes #3994.
1 parent 420333a commit 8eea470

File tree

4 files changed

+62
-17
lines changed

4 files changed

+62
-17
lines changed

crates/requirements-txt/src/lib.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ use tracing::instrument;
4444
use unscanny::{Pattern, Scanner};
4545
use url::Url;
4646

47-
use crate::requirement::EditableError;
4847
use distribution_types::{UnresolvedRequirement, UnresolvedRequirementSpecification};
4948
use pep508_rs::{
5049
expand_env_vars, split_scheme, strip_host, Pep508Error, RequirementOrigin, Scheme, VerbatimUrl,
@@ -57,11 +56,12 @@ use uv_configuration::{NoBinary, NoBuild, PackageNameSpecifier};
5756
use uv_fs::{normalize_url_path, Simplified};
5857
use uv_warnings::warn_user;
5958

59+
use crate::requirement::EditableError;
6060
pub use crate::requirement::RequirementsTxtRequirement;
6161

6262
mod requirement;
6363

64-
/// We emit one of those for each requirements.txt entry
64+
/// We emit one of those for each `requirements.txt` entry.
6565
enum RequirementsTxtStatement {
6666
/// `-r` inclusion filename
6767
Requirements {
@@ -500,7 +500,8 @@ fn parse_entry(
500500
Some(requirements_txt)
501501
};
502502

503-
let (requirement, hashes) = parse_requirement_and_hashes(s, content, source, working_dir)?;
503+
let (requirement, hashes) =
504+
parse_requirement_and_hashes(s, content, source, working_dir, true)?;
504505
let requirement =
505506
requirement
506507
.into_editable()
@@ -579,7 +580,8 @@ fn parse_entry(
579580
Some(requirements_txt)
580581
};
581582

582-
let (requirement, hashes) = parse_requirement_and_hashes(s, content, source, working_dir)?;
583+
let (requirement, hashes) =
584+
parse_requirement_and_hashes(s, content, source, working_dir, false)?;
583585
RequirementsTxtStatement::RequirementEntry(RequirementEntry {
584586
requirement,
585587
hashes,
@@ -643,6 +645,7 @@ fn parse_requirement_and_hashes(
643645
content: &str,
644646
source: Option<&Path>,
645647
working_dir: &Path,
648+
editable: bool,
646649
) -> Result<(RequirementsTxtRequirement, Vec<String>), RequirementsTxtParserError> {
647650
// PEP 508 requirement
648651
let start = s.cursor();
@@ -699,7 +702,7 @@ fn parse_requirement_and_hashes(
699702
}
700703
}
701704

702-
let requirement = RequirementsTxtRequirement::parse(requirement, working_dir)
705+
let requirement = RequirementsTxtRequirement::parse(requirement, working_dir, editable)
703706
.map(|requirement| {
704707
if let Some(source) = source {
705708
requirement.with_origin(RequirementOrigin::File(source.to_path_buf()))

crates/requirements-txt/src/requirement.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,22 @@ impl RequirementsTxtRequirement {
119119
pub fn parse(
120120
input: &str,
121121
working_dir: impl AsRef<Path>,
122+
editable: bool,
122123
) -> Result<Self, Box<Pep508Error<VerbatimParsedUrl>>> {
123124
// Attempt to parse as a PEP 508-compliant requirement.
124125
match pep508_rs::Requirement::parse(input, &working_dir) {
125-
Ok(requirement) => Ok(Self::Named(requirement)),
126+
Ok(requirement) => {
127+
// As a special-case, interpret `dagster` as `./dagster` if we're in editable mode.
128+
if editable && requirement.version_or_url.is_none() {
129+
Ok(Self::Unnamed(UnnamedRequirement::parse(
130+
input,
131+
&working_dir,
132+
&mut TracingReporter,
133+
)?))
134+
} else {
135+
Ok(Self::Named(requirement))
136+
}
137+
}
126138
Err(err) => match err.message {
127139
Pep508ErrorSource::UnsupportedRequirement(_) => {
128140
// If that fails, attempt to parse as a direct URL requirement.

crates/uv-requirements/src/specification.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,18 @@ impl RequirementsSpecification {
8686
) -> Result<Self> {
8787
Ok(match source {
8888
RequirementsSource::Package(name) => {
89-
let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?)
90-
.with_context(|| format!("Failed to parse: `{name}`"))?;
89+
let requirement =
90+
RequirementsTxtRequirement::parse(name, std::env::current_dir()?, false)
91+
.with_context(|| format!("Failed to parse: `{name}`"))?;
9192
Self {
9293
requirements: vec![UnresolvedRequirementSpecification::from(requirement)],
9394
..Self::default()
9495
}
9596
}
9697
RequirementsSource::Editable(name) => {
97-
let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?)
98-
.with_context(|| format!("Failed to parse: `{name}`"))?;
98+
let requirement =
99+
RequirementsTxtRequirement::parse(name, std::env::current_dir()?, true)
100+
.with_context(|| format!("Failed to parse: `{name}`"))?;
99101
Self {
100102
requirements: vec![UnresolvedRequirementSpecification::from(
101103
requirement.into_editable()?,

crates/uv/tests/pip_install.rs

+35-7
Original file line numberDiff line numberDiff line change
@@ -1114,22 +1114,50 @@ fn install_editable_pep_508_cli() {
11141114
}
11151115

11161116
#[test]
1117-
fn invalid_editable_no_version() -> Result<()> {
1117+
fn install_editable_bare_cli() {
1118+
let context = TestContext::new("3.12");
1119+
1120+
let packages_dir = context.workspace_root.join("scripts/packages");
1121+
1122+
uv_snapshot!(context.filters(), context.install()
1123+
.arg("-e")
1124+
.arg("black_editable")
1125+
.current_dir(&packages_dir), @r###"
1126+
success: true
1127+
exit_code: 0
1128+
----- stdout -----
1129+
1130+
----- stderr -----
1131+
Resolved 1 package in [TIME]
1132+
Downloaded 1 package in [TIME]
1133+
Installed 1 package in [TIME]
1134+
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
1135+
"###
1136+
);
1137+
}
1138+
1139+
#[test]
1140+
fn install_editable_bare_requirements_txt() -> Result<()> {
11181141
let context = TestContext::new("3.12");
11191142

11201143
let requirements_txt = context.temp_dir.child("requirements.txt");
1121-
requirements_txt.write_str("-e black")?;
1144+
requirements_txt.write_str("-e black_editable")?;
1145+
1146+
let packages_dir = context.workspace_root.join("scripts/packages");
11221147

11231148
uv_snapshot!(context.filters(), context.install()
11241149
.arg("-r")
1125-
.arg("requirements.txt"), @r###"
1126-
success: false
1127-
exit_code: 2
1150+
.arg(requirements_txt.path())
1151+
.current_dir(&packages_dir), @r###"
1152+
success: true
1153+
exit_code: 0
11281154
----- stdout -----
11291155
11301156
----- stderr -----
1131-
error: Unsupported editable requirement in `requirements.txt`
1132-
Caused by: Editable `black` must refer to a local directory
1157+
Resolved 1 package in [TIME]
1158+
Downloaded 1 package in [TIME]
1159+
Installed 1 package in [TIME]
1160+
+ black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable)
11331161
"###
11341162
);
11351163

0 commit comments

Comments
 (0)