mirror of https://github.com/astral-sh/uv
Support unnamed requirements in `uv add` (#4326)
## Summary Support unnamed URL requirements in `uv add`. For example, `uv add git+https://github.com/pallets/flask`. Part of https://github.com/astral-sh/uv/issues/3959.
This commit is contained in:
parent
accbb9b695
commit
042fdea087
|
|
@ -43,6 +43,27 @@ impl Requirement {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Requirement> for pep508_rs::Requirement<VerbatimUrl> {
|
||||
/// Convert a [`Requirement`] to a [`pep508_rs::Requirement`].
|
||||
fn from(requirement: Requirement) -> Self {
|
||||
pep508_rs::Requirement {
|
||||
name: requirement.name,
|
||||
extras: requirement.extras,
|
||||
marker: requirement.marker,
|
||||
origin: requirement.origin,
|
||||
version_or_url: match requirement.source {
|
||||
RequirementSource::Registry { specifier, .. } => {
|
||||
Some(VersionOrUrl::VersionSpecifier(specifier))
|
||||
}
|
||||
RequirementSource::Url { url, .. }
|
||||
| RequirementSource::Git { url, .. }
|
||||
| RequirementSource::Path { url, .. }
|
||||
| RequirementSource::Directory { url, .. } => Some(VersionOrUrl::Url(url)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pep508_rs::Requirement<VerbatimParsedUrl>> for Requirement {
|
||||
/// Convert a [`pep508_rs::Requirement`] to a [`Requirement`].
|
||||
fn from(requirement: pep508_rs::Requirement<VerbatimParsedUrl>) -> Self {
|
||||
|
|
|
|||
|
|
@ -1602,6 +1602,15 @@ pub(crate) struct AddArgs {
|
|||
#[arg(required = true)]
|
||||
pub(crate) requirements: Vec<String>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) installer: ResolverInstallerArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) build: BuildArgs,
|
||||
|
||||
#[command(flatten)]
|
||||
pub(crate) refresh: RefreshArgs,
|
||||
|
||||
/// The Python interpreter into which packages should be installed.
|
||||
///
|
||||
/// By default, `uv` installs into the virtual environment in the current working directory or
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use pep508_rs::Requirement;
|
||||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_distribution::pyproject_mut::PyProjectTomlMut;
|
||||
use uv_distribution::ProjectWorkspace;
|
||||
use uv_git::GitResolver;
|
||||
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
|
||||
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, SetupPyStrategy};
|
||||
use uv_distribution::{DistributionDatabase, ProjectWorkspace};
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::commands::pip::resolution_environment;
|
||||
use crate::commands::reporters::ResolverReporter;
|
||||
use crate::commands::{project, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::{InstallerSettings, ResolverSettings};
|
||||
use crate::settings::ResolverInstallerSettings;
|
||||
|
||||
/// Add one or more packages to the project requirements.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn add(
|
||||
requirements: Vec<String>,
|
||||
requirements: Vec<RequirementsSource>,
|
||||
python: Option<String>,
|
||||
settings: ResolverInstallerSettings,
|
||||
preview: PreviewMode,
|
||||
connectivity: Connectivity,
|
||||
concurrency: Concurrency,
|
||||
|
|
@ -33,10 +38,92 @@ pub(crate) async fn add(
|
|||
// Find the project requirements.
|
||||
let project = ProjectWorkspace::discover(&std::env::current_dir()?, None).await?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
|
||||
|
||||
let client_builder = BaseClientBuilder::new()
|
||||
.connectivity(connectivity)
|
||||
.native_tls(native_tls)
|
||||
.keyring(settings.keyring_provider);
|
||||
|
||||
// Read the requirements.
|
||||
let RequirementsSpecification { requirements, .. } =
|
||||
RequirementsSpecification::from_sources(&requirements, &[], &[], &client_builder).await?;
|
||||
|
||||
// TODO(charlie): These are all default values. We should consider whether we want to make them
|
||||
// optional on the downstream APIs.
|
||||
let python_version = None;
|
||||
let python_platform = None;
|
||||
let hasher = HashStrategy::default();
|
||||
let setup_py = SetupPyStrategy::default();
|
||||
let build_isolation = BuildIsolation::default();
|
||||
|
||||
// Determine the environment for the resolution.
|
||||
let (tags, markers) =
|
||||
resolution_environment(python_version, python_platform, venv.interpreter())?;
|
||||
|
||||
// Initialize the registry client.
|
||||
let client = RegistryClientBuilder::new(cache.clone())
|
||||
.native_tls(native_tls)
|
||||
.connectivity(connectivity)
|
||||
.index_urls(settings.index_locations.index_urls())
|
||||
.index_strategy(settings.index_strategy)
|
||||
.keyring(settings.keyring_provider)
|
||||
.markers(&markers)
|
||||
.platform(venv.interpreter().platform())
|
||||
.build();
|
||||
|
||||
// Initialize any shared state.
|
||||
let git = GitResolver::default();
|
||||
let in_flight = InFlight::default();
|
||||
let index = InMemoryIndex::default();
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
let flat_index = {
|
||||
let client = FlatIndexClient::new(&client, cache);
|
||||
let entries = client.fetch(settings.index_locations.flat_index()).await?;
|
||||
FlatIndex::from_entries(entries, Some(&tags), &hasher, &settings.build_options)
|
||||
};
|
||||
|
||||
// Create a build dispatch.
|
||||
let build_dispatch = BuildDispatch::new(
|
||||
&client,
|
||||
cache,
|
||||
venv.interpreter(),
|
||||
&settings.index_locations,
|
||||
&flat_index,
|
||||
&index,
|
||||
&git,
|
||||
&in_flight,
|
||||
setup_py,
|
||||
&settings.config_setting,
|
||||
build_isolation,
|
||||
settings.link_mode,
|
||||
&settings.build_options,
|
||||
concurrency,
|
||||
preview,
|
||||
)
|
||||
.with_options(
|
||||
OptionsBuilder::new()
|
||||
.exclude_newer(settings.exclude_newer)
|
||||
.build(),
|
||||
);
|
||||
|
||||
// Resolve any unnamed requirements.
|
||||
let requirements = NamedRequirementsResolver::new(
|
||||
requirements,
|
||||
&hasher,
|
||||
&index,
|
||||
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||
)
|
||||
.with_reporter(ResolverReporter::from(printer))
|
||||
.resolve()
|
||||
.await?;
|
||||
|
||||
// Add the requirements to the `pyproject.toml`.
|
||||
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
|
||||
for req in requirements {
|
||||
let req = Requirement::from_str(&req)?;
|
||||
pyproject.add_dependency(&req)?;
|
||||
pyproject.add_dependency(&pep508_rs::Requirement::from(req))?;
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
|
|
@ -45,12 +132,6 @@ pub(crate) async fn add(
|
|||
pyproject.to_string(),
|
||||
)?;
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let venv = project::init_environment(project.workspace(), python.as_deref(), cache, printer)?;
|
||||
|
||||
// Use the default settings.
|
||||
let settings = ResolverSettings::default();
|
||||
|
||||
// Lock and sync the environment.
|
||||
let lock = project::lock::do_lock(
|
||||
project.workspace(),
|
||||
|
|
@ -76,7 +157,6 @@ pub(crate) async fn add(
|
|||
|
||||
// Perform a full sync, because we don't know what exactly is affected by the removal.
|
||||
// TODO(ibraheem): Should we accept CLI overrides for this? Should we even sync here?
|
||||
let settings = InstallerSettings::default();
|
||||
let extras = ExtrasSpecification::All;
|
||||
let dev = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -683,11 +683,18 @@ async fn run() -> Result<ExitStatus> {
|
|||
show_settings!(args);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
let cache = cache.init()?.with_refresh(args.refresh);
|
||||
|
||||
let requirements = args
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(RequirementsSource::Package)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
commands::add(
|
||||
args.requirements,
|
||||
requirements,
|
||||
args.python,
|
||||
args.settings,
|
||||
globals.preview,
|
||||
globals.connectivity,
|
||||
Concurrency::default(),
|
||||
|
|
|
|||
|
|
@ -365,20 +365,30 @@ impl LockSettings {
|
|||
pub(crate) struct AddSettings {
|
||||
pub(crate) requirements: Vec<String>,
|
||||
pub(crate) python: Option<String>,
|
||||
pub(crate) refresh: Refresh,
|
||||
pub(crate) settings: ResolverInstallerSettings,
|
||||
}
|
||||
|
||||
impl AddSettings {
|
||||
/// Resolve the [`AddSettings`] from the CLI and filesystem configuration.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn resolve(args: AddArgs, _filesystem: Option<FilesystemOptions>) -> Self {
|
||||
pub(crate) fn resolve(args: AddArgs, filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let AddArgs {
|
||||
requirements,
|
||||
installer,
|
||||
build,
|
||||
refresh,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
requirements,
|
||||
python,
|
||||
refresh: Refresh::from(refresh),
|
||||
settings: ResolverInstallerSettings::combine(
|
||||
resolver_installer_options(installer, build),
|
||||
filesystem,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -282,6 +282,98 @@ fn add_git() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Add an unnamed requirement.
|
||||
#[test]
|
||||
fn add_unnamed() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
# ...
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add(&["git+https://github.com/astral-test/uv-public-pypackage@0.0.1"]), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning.
|
||||
Resolved 2 packages in [TIME]
|
||||
Downloaded 2 packages in [TIME]
|
||||
Installed 2 packages in [TIME]
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
# ...
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0.0.1",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
sdist = { path = "." }
|
||||
|
||||
[[distribution.dependencies]]
|
||||
name = "uv-public-pypackage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979"
|
||||
|
||||
[[distribution]]
|
||||
name = "uv-public-pypackage"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979"
|
||||
sdist = { url = "https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979" }
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 2 packages in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update a PyPI requirement.
|
||||
#[test]
|
||||
fn update_registry() -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue