mirror of https://github.com/astral-sh/uv
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
|
|
@ -262,6 +262,7 @@ impl RequirementsTxt {
|
||||||
read_url_to_string(&requirements_txt, client).await
|
read_url_to_string(&requirements_txt, client).await
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Ex) `file:///home/ferris/project/requirements.txt`
|
||||||
uv_fs::read_to_string_transcode(&requirements_txt)
|
uv_fs::read_to_string_transcode(&requirements_txt)
|
||||||
.await
|
.await
|
||||||
.map_err(RequirementsTxtParserError::IO)
|
.map_err(RequirementsTxtParserError::IO)
|
||||||
|
|
@ -321,6 +322,22 @@ impl RequirementsTxt {
|
||||||
let sub_file =
|
let sub_file =
|
||||||
if filename.starts_with("http://") || filename.starts_with("https://") {
|
if filename.starts_with("http://") || filename.starts_with("https://") {
|
||||||
PathBuf::from(filename.as_ref())
|
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 {
|
} else {
|
||||||
requirements_dir.join(filename.as_ref())
|
requirements_dir.join(filename.as_ref())
|
||||||
};
|
};
|
||||||
|
|
@ -360,6 +377,22 @@ impl RequirementsTxt {
|
||||||
let sub_file =
|
let sub_file =
|
||||||
if filename.starts_with("http://") || filename.starts_with("https://") {
|
if filename.starts_with("http://") || filename.starts_with("https://") {
|
||||||
PathBuf::from(filename.as_ref())
|
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 {
|
} else {
|
||||||
requirements_dir.join(filename.as_ref())
|
requirements_dir.join(filename.as_ref())
|
||||||
};
|
};
|
||||||
|
|
@ -815,6 +848,11 @@ pub enum RequirementsTxtParserError {
|
||||||
start: usize,
|
start: usize,
|
||||||
end: usize,
|
end: usize,
|
||||||
},
|
},
|
||||||
|
FileUrl {
|
||||||
|
url: String,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
VerbatimUrl {
|
VerbatimUrl {
|
||||||
source: pep508_rs::VerbatimUrlError,
|
source: pep508_rs::VerbatimUrlError,
|
||||||
url: String,
|
url: String,
|
||||||
|
|
@ -882,6 +920,9 @@ impl Display for RequirementsTxtParserError {
|
||||||
Self::Url { url, start, .. } => {
|
Self::Url { url, start, .. } => {
|
||||||
write!(f, "Invalid URL at position {start}: `{url}`")
|
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 } => {
|
Self::VerbatimUrl { source, url } => {
|
||||||
write!(f, "Invalid URL: `{url}`: {source}")
|
write!(f, "Invalid URL: `{url}`: {source}")
|
||||||
}
|
}
|
||||||
|
|
@ -945,6 +986,7 @@ impl std::error::Error for RequirementsTxtParserError {
|
||||||
match &self {
|
match &self {
|
||||||
Self::IO(err) => err.source(),
|
Self::IO(err) => err.source(),
|
||||||
Self::Url { source, .. } => Some(source),
|
Self::Url { source, .. } => Some(source),
|
||||||
|
Self::FileUrl { .. } => None,
|
||||||
Self::VerbatimUrl { source, .. } => Some(source),
|
Self::VerbatimUrl { source, .. } => Some(source),
|
||||||
Self::UrlConversion(_) => None,
|
Self::UrlConversion(_) => None,
|
||||||
Self::UnsupportedUrl(_) => None,
|
Self::UnsupportedUrl(_) => None,
|
||||||
|
|
@ -976,6 +1018,13 @@ impl Display for RequirementsTxtFileError {
|
||||||
self.file.user_display(),
|
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, .. } => {
|
RequirementsTxtParserError::VerbatimUrl { url, .. } => {
|
||||||
write!(f, "Invalid URL in `{}`: `{url}`", self.file.user_display())
|
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`.
|
/// 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() {
|
if input.is_empty() {
|
||||||
Ok(Maybe::None)
|
Ok(Maybe::None)
|
||||||
} else {
|
} else {
|
||||||
match PathBuf::from_str(input) {
|
parse_file_path(input).map(Maybe::Some)
|
||||||
Ok(path) => Ok(Maybe::Some(path)),
|
|
||||||
Err(err) => Err(err.to_string()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +286,7 @@ pub(crate) struct PipCompileArgs {
|
||||||
/// Include all packages listed in the given `requirements.in` files.
|
/// Include all packages listed in the given `requirements.in` files.
|
||||||
///
|
///
|
||||||
/// When the path is `-`, then requirements are read from stdin.
|
/// 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>,
|
pub(crate) src_file: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Constrain versions using the given requirements files.
|
/// Constrain versions using the given requirements files.
|
||||||
|
|
@ -281,7 +296,7 @@ pub(crate) struct PipCompileArgs {
|
||||||
/// trigger the installation of that package.
|
/// trigger the installation of that package.
|
||||||
///
|
///
|
||||||
/// This is equivalent to pip's `--constraint` option.
|
/// 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>>,
|
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||||
|
|
||||||
/// Override versions using the given requirements files.
|
/// 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
|
/// 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
|
/// constituent packages, overrides are _absolute_, in that they completely replace the
|
||||||
/// requirements of the constituent packages.
|
/// requirements of the constituent packages.
|
||||||
#[arg(long)]
|
#[arg(long, value_parser = parse_file_path)]
|
||||||
pub(crate) r#override: Vec<PathBuf>,
|
pub(crate) r#override: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Include optional dependencies in the given extra group name; may be provided more than once.
|
/// 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)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub(crate) struct PipSyncArgs {
|
pub(crate) struct PipSyncArgs {
|
||||||
/// Include all packages listed in the given `requirements.txt` files.
|
/// 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>,
|
pub(crate) src_file: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Constrain versions using the given requirements files.
|
/// Constrain versions using the given requirements files.
|
||||||
|
|
@ -603,7 +618,7 @@ pub(crate) struct PipSyncArgs {
|
||||||
/// trigger the installation of that package.
|
/// trigger the installation of that package.
|
||||||
///
|
///
|
||||||
/// This is equivalent to pip's `--constraint` option.
|
/// 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>>,
|
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||||
|
|
||||||
/// Reinstall all packages, regardless of whether they're already installed.
|
/// Reinstall all packages, regardless of whether they're already installed.
|
||||||
|
|
@ -892,7 +907,7 @@ pub(crate) struct PipInstallArgs {
|
||||||
pub(crate) package: Vec<String>,
|
pub(crate) package: Vec<String>,
|
||||||
|
|
||||||
/// Install all packages listed in the given requirements files.
|
/// 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>,
|
pub(crate) requirement: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Install the editable package based on the provided local file path.
|
/// 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.
|
/// trigger the installation of that package.
|
||||||
///
|
///
|
||||||
/// This is equivalent to pip's `--constraint` option.
|
/// 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>>,
|
pub(crate) constraint: Vec<Maybe<PathBuf>>,
|
||||||
|
|
||||||
/// Override versions using the given requirements files.
|
/// 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
|
/// 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
|
/// constituent packages, overrides are _absolute_, in that they completely replace the
|
||||||
/// requirements of the constituent packages.
|
/// requirements of the constituent packages.
|
||||||
#[arg(long)]
|
#[arg(long, value_parser = parse_file_path)]
|
||||||
pub(crate) r#override: Vec<PathBuf>,
|
pub(crate) r#override: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Include optional dependencies in the given extra group name; may be provided more than once.
|
/// 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>,
|
pub(crate) package: Vec<String>,
|
||||||
|
|
||||||
/// Uninstall all packages listed in the given requirements files.
|
/// 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>,
|
pub(crate) requirement: Vec<PathBuf>,
|
||||||
|
|
||||||
/// The Python interpreter from which packages should be uninstalled.
|
/// The Python interpreter from which packages should be uninstalled.
|
||||||
|
|
|
||||||
|
|
@ -9629,3 +9629,29 @@ fn dynamic_pyproject_toml() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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…
Reference in New Issue