mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Enable unnamed requirements for direct URLs (#2569)
## Summary This PR enables `uv pip install` to accept unnamed requirements, as long as the requirement ends with the wheel or source distribution archive name. For example: `cargo run pip install ~/Downloads/anyio-4.3.0.tar.gz`. In subsequent PRs, I'll expand the scope of supported archives and patterns. Part of: https://github.com/astral-sh/uv/issues/313.
This commit is contained in:
parent
ee211b35bc
commit
4d96255ade
8 changed files with 251 additions and 86 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4357,6 +4357,7 @@ dependencies = [
|
|||
"clap_complete_command",
|
||||
"console",
|
||||
"ctrlc",
|
||||
"distribution-filename",
|
||||
"distribution-types",
|
||||
"filetime",
|
||||
"flate2",
|
||||
|
|
|
@ -64,6 +64,7 @@ tracing-subscriber = { workspace = true, features = ["json"] }
|
|||
tracing-tree = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
url = { workspace = true }
|
||||
distribution-filename = { version = "0.0.1", path = "../distribution-filename" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
mimalloc = { version = "0.1.39" }
|
||||
|
|
|
@ -37,7 +37,8 @@ use crate::commands::reporters::{DownloadReporter, ResolverReporter};
|
|||
use crate::commands::{elapsed, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
use crate::requirements::{
|
||||
read_lockfile, ExtrasSpecification, RequirementsSource, RequirementsSpecification,
|
||||
read_lockfile, ExtrasSpecification, NamedRequirements, RequirementsSource,
|
||||
RequirementsSpecification,
|
||||
};
|
||||
|
||||
/// Resolve a set of requirements into a set of pinned versions.
|
||||
|
@ -89,18 +90,7 @@ pub(crate) async fn pip_compile(
|
|||
}
|
||||
|
||||
// Read all requirements from the provided sources.
|
||||
let RequirementsSpecification {
|
||||
project,
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
editables,
|
||||
index_url,
|
||||
extra_index_urls,
|
||||
no_index,
|
||||
find_links,
|
||||
extras: used_extras,
|
||||
} = RequirementsSpecification::from_sources(
|
||||
let spec = RequirementsSpecification::from_sources(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
|
@ -113,7 +103,7 @@ pub(crate) async fn pip_compile(
|
|||
if let ExtrasSpecification::Some(extras) = extras {
|
||||
let mut unused_extras = extras
|
||||
.iter()
|
||||
.filter(|extra| !used_extras.contains(extra))
|
||||
.filter(|extra| !spec.extras.contains(extra))
|
||||
.collect::<Vec<_>>();
|
||||
if !unused_extras.is_empty() {
|
||||
unused_extras.sort_unstable();
|
||||
|
@ -126,6 +116,19 @@ pub(crate) async fn pip_compile(
|
|||
}
|
||||
}
|
||||
|
||||
// Convert from unnamed to named requirements.
|
||||
let NamedRequirements {
|
||||
project,
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
editables,
|
||||
index_url,
|
||||
extra_index_urls,
|
||||
no_index,
|
||||
find_links,
|
||||
} = NamedRequirements::from_spec(spec)?;
|
||||
|
||||
// Read the lockfile, if present.
|
||||
let preferences = read_lockfile(output_file, upgrade).await?;
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ use uv_warnings::warn_user;
|
|||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
use crate::requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification};
|
||||
use crate::requirements::{
|
||||
ExtrasSpecification, NamedRequirements, RequirementsSource, RequirementsSpecification,
|
||||
};
|
||||
|
||||
use super::{DryRunEvent, Upgrade};
|
||||
|
||||
|
@ -76,10 +78,10 @@ pub(crate) async fn pip_install(
|
|||
dry_run: bool,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
let start = std::time::Instant::now();
|
||||
let start = Instant::now();
|
||||
|
||||
// Read all requirements from the provided sources.
|
||||
let RequirementsSpecification {
|
||||
let NamedRequirements {
|
||||
project,
|
||||
requirements,
|
||||
constraints,
|
||||
|
@ -89,25 +91,7 @@ pub(crate) async fn pip_install(
|
|||
extra_index_urls,
|
||||
no_index,
|
||||
find_links,
|
||||
extras: used_extras,
|
||||
} = specification(requirements, constraints, overrides, extras, connectivity).await?;
|
||||
|
||||
// Check that all provided extras are used
|
||||
if let ExtrasSpecification::Some(extras) = extras {
|
||||
let mut unused_extras = extras
|
||||
.iter()
|
||||
.filter(|extra| !used_extras.contains(extra))
|
||||
.collect::<Vec<_>>();
|
||||
if !unused_extras.is_empty() {
|
||||
unused_extras.sort_unstable();
|
||||
unused_extras.dedup();
|
||||
let s = if unused_extras.len() == 1 { "" } else { "s" };
|
||||
return Err(anyhow!(
|
||||
"Requested extra{s} not found: {}",
|
||||
unused_extras.iter().join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
} = read_requirements(requirements, constraints, overrides, extras, connectivity).await?;
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python.as_ref() {
|
||||
|
@ -348,13 +332,13 @@ pub(crate) async fn pip_install(
|
|||
}
|
||||
|
||||
/// Consolidate the requirements for an installation.
|
||||
async fn specification(
|
||||
async fn read_requirements(
|
||||
requirements: &[RequirementsSource],
|
||||
constraints: &[RequirementsSource],
|
||||
overrides: &[RequirementsSource],
|
||||
extras: &ExtrasSpecification<'_>,
|
||||
connectivity: Connectivity,
|
||||
) -> Result<RequirementsSpecification, Error> {
|
||||
) -> Result<NamedRequirements, Error> {
|
||||
// If the user requests `extras` but does not provide a pyproject toml source
|
||||
if !matches!(extras, ExtrasSpecification::None)
|
||||
&& !requirements
|
||||
|
@ -392,6 +376,9 @@ async fn specification(
|
|||
}
|
||||
}
|
||||
|
||||
// Convert from unnamed to named requirements.
|
||||
let spec = NamedRequirements::from_spec(spec)?;
|
||||
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ use uv_warnings::warn_user;
|
|||
use crate::commands::reporters::{DownloadReporter, FinderReporter, InstallReporter};
|
||||
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
use crate::requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use crate::requirements::{NamedRequirements, RequirementsSource, RequirementsSpecification};
|
||||
|
||||
/// Install a set of locked requirements into the current Python environment.
|
||||
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
|
||||
|
@ -54,20 +54,10 @@ pub(crate) async fn pip_sync(
|
|||
let start = std::time::Instant::now();
|
||||
|
||||
// Read all requirements from the provided sources.
|
||||
let RequirementsSpecification {
|
||||
project: _project,
|
||||
requirements,
|
||||
constraints: _constraints,
|
||||
overrides: _overrides,
|
||||
editables,
|
||||
index_url,
|
||||
extra_index_urls,
|
||||
no_index,
|
||||
find_links,
|
||||
extras: _extras,
|
||||
} = RequirementsSpecification::from_simple_sources(sources, connectivity).await?;
|
||||
let spec = RequirementsSpecification::from_simple_sources(sources, connectivity).await?;
|
||||
|
||||
let num_requirements = requirements.len() + editables.len();
|
||||
// Validate that the requirements are non-empty.
|
||||
let num_requirements = spec.requirements.len() + spec.editables.len();
|
||||
if num_requirements == 0 {
|
||||
writeln!(printer.stderr(), "No requirements found")?;
|
||||
return Ok(ExitStatus::Success);
|
||||
|
@ -107,6 +97,19 @@ pub(crate) async fn pip_sync(
|
|||
}
|
||||
}
|
||||
|
||||
// Convert from unnamed to named requirements.
|
||||
let NamedRequirements {
|
||||
project: _project,
|
||||
requirements,
|
||||
constraints: _constraints,
|
||||
overrides: _overrides,
|
||||
editables,
|
||||
index_url,
|
||||
extra_index_urls,
|
||||
no_index,
|
||||
find_links,
|
||||
} = NamedRequirements::from_spec(spec)?;
|
||||
|
||||
let _lock = venv.lock()?;
|
||||
|
||||
// Determine the current environment markers.
|
||||
|
|
|
@ -12,7 +12,7 @@ use uv_interpreter::PythonEnvironment;
|
|||
|
||||
use crate::commands::{elapsed, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
use crate::requirements::{RequirementsSource, RequirementsSpecification};
|
||||
use crate::requirements::{NamedRequirements, RequirementsSource, RequirementsSpecification};
|
||||
|
||||
/// Uninstall packages from the current environment.
|
||||
pub(crate) async fn pip_uninstall(
|
||||
|
@ -27,18 +27,7 @@ pub(crate) async fn pip_uninstall(
|
|||
let start = std::time::Instant::now();
|
||||
|
||||
// Read all requirements from the provided sources.
|
||||
let RequirementsSpecification {
|
||||
project: _project,
|
||||
requirements,
|
||||
constraints: _constraints,
|
||||
overrides: _overrides,
|
||||
editables,
|
||||
index_url: _index_url,
|
||||
extra_index_urls: _extra_index_urls,
|
||||
no_index: _no_index,
|
||||
find_links: _find_links,
|
||||
extras: _extras,
|
||||
} = RequirementsSpecification::from_simple_sources(sources, connectivity).await?;
|
||||
let spec = RequirementsSpecification::from_simple_sources(sources, connectivity).await?;
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
let venv = if let Some(python) = python.as_ref() {
|
||||
|
@ -74,6 +63,19 @@ pub(crate) async fn pip_uninstall(
|
|||
}
|
||||
}
|
||||
|
||||
// Convert from unnamed to named requirements.
|
||||
let NamedRequirements {
|
||||
project: _,
|
||||
requirements,
|
||||
constraints: _,
|
||||
overrides: _,
|
||||
editables,
|
||||
index_url: _,
|
||||
extra_index_urls: _,
|
||||
no_index: _,
|
||||
find_links: _,
|
||||
} = NamedRequirements::from_spec(spec)?;
|
||||
|
||||
let _lock = venv.lock()?;
|
||||
|
||||
// Index the current `site-packages` directory.
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use console::Term;
|
||||
use distribution_filename::{SourceDistFilename, WheelFilename};
|
||||
use indexmap::IndexMap;
|
||||
use rustc_hash::FxHashSet;
|
||||
use tracing::{instrument, Level};
|
||||
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||
use pep508_rs::{Requirement, RequirementsTxtRequirement};
|
||||
use distribution_types::{FlatIndexLocation, IndexUrl, RemoteSource};
|
||||
use pep508_rs::{Requirement, RequirementsTxtRequirement, UnnamedRequirement, VersionOrUrl};
|
||||
use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt};
|
||||
use uv_client::Connectivity;
|
||||
use uv_fs::Simplified;
|
||||
|
@ -104,7 +105,7 @@ pub(crate) struct RequirementsSpecification {
|
|||
/// The name of the project specifying requirements.
|
||||
pub(crate) project: Option<PackageName>,
|
||||
/// The requirements for the project.
|
||||
pub(crate) requirements: Vec<Requirement>,
|
||||
pub(crate) requirements: Vec<RequirementsTxtRequirement>,
|
||||
/// The constraints for the project.
|
||||
pub(crate) constraints: Vec<Requirement>,
|
||||
/// The overrides for the project.
|
||||
|
@ -133,7 +134,7 @@ impl RequirementsSpecification {
|
|||
) -> Result<Self> {
|
||||
Ok(match source {
|
||||
RequirementsSource::Package(name) => {
|
||||
let requirement = Requirement::parse(name, std::env::current_dir()?)
|
||||
let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?)
|
||||
.with_context(|| format!("Failed to parse `{name}`"))?;
|
||||
Self {
|
||||
project: None,
|
||||
|
@ -172,13 +173,8 @@ impl RequirementsSpecification {
|
|||
requirements: requirements_txt
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(|entry| match entry.requirement {
|
||||
RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement),
|
||||
RequirementsTxtRequirement::Unnamed(requirement) => Err(anyhow!(
|
||||
"Unnamed URL requirements are not yet supported: {requirement}"
|
||||
)),
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
.map(|entry| entry.requirement)
|
||||
.collect(),
|
||||
constraints: requirements_txt.constraints,
|
||||
editables: requirements_txt.editables,
|
||||
overrides: vec![],
|
||||
|
@ -253,7 +249,10 @@ impl RequirementsSpecification {
|
|||
|
||||
Self {
|
||||
project: project_name,
|
||||
requirements,
|
||||
requirements: requirements
|
||||
.into_iter()
|
||||
.map(RequirementsTxtRequirement::Pep508)
|
||||
.collect(),
|
||||
constraints: vec![],
|
||||
overrides: vec![],
|
||||
editables: vec![],
|
||||
|
@ -309,7 +308,18 @@ impl RequirementsSpecification {
|
|||
// Read all constraints, treating _everything_ as a constraint.
|
||||
for source in constraints {
|
||||
let source = Self::from_source(source, extras, connectivity).await?;
|
||||
spec.constraints.extend(source.requirements);
|
||||
for requirement in source.requirements {
|
||||
match requirement {
|
||||
RequirementsTxtRequirement::Pep508(requirement) => {
|
||||
spec.constraints.push(requirement);
|
||||
}
|
||||
RequirementsTxtRequirement::Unnamed(requirement) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unnamed requirements are not allowed as constraints (found: `{requirement}`)"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.constraints.extend(source.constraints);
|
||||
spec.constraints.extend(source.overrides);
|
||||
|
||||
|
@ -329,7 +339,18 @@ impl RequirementsSpecification {
|
|||
// Read all overrides, treating both requirements _and_ constraints as overrides.
|
||||
for source in overrides {
|
||||
let source = Self::from_source(source, extras, connectivity).await?;
|
||||
spec.overrides.extend(source.requirements);
|
||||
for requirement in source.requirements {
|
||||
match requirement {
|
||||
RequirementsTxtRequirement::Pep508(requirement) => {
|
||||
spec.overrides.push(requirement);
|
||||
}
|
||||
RequirementsTxtRequirement::Unnamed(requirement) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unnamed requirements are not allowed as overrides (found: `{requirement}`)"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.overrides.extend(source.constraints);
|
||||
spec.overrides.extend(source.overrides);
|
||||
|
||||
|
@ -470,3 +491,93 @@ pub(crate) async fn read_lockfile(
|
|||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`RequirementsSpecification`], but with concrete names for all requirements.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct NamedRequirements {
|
||||
/// The name of the project specifying requirements.
|
||||
pub(crate) project: Option<PackageName>,
|
||||
/// The requirements for the project.
|
||||
pub(crate) requirements: Vec<Requirement>,
|
||||
/// The constraints for the project.
|
||||
pub(crate) constraints: Vec<Requirement>,
|
||||
/// The overrides for the project.
|
||||
pub(crate) overrides: Vec<Requirement>,
|
||||
/// Package to install as editable installs
|
||||
pub(crate) editables: Vec<EditableRequirement>,
|
||||
/// The index URL to use for fetching packages.
|
||||
pub(crate) index_url: Option<IndexUrl>,
|
||||
/// The extra index URLs to use for fetching packages.
|
||||
pub(crate) extra_index_urls: Vec<IndexUrl>,
|
||||
/// Whether to disallow index usage.
|
||||
pub(crate) no_index: bool,
|
||||
/// The `--find-links` locations to use for fetching packages.
|
||||
pub(crate) find_links: Vec<FlatIndexLocation>,
|
||||
}
|
||||
|
||||
impl NamedRequirements {
|
||||
/// Convert a [`RequirementsSpecification`] into a [`NamedRequirements`].
|
||||
pub(crate) fn from_spec(spec: RequirementsSpecification) -> Result<Self> {
|
||||
Ok(Self {
|
||||
project: spec.project,
|
||||
requirements: spec
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(|requirement| match requirement {
|
||||
RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement),
|
||||
RequirementsTxtRequirement::Unnamed(requirement) => {
|
||||
Self::name_requirement(requirement)
|
||||
}
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
constraints: spec.constraints,
|
||||
overrides: spec.overrides,
|
||||
editables: spec.editables,
|
||||
index_url: spec.index_url,
|
||||
extra_index_urls: spec.extra_index_urls,
|
||||
no_index: spec.no_index,
|
||||
find_links: spec.find_links,
|
||||
})
|
||||
}
|
||||
|
||||
/// Infer the package name for a given "unnamed" requirement.
|
||||
fn name_requirement(requirement: UnnamedRequirement) -> Result<Requirement> {
|
||||
// If the requirement is a wheel, extract the package name from the wheel filename.
|
||||
//
|
||||
// Ex) `anyio-4.3.0-py3-none-any.whl`
|
||||
if Path::new(requirement.url.path())
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("whl"))
|
||||
{
|
||||
let filename = WheelFilename::from_str(&requirement.url.filename()?)?;
|
||||
return Ok(Requirement {
|
||||
name: filename.name,
|
||||
extras: requirement.extras,
|
||||
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
|
||||
marker: requirement.marker,
|
||||
});
|
||||
}
|
||||
|
||||
// If the requirement is a source archive, try to extract the package name from the archive
|
||||
// filename. This isn't guaranteed to work.
|
||||
//
|
||||
// Ex) `anyio-4.3.0.tar.gz`
|
||||
if let Some(filename) = requirement
|
||||
.url
|
||||
.filename()
|
||||
.ok()
|
||||
.and_then(|filename| SourceDistFilename::parsed_normalized_filename(&filename).ok())
|
||||
{
|
||||
return Ok(Requirement {
|
||||
name: filename.name,
|
||||
extras: requirement.extras,
|
||||
version_or_url: Some(VersionOrUrl::Url(requirement.url)),
|
||||
marker: requirement.marker,
|
||||
});
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Unable to infer package name for the unnamed requirement: {requirement}"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2485,12 +2485,29 @@ fn respect_unnamed_env_var() -> Result<()> {
|
|||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in")
|
||||
.env("URL", "https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
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 2023-11-18T12:00:00Z requirements.in
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask @ ${URL}
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
error: Unnamed URL requirements are not yet supported: https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl
|
||||
Resolved 7 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
|
@ -3444,13 +3461,53 @@ fn missing_editable_requirement() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to resolve a URL requirement without a package name.
|
||||
/// Attempt to resolve a URL requirement without a package name. The package name can be extracted
|
||||
/// from the URL.
|
||||
#[test]
|
||||
fn missing_package_name() -> Result<()> {
|
||||
fn unnamed_requirement_with_package_name() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl")?;
|
||||
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in"), @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 2023-11-18T12:00:00Z requirements.in
|
||||
blinker==1.7.0
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
flask @ https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl
|
||||
itsdangerous==2.1.2
|
||||
# via flask
|
||||
jinja2==3.1.2
|
||||
# via flask
|
||||
markupsafe==2.1.3
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
werkzeug==3.0.1
|
||||
# via flask
|
||||
|
||||
----- stderr -----
|
||||
Resolved 7 packages in [TIME]
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to resolve a URL requirement without a package name. The package name can't be extracted
|
||||
/// from the URL.
|
||||
#[test]
|
||||
fn unnamed_requirement_ambiguous() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
requirements_in.write_str("https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0")?;
|
||||
|
||||
uv_snapshot!(context.compile()
|
||||
.arg("requirements.in"), @r###"
|
||||
success: false
|
||||
|
@ -3458,7 +3515,7 @@ fn missing_package_name() -> Result<()> {
|
|||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Unnamed URL requirements are not yet supported: https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0-py3-none-any.whl
|
||||
error: Unable to infer package name for the unnamed requirement: https://files.pythonhosted.org/packages/36/42/015c23096649b908c809c69388a805a571a3bea44362fe87e33fc3afa01f/flask-3.0.0
|
||||
"###
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue