mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-02 21:02:37 +00:00
Accept file:// URLs for requirements.txt et all references (#4145)
## Summary Closes https://github.com/astral-sh/uv/issues/4124.
This commit is contained in:
parent
e3b274413d
commit
d7cc622d6c
3 changed files with 104 additions and 14 deletions
|
|
@ -262,6 +262,7 @@ impl RequirementsTxt {
|
|||
read_url_to_string(&requirements_txt, client).await
|
||||
}
|
||||
} else {
|
||||
// Ex) `file:///home/ferris/project/requirements.txt`
|
||||
uv_fs::read_to_string_transcode(&requirements_txt)
|
||||
.await
|
||||
.map_err(RequirementsTxtParserError::IO)
|
||||
|
|
@ -321,6 +322,22 @@ impl RequirementsTxt {
|
|||
let sub_file =
|
||||
if filename.starts_with("http://") || filename.starts_with("https://") {
|
||||
PathBuf::from(filename.as_ref())
|
||||
} else if filename.starts_with("file://") {
|
||||
requirements_txt.join(
|
||||
Url::parse(filename.as_ref())
|
||||
.map_err(|err| RequirementsTxtParserError::Url {
|
||||
source: err,
|
||||
url: filename.to_string(),
|
||||
start,
|
||||
end,
|
||||
})?
|
||||
.to_file_path()
|
||||
.map_err(|()| RequirementsTxtParserError::FileUrl {
|
||||
url: filename.to_string(),
|
||||
start,
|
||||
end,
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
requirements_dir.join(filename.as_ref())
|
||||
};
|
||||
|
|
@ -360,6 +377,22 @@ impl RequirementsTxt {
|
|||
let sub_file =
|
||||
if filename.starts_with("http://") || filename.starts_with("https://") {
|
||||
PathBuf::from(filename.as_ref())
|
||||
} else if filename.starts_with("file://") {
|
||||
requirements_txt.join(
|
||||
Url::parse(filename.as_ref())
|
||||
.map_err(|err| RequirementsTxtParserError::Url {
|
||||
source: err,
|
||||
url: filename.to_string(),
|
||||
start,
|
||||
end,
|
||||
})?
|
||||
.to_file_path()
|
||||
.map_err(|()| RequirementsTxtParserError::FileUrl {
|
||||
url: filename.to_string(),
|
||||
start,
|
||||
end,
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
requirements_dir.join(filename.as_ref())
|
||||
};
|
||||
|
|
@ -815,6 +848,11 @@ pub enum RequirementsTxtParserError {
|
|||
start: usize,
|
||||
end: usize,
|
||||
},
|
||||
FileUrl {
|
||||
url: String,
|
||||
start: usize,
|
||||
end: usize,
|
||||
},
|
||||
VerbatimUrl {
|
||||
source: pep508_rs::VerbatimUrlError,
|
||||
url: String,
|
||||
|
|
@ -882,6 +920,9 @@ impl Display for RequirementsTxtParserError {
|
|||
Self::Url { url, start, .. } => {
|
||||
write!(f, "Invalid URL at position {start}: `{url}`")
|
||||
}
|
||||
Self::FileUrl { url, start, .. } => {
|
||||
write!(f, "Invalid file URL at position {start}: `{url}`")
|
||||
}
|
||||
Self::VerbatimUrl { source, url } => {
|
||||
write!(f, "Invalid URL: `{url}`: {source}")
|
||||
}
|
||||
|
|
@ -945,6 +986,7 @@ impl std::error::Error for RequirementsTxtParserError {
|
|||
match &self {
|
||||
Self::IO(err) => err.source(),
|
||||
Self::Url { source, .. } => Some(source),
|
||||
Self::FileUrl { .. } => None,
|
||||
Self::VerbatimUrl { source, .. } => Some(source),
|
||||
Self::UrlConversion(_) => None,
|
||||
Self::UnsupportedUrl(_) => None,
|
||||
|
|
@ -976,6 +1018,13 @@ impl Display for RequirementsTxtFileError {
|
|||
self.file.user_display(),
|
||||
)
|
||||
}
|
||||
RequirementsTxtParserError::FileUrl { url, start, .. } => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid file URL in `{}` at position {start}: `{url}`",
|
||||
self.file.user_display(),
|
||||
)
|
||||
}
|
||||
RequirementsTxtParserError::VerbatimUrl { url, .. } => {
|
||||
write!(f, "Invalid URL in `{}`: `{url}`", self.file.user_display())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,15 +253,30 @@ fn parse_index_url(input: &str) -> Result<Maybe<IndexUrl>, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse a string into a [`PathBuf`]. The string can represent a file, either as a path or a
|
||||
/// `file://` URL.
|
||||
fn parse_file_path(input: &str) -> Result<PathBuf, String> {
|
||||
if input.starts_with("file://") {
|
||||
let url = match url::Url::from_str(input) {
|
||||
Ok(url) => url,
|
||||
Err(err) => return Err(err.to_string()),
|
||||
};
|
||||
url.to_file_path()
|
||||
.map_err(|()| "invalid file URL".to_string())
|
||||
} else {
|
||||
match PathBuf::from_str(input) {
|
||||
Ok(path) => Ok(path),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a string into a [`PathBuf`], mapping the empty string to `None`.
|
||||
fn parse_file_path(input: &str) -> Result<Maybe<PathBuf>, String> {
|
||||
fn parse_maybe_file_path(input: &str) -> Result<Maybe<PathBuf>, String> {
|
||||
if input.is_empty() {
|
||||
Ok(Maybe::None)
|
||||
} else {
|
||||
match PathBuf::from_str(input) {
|
||||
Ok(path) => Ok(Maybe::Some(path)),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
parse_file_path(input).map(Maybe::Some)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,7 +286,7 @@ pub(crate) struct PipCompileArgs {
|
|||
/// Include all packages listed in the given `requirements.in` files.
|
||||
///
|
||||
/// When the path is `-`, then requirements are read from stdin.
|
||||
#[arg(required(true))]
|
||||
#[arg(required(true), value_parser = parse_file_path)]
|
||||
pub(crate) src_file: Vec<PathBuf>,
|
||||
|
||||
/// Constrain versions using the given requirements files.
|
||||
|
|
@ -281,7 +296,7 @@ pub(crate) struct PipCompileArgs {
|
|||
/// trigger the installation of that package.
|
||||
///
|
||||
/// This is equivalent to pip's `--constraint` option.
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_file_path)]
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
|
||||
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||
|
||||
/// Override versions using the given requirements files.
|
||||
|
|
@ -293,7 +308,7 @@ pub(crate) struct PipCompileArgs {
|
|||
/// While constraints are _additive_, in that they're combined with the requirements of the
|
||||
/// constituent packages, overrides are _absolute_, in that they completely replace the
|
||||
/// requirements of the constituent packages.
|
||||
#[arg(long)]
|
||||
#[arg(long, value_parser = parse_file_path)]
|
||||
pub(crate) r#override: Vec<PathBuf>,
|
||||
|
||||
/// Include optional dependencies in the given extra group name; may be provided more than once.
|
||||
|
|
@ -593,7 +608,7 @@ pub(crate) struct PipCompileArgs {
|
|||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub(crate) struct PipSyncArgs {
|
||||
/// Include all packages listed in the given `requirements.txt` files.
|
||||
#[arg(required(true))]
|
||||
#[arg(required(true), value_parser = parse_file_path)]
|
||||
pub(crate) src_file: Vec<PathBuf>,
|
||||
|
||||
/// Constrain versions using the given requirements files.
|
||||
|
|
@ -603,7 +618,7 @@ pub(crate) struct PipSyncArgs {
|
|||
/// trigger the installation of that package.
|
||||
///
|
||||
/// This is equivalent to pip's `--constraint` option.
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_file_path)]
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
|
||||
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||
|
||||
/// Reinstall all packages, regardless of whether they're already installed.
|
||||
|
|
@ -892,7 +907,7 @@ pub(crate) struct PipInstallArgs {
|
|||
pub(crate) package: Vec<String>,
|
||||
|
||||
/// Install all packages listed in the given requirements files.
|
||||
#[arg(long, short, group = "sources")]
|
||||
#[arg(long, short, group = "sources", value_parser = parse_file_path)]
|
||||
pub(crate) requirement: Vec<PathBuf>,
|
||||
|
||||
/// Install the editable package based on the provided local file path.
|
||||
|
|
@ -906,7 +921,7 @@ pub(crate) struct PipInstallArgs {
|
|||
/// trigger the installation of that package.
|
||||
///
|
||||
/// This is equivalent to pip's `--constraint` option.
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_file_path)]
|
||||
#[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
|
||||
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||
|
||||
/// Override versions using the given requirements files.
|
||||
|
|
@ -918,7 +933,7 @@ pub(crate) struct PipInstallArgs {
|
|||
/// While constraints are _additive_, in that they're combined with the requirements of the
|
||||
/// constituent packages, overrides are _absolute_, in that they completely replace the
|
||||
/// requirements of the constituent packages.
|
||||
#[arg(long)]
|
||||
#[arg(long, value_parser = parse_file_path)]
|
||||
pub(crate) r#override: Vec<PathBuf>,
|
||||
|
||||
/// Include optional dependencies in the given extra group name; may be provided more than once.
|
||||
|
|
@ -1259,7 +1274,7 @@ pub(crate) struct PipUninstallArgs {
|
|||
pub(crate) package: Vec<String>,
|
||||
|
||||
/// Uninstall all packages listed in the given requirements files.
|
||||
#[arg(long, short, group = "sources")]
|
||||
#[arg(long, short, group = "sources", value_parser = parse_file_path)]
|
||||
pub(crate) requirement: Vec<PathBuf>,
|
||||
|
||||
/// The Python interpreter from which packages should be uninstalled.
|
||||
|
|
|
|||
|
|
@ -9629,3 +9629,29 @@ fn dynamic_pyproject_toml() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accept `file://` URLs as installation sources.
|
||||
#[test]
|
||||
fn file_url() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let requirements_txt = context.temp_dir.child("requirements file.txt");
|
||||
requirements_txt.write_str("iniconfig")?;
|
||||
|
||||
let url = Url::from_file_path(requirements_txt.simple_canonicalize()?).expect("valid file URL");
|
||||
|
||||
uv_snapshot!(context.filters(), context.compile().arg(url.to_string()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z file://[TEMP_DIR]/requirements%20file.txt
|
||||
iniconfig==2.0.0
|
||||
# via -r requirements file.txt
|
||||
|
||||
----- stderr -----
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue