mirror of https://github.com/astral-sh/uv
Add `uv sync --script` (#11361)
## Summary The environment is located at a stable path within the cache, based on the script's absolute path. If a lockfile exists for the script, then we use our standard lockfile semantics (i.e., update the lockfile if necessary, etc.); if not, we just do a `uv pip sync` (roughly). Example usage: ``` ❯ uv init --script hello.py Initialized script at `hello.py` ❯ uv add --script hello.py requests Updated `hello.py` ❯ cargo run sync --script hello.py Using script environment at: /Users/crmarsh/.cache/uv/environments-v1/hello-84e289fe3f6241a0 Resolved 5 packages in 3ms Installed 5 packages in 12ms + certifi==2025.1.31 + charset-normalizer==3.4.1 + idna==3.10 + requests==2.32.3 + urllib3==2.3.0 ``` Closes https://github.com/astral-sh/uv/issues/6637.
This commit is contained in:
parent
0b4a349173
commit
792dc9d1c5
|
|
@ -3135,6 +3135,32 @@ pub struct SyncArgs {
|
|||
#[arg(long, conflicts_with = "all_packages")]
|
||||
pub package: Option<PackageName>,
|
||||
|
||||
/// Sync the environment for a Python script, rather than the current project.
|
||||
///
|
||||
/// If provided, uv will sync the dependencies based on the script's inline metadata table, in
|
||||
/// adherence with PEP 723.
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with = "active",
|
||||
conflicts_with = "all_packages",
|
||||
conflicts_with = "package",
|
||||
conflicts_with = "no_install_project",
|
||||
conflicts_with = "no_install_workspace",
|
||||
conflicts_with = "extra",
|
||||
conflicts_with = "all_extras",
|
||||
conflicts_with = "no_extra",
|
||||
conflicts_with = "no_all_extras",
|
||||
conflicts_with = "dev",
|
||||
conflicts_with = "no_dev",
|
||||
conflicts_with = "only_dev",
|
||||
conflicts_with = "group",
|
||||
conflicts_with = "no_group",
|
||||
conflicts_with = "no_default_groups",
|
||||
conflicts_with = "only_group",
|
||||
conflicts_with = "all_groups"
|
||||
)]
|
||||
pub script: Option<PathBuf>,
|
||||
|
||||
/// The Python interpreter to use for the project environment.
|
||||
///
|
||||
/// By default, the first interpreter that meets the project's `requires-python` constraint is
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ pub use download::LocalWheel;
|
|||
pub use error::Error;
|
||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||
pub use metadata::{
|
||||
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, Metadata, MetadataError,
|
||||
RequiresDist,
|
||||
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata,
|
||||
MetadataError, RequiresDist,
|
||||
};
|
||||
pub use reporter::Reporter;
|
||||
pub use source::prune;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use uv_workspace::WorkspaceError;
|
|||
|
||||
pub use crate::metadata::build_requires::BuildRequires;
|
||||
pub use crate::metadata::lowering::LoweredRequirement;
|
||||
use crate::metadata::lowering::LoweringError;
|
||||
pub use crate::metadata::lowering::LoweringError;
|
||||
pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
|
||||
|
||||
mod build_requires;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -14,10 +13,10 @@ use uv_cache_key::cache_digest;
|
|||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DryRun,
|
||||
ExtrasSpecification, PreviewMode, Reinstall, TrustedHost, Upgrade,
|
||||
ExtrasSpecification, PreviewMode, Reinstall, SourceStrategy, TrustedHost, Upgrade,
|
||||
};
|
||||
use uv_dispatch::{BuildDispatch, SharedState};
|
||||
use uv_distribution::DistributionDatabase;
|
||||
use uv_distribution::{DistributionDatabase, LoweredRequirement};
|
||||
use uv_distribution_types::{
|
||||
Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||
};
|
||||
|
|
@ -227,6 +226,9 @@ pub(crate) enum ProjectError {
|
|||
#[error(transparent)]
|
||||
Metadata(#[from] uv_distribution::MetadataError),
|
||||
|
||||
#[error(transparent)]
|
||||
Lowering(#[from] uv_distribution::LoweringError),
|
||||
|
||||
#[error(transparent)]
|
||||
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
|
||||
|
||||
|
|
@ -1224,7 +1226,7 @@ impl ProjectEnvironment {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deref for ProjectEnvironment {
|
||||
impl std::ops::Deref for ProjectEnvironment {
|
||||
type Target = PythonEnvironment;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
|
|
@ -1240,7 +1242,30 @@ impl Deref for ProjectEnvironment {
|
|||
|
||||
/// The Python environment for a script.
|
||||
#[derive(Debug)]
|
||||
struct ScriptEnvironment(PythonEnvironment);
|
||||
enum ScriptEnvironment {
|
||||
/// An existing [`PythonEnvironment`] was discovered, which satisfies the script's requirements.
|
||||
Existing(PythonEnvironment),
|
||||
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the script's
|
||||
/// requirements, and so was replaced.
|
||||
Replaced(PythonEnvironment),
|
||||
/// A new [`PythonEnvironment`] was created for the script.
|
||||
Created(PythonEnvironment),
|
||||
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the script's
|
||||
/// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as
|
||||
/// such, a temporary environment was created instead.
|
||||
WouldReplace(
|
||||
PathBuf,
|
||||
PythonEnvironment,
|
||||
#[allow(unused)] tempfile::TempDir,
|
||||
),
|
||||
/// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such,
|
||||
/// a temporary environment was created instead.
|
||||
WouldCreate(
|
||||
PathBuf,
|
||||
PythonEnvironment,
|
||||
#[allow(unused)] tempfile::TempDir,
|
||||
),
|
||||
}
|
||||
|
||||
impl ScriptEnvironment {
|
||||
/// Initialize a virtual environment for a PEP 723 script.
|
||||
|
|
@ -1255,6 +1280,7 @@ impl ScriptEnvironment {
|
|||
install_mirrors: &PythonInstallMirrors,
|
||||
no_config: bool,
|
||||
cache: &Cache,
|
||||
dry_run: DryRun,
|
||||
printer: Printer,
|
||||
) -> Result<Self, ProjectError> {
|
||||
// Lock the script environment to avoid synchronization issues.
|
||||
|
|
@ -1276,29 +1302,12 @@ impl ScriptEnvironment {
|
|||
.await?
|
||||
{
|
||||
// If we found an existing, compatible environment, use it.
|
||||
ScriptInterpreter::Environment(environment) => Ok(Self(environment)),
|
||||
ScriptInterpreter::Environment(environment) => Ok(Self::Existing(environment)),
|
||||
|
||||
// Otherwise, create a virtual environment with the discovered interpreter.
|
||||
ScriptInterpreter::Interpreter(interpreter) => {
|
||||
let root = ScriptInterpreter::root(script, cache);
|
||||
|
||||
// Remove the existing virtual environment.
|
||||
match fs_err::remove_dir_all(&root) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Removed virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
);
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Creating script environment at: {}",
|
||||
root.user_display().cyan()
|
||||
);
|
||||
|
||||
// Determine a prompt for the environment, in order of preference:
|
||||
//
|
||||
// 1) The name of the script
|
||||
|
|
@ -1310,6 +1319,43 @@ impl ScriptEnvironment {
|
|||
.map(uv_virtualenv::Prompt::Static)
|
||||
.unwrap_or(uv_virtualenv::Prompt::None);
|
||||
|
||||
// Under `--dry-run`, avoid modifying the environment.
|
||||
if dry_run.enabled() {
|
||||
let temp_dir = cache.venv_dir()?;
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
temp_dir.path(),
|
||||
interpreter,
|
||||
prompt,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
return Ok(if root.exists() {
|
||||
Self::WouldReplace(root, environment, temp_dir)
|
||||
} else {
|
||||
Self::WouldCreate(root, environment, temp_dir)
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the existing virtual environment.
|
||||
let replaced = match fs_err::remove_dir_all(&root) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Removed virtual environment at: {}",
|
||||
root.user_display().cyan()
|
||||
);
|
||||
true
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Creating script environment at: {}",
|
||||
root.user_display().cyan()
|
||||
);
|
||||
|
||||
let environment = uv_virtualenv::create_venv(
|
||||
&root,
|
||||
interpreter,
|
||||
|
|
@ -1320,14 +1366,42 @@ impl ScriptEnvironment {
|
|||
false,
|
||||
)?;
|
||||
|
||||
Ok(Self(environment))
|
||||
Ok(if replaced {
|
||||
Self::Replaced(environment)
|
||||
} else {
|
||||
Self::Created(environment)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the [`ScriptEnvironment`] into a [`PythonEnvironment`].
|
||||
pub(crate) fn into_environment(self) -> PythonEnvironment {
|
||||
self.0
|
||||
///
|
||||
/// Returns an error if the environment was created in `--dry-run` mode, as dropping the
|
||||
/// associated temporary directory could lead to errors downstream.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
|
||||
match self {
|
||||
Self::Existing(environment) => Ok(environment),
|
||||
Self::Replaced(environment) => Ok(environment),
|
||||
Self::Created(environment) => Ok(environment),
|
||||
Self::WouldReplace(..) => Err(ProjectError::DroppedEnvironment),
|
||||
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ScriptEnvironment {
|
||||
type Target = PythonEnvironment;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Existing(environment) => environment,
|
||||
Self::Replaced(environment) => environment,
|
||||
Self::Created(environment) => environment,
|
||||
Self::WouldReplace(_, environment, _) => environment,
|
||||
Self::WouldCreate(_, environment, _) => environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1839,6 +1913,7 @@ pub(crate) async fn update_environment(
|
|||
native_tls: bool,
|
||||
allow_insecure_host: &[TrustedHost],
|
||||
cache: &Cache,
|
||||
dry_run: DryRun,
|
||||
printer: Printer,
|
||||
preview: PreviewMode,
|
||||
) -> Result<EnvironmentUpdate, ProjectError> {
|
||||
|
|
@ -1954,7 +2029,6 @@ pub(crate) async fn update_environment(
|
|||
// optional on the downstream APIs.
|
||||
let build_constraints = Constraints::default();
|
||||
let build_hasher = HashStrategy::default();
|
||||
let dry_run = DryRun::default();
|
||||
let extras = ExtrasSpecification::default();
|
||||
let groups = DevGroupsSpecification::default();
|
||||
let hasher = HashStrategy::default();
|
||||
|
|
@ -2225,6 +2299,113 @@ pub(crate) fn detect_conflicts(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine the [`RequirementsSpecification`] for a script.
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub(crate) fn script_specification(
|
||||
script: Pep723ItemRef<'_>,
|
||||
settings: ResolverSettingsRef,
|
||||
) -> Result<Option<RequirementsSpecification>, ProjectError> {
|
||||
let Some(dependencies) = script.metadata().dependencies.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Determine the working directory for the script.
|
||||
let script_dir = match &script {
|
||||
Pep723ItemRef::Script(script) => std::path::absolute(&script.path)?
|
||||
.parent()
|
||||
.expect("script path has no parent")
|
||||
.to_owned(),
|
||||
Pep723ItemRef::Stdin(..) | Pep723ItemRef::Remote(..) => std::env::current_dir()?,
|
||||
};
|
||||
|
||||
// Collect any `tool.uv.index` from the script.
|
||||
let empty = Vec::default();
|
||||
let script_indexes = match settings.sources {
|
||||
SourceStrategy::Enabled => script
|
||||
.metadata()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.top_level.index.as_deref())
|
||||
.unwrap_or(&empty),
|
||||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
// Collect any `tool.uv.sources` from the script.
|
||||
let empty = BTreeMap::default();
|
||||
let script_sources = match settings.sources {
|
||||
SourceStrategy::Enabled => script
|
||||
.metadata()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.sources.as_ref())
|
||||
.unwrap_or(&empty),
|
||||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
let requirements = dependencies
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let constraints = script
|
||||
.metadata()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.constraint_dependencies.as_ref())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let overrides = script
|
||||
.metadata()
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.override_dependencies.as_ref())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(Some(RequirementsSpecification::from_overrides(
|
||||
requirements,
|
||||
constraints,
|
||||
overrides,
|
||||
)))
|
||||
}
|
||||
|
||||
/// Warn if the user provides (e.g.) an `--index-url` in a requirements file.
|
||||
fn warn_on_requirements_txt_setting(
|
||||
spec: &RequirementsSpecification,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write;
|
||||
use std::io::Read;
|
||||
|
|
@ -18,9 +17,8 @@ use uv_cli::ExternalCommand;
|
|||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{
|
||||
Concurrency, DevGroupsSpecification, DryRun, EditableMode, ExtrasSpecification, InstallOptions,
|
||||
PreviewMode, SourceStrategy, TrustedHost,
|
||||
PreviewMode, TrustedHost,
|
||||
};
|
||||
use uv_distribution::LoweredRequirement;
|
||||
use uv_fs::which::is_executable;
|
||||
use uv_fs::{PythonExt, Simplified};
|
||||
use uv_installer::{SatisfiesResult, SitePackages};
|
||||
|
|
@ -46,9 +44,10 @@ use crate::commands::project::install_target::InstallTarget;
|
|||
use crate::commands::project::lock::LockMode;
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
default_dependency_groups, update_environment, validate_project_requires_python,
|
||||
DependencyGroupsTarget, EnvironmentSpecification, ProjectEnvironment, ProjectError,
|
||||
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
|
||||
default_dependency_groups, script_specification, update_environment,
|
||||
validate_project_requires_python, DependencyGroupsTarget, EnvironmentSpecification,
|
||||
ProjectEnvironment, ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState,
|
||||
WorkspacePython,
|
||||
};
|
||||
use crate::commands::reporters::PythonDownloadReporter;
|
||||
use crate::commands::run::run_to_completion;
|
||||
|
|
@ -211,10 +210,11 @@ pub(crate) async fn run(
|
|||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment();
|
||||
.into_environment()?;
|
||||
|
||||
// Determine the lock mode.
|
||||
let mode = if frozen {
|
||||
|
|
@ -317,98 +317,8 @@ pub(crate) async fn run(
|
|||
);
|
||||
}
|
||||
|
||||
// Determine the working directory for the script.
|
||||
let script_dir = match &script {
|
||||
Pep723Item::Script(script) => std::path::absolute(&script.path)?
|
||||
.parent()
|
||||
.expect("script path has no parent")
|
||||
.to_owned(),
|
||||
Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?,
|
||||
};
|
||||
let metadata = script.metadata();
|
||||
|
||||
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||
if let Some(dependencies) = metadata.dependencies.as_ref() {
|
||||
// Collect any `tool.uv.index` from the script.
|
||||
let empty = Vec::default();
|
||||
let script_indexes = match settings.sources {
|
||||
SourceStrategy::Enabled => metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.top_level.index.as_deref())
|
||||
.unwrap_or(&empty),
|
||||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
// Collect any `tool.uv.sources` from the script.
|
||||
let empty = BTreeMap::default();
|
||||
let script_sources = match settings.sources {
|
||||
SourceStrategy::Enabled => metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.sources.as_ref())
|
||||
.unwrap_or(&empty),
|
||||
SourceStrategy::Disabled => &empty,
|
||||
};
|
||||
|
||||
let requirements = dependencies
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
&settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
let constraints = metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.constraint_dependencies.as_ref())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
&settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let overrides = metadata
|
||||
.tool
|
||||
.as_ref()
|
||||
.and_then(|tool| tool.uv.as_ref())
|
||||
.and_then(|uv| uv.override_dependencies.as_ref())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.flat_map(|requirement| {
|
||||
LoweredRequirement::from_non_workspace_requirement(
|
||||
requirement,
|
||||
script_dir.as_ref(),
|
||||
script_sources,
|
||||
script_indexes,
|
||||
&settings.index_locations,
|
||||
)
|
||||
.map_ok(LoweredRequirement::into_inner)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let spec =
|
||||
RequirementsSpecification::from_overrides(requirements, constraints, overrides);
|
||||
|
||||
if let Some(spec) = script_specification((&script).into(), settings.as_ref().into())? {
|
||||
let environment = ScriptEnvironment::get_or_init(
|
||||
(&script).into(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
|
|
@ -420,10 +330,11 @@ pub(crate) async fn run(
|
|||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
)
|
||||
.await?
|
||||
.into_environment();
|
||||
.into_environment()?;
|
||||
|
||||
match update_environment(
|
||||
environment,
|
||||
|
|
@ -447,6 +358,7 @@ pub(crate) async fn run(
|
|||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ use uv_pep508::{MarkerTree, VersionOrUrl};
|
|||
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
|
||||
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||
use uv_resolver::{FlatIndex, Installable};
|
||||
use uv_scripts::{Pep723ItemRef, Pep723Script};
|
||||
use uv_settings::PythonInstallMirrors;
|
||||
use uv_types::{BuildIsolation, HashStrategy};
|
||||
use uv_warnings::warn_user;
|
||||
|
|
@ -36,8 +38,9 @@ use crate::commands::project::install_target::InstallTarget;
|
|||
use crate::commands::project::lock::{do_safe_lock, LockMode, LockResult};
|
||||
use crate::commands::project::lock_target::LockTarget;
|
||||
use crate::commands::project::{
|
||||
default_dependency_groups, detect_conflicts, DependencyGroupsTarget, PlatformState,
|
||||
ProjectEnvironment, ProjectError, UniversalState,
|
||||
default_dependency_groups, detect_conflicts, script_specification, update_environment,
|
||||
DependencyGroupsTarget, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
|
||||
UniversalState,
|
||||
};
|
||||
use crate::commands::{diagnostics, ExitStatus};
|
||||
use crate::printer::Printer;
|
||||
|
|
@ -63,6 +66,7 @@ pub(crate) async fn sync(
|
|||
python_preference: PythonPreference,
|
||||
python_downloads: PythonDownloads,
|
||||
settings: ResolverInstallerSettings,
|
||||
script: Option<Pep723Script>,
|
||||
installer_metadata: bool,
|
||||
connectivity: Connectivity,
|
||||
concurrency: Concurrency,
|
||||
|
|
@ -73,75 +77,152 @@ pub(crate) async fn sync(
|
|||
printer: Printer,
|
||||
preview: PreviewMode,
|
||||
) -> Result<ExitStatus> {
|
||||
// Identify the project.
|
||||
let project = if frozen {
|
||||
VirtualProject::discover(
|
||||
project_dir,
|
||||
&DiscoveryOptions {
|
||||
members: MemberDiscovery::None,
|
||||
..DiscoveryOptions::default()
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(project_dir, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
// Identify the target.
|
||||
let target = if let Some(script) = script {
|
||||
SyncTarget::Script(script)
|
||||
} else {
|
||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||
// Identify the project.
|
||||
let project = if frozen {
|
||||
VirtualProject::discover(
|
||||
project_dir,
|
||||
&DiscoveryOptions {
|
||||
members: MemberDiscovery::None,
|
||||
..DiscoveryOptions::default()
|
||||
},
|
||||
)
|
||||
.await?
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
VirtualProject::Project(
|
||||
Workspace::discover(project_dir, &DiscoveryOptions::default())
|
||||
.await?
|
||||
.with_current_project(package.clone())
|
||||
.with_context(|| format!("Package `{package}` not found in workspace"))?,
|
||||
)
|
||||
} else {
|
||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||
};
|
||||
|
||||
// TODO(lucab): improve warning content
|
||||
// <https://github.com/astral-sh/uv/issues/7428>
|
||||
if project.workspace().pyproject_toml().has_scripts()
|
||||
&& !project.workspace().pyproject_toml().is_package()
|
||||
{
|
||||
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
|
||||
}
|
||||
|
||||
SyncTarget::Project(project)
|
||||
};
|
||||
|
||||
// Validate that any referenced dependency groups are defined in the workspace.
|
||||
if !frozen {
|
||||
let target = match &project {
|
||||
VirtualProject::Project(project) => {
|
||||
if all_packages {
|
||||
DependencyGroupsTarget::Workspace(project.workspace())
|
||||
} else {
|
||||
DependencyGroupsTarget::Project(project)
|
||||
}
|
||||
match &target {
|
||||
SyncTarget::Project(project) => {
|
||||
let target = match &project {
|
||||
VirtualProject::Project(project) => {
|
||||
if all_packages {
|
||||
DependencyGroupsTarget::Workspace(project.workspace())
|
||||
} else {
|
||||
DependencyGroupsTarget::Project(project)
|
||||
}
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
DependencyGroupsTarget::Workspace(workspace)
|
||||
}
|
||||
};
|
||||
target.validate(&dev)?;
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace),
|
||||
};
|
||||
target.validate(&dev)?;
|
||||
SyncTarget::Script(..) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the default groups to include.
|
||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
||||
|
||||
// TODO(lucab): improve warning content
|
||||
// <https://github.com/astral-sh/uv/issues/7428>
|
||||
if project.workspace().pyproject_toml().has_scripts()
|
||||
&& !project.workspace().pyproject_toml().is_package()
|
||||
{
|
||||
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
|
||||
}
|
||||
let defaults = match &target {
|
||||
SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
|
||||
SyncTarget::Script(..) => Vec::new(),
|
||||
};
|
||||
|
||||
// Discover or create the virtual environment.
|
||||
let environment = ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
active,
|
||||
cache,
|
||||
dry_run,
|
||||
printer,
|
||||
)
|
||||
.await?;
|
||||
let environment = match &target {
|
||||
SyncTarget::Project(project) => SyncEnvironment::Project(
|
||||
ProjectEnvironment::get_or_init(
|
||||
project.workspace(),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
&install_mirrors,
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
no_config,
|
||||
active,
|
||||
cache,
|
||||
dry_run,
|
||||
printer,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
SyncTarget::Script(script) => SyncEnvironment::Script(
|
||||
ScriptEnvironment::get_or_init(
|
||||
Pep723ItemRef::Script(script),
|
||||
python.as_deref().map(PythonRequest::parse),
|
||||
python_preference,
|
||||
python_downloads,
|
||||
connectivity,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
&install_mirrors,
|
||||
no_config,
|
||||
cache,
|
||||
dry_run,
|
||||
printer,
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
};
|
||||
|
||||
// In `--dry-run` mode, print the environment discovery or creation.
|
||||
if dry_run.enabled() {
|
||||
match &environment {
|
||||
ProjectEnvironment::Existing(environment) => {
|
||||
// Notify the user of any environment changes.
|
||||
match &environment {
|
||||
SyncEnvironment::Project(ProjectEnvironment::Existing(environment))
|
||||
if dry_run.enabled() =>
|
||||
{
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Discovered existing environment at: {}",
|
||||
environment.root().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..))
|
||||
if dry_run.enabled() =>
|
||||
{
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would replace existing virtual environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..))
|
||||
if dry_run.enabled() =>
|
||||
{
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would create virtual environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => {
|
||||
if dry_run.enabled() {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
|
|
@ -151,50 +232,102 @@ pub(crate) async fn sync(
|
|||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
ProjectEnvironment::Replaced(environment) => {
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Replaced existing environment at: {}",
|
||||
environment.root().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
"Using script environment at: {}",
|
||||
environment.root().user_display().cyan()
|
||||
)?;
|
||||
}
|
||||
ProjectEnvironment::Created(environment) => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Created new environment at: {}",
|
||||
environment.root().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) if !dry_run.enabled() => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Recreating script environment at: {}",
|
||||
environment.root().user_display().cyan()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Script(ScriptEnvironment::Created(environment)) if !dry_run.enabled() => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Creating script environment at: {}",
|
||||
environment.root().user_display().cyan()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would replace existing script environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would create script environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Special-case: we're syncing a script that doesn't have an associated lockfile. In that case,
|
||||
// we don't create a lockfile, so the resolve-and-install semantics are different.
|
||||
if let SyncTarget::Script(script) = &target {
|
||||
let lockfile = LockTarget::from(script).lock_path();
|
||||
if !lockfile.is_file() {
|
||||
if frozen {
|
||||
return Err(anyhow::anyhow!(
|
||||
"`uv sync --frozen` requires a script lockfile; run `{}` to lock the script",
|
||||
format!("uv lock --script {}", script.path.user_display()).green(),
|
||||
));
|
||||
}
|
||||
ProjectEnvironment::WouldReplace(root, ..) => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would replace existing virtual environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
|
||||
if locked {
|
||||
return Err(anyhow::anyhow!(
|
||||
"`uv sync --locked` requires a script lockfile; run `{}` to lock the script",
|
||||
format!("uv lock --script {}", script.path.user_display()).green(),
|
||||
));
|
||||
}
|
||||
ProjectEnvironment::WouldCreate(root, ..) => {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"{}",
|
||||
format!(
|
||||
"Would create virtual environment at: {}",
|
||||
root.user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
|
||||
let spec =
|
||||
script_specification(Pep723ItemRef::Script(script), settings.as_ref().into())?
|
||||
.unwrap_or_default();
|
||||
match update_environment(
|
||||
Deref::deref(&environment).clone(),
|
||||
spec,
|
||||
modifications,
|
||||
&settings,
|
||||
&PlatformState::default(),
|
||||
Box::new(DefaultResolveLogger),
|
||||
Box::new(DefaultInstallLogger),
|
||||
installer_metadata,
|
||||
connectivity,
|
||||
concurrency,
|
||||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
dry_run,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(..) => return Ok(ExitStatus::Success),
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::native_tls(native_tls)
|
||||
.report(err)
|
||||
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,11 +346,14 @@ pub(crate) async fn sync(
|
|||
LockMode::Write(environment.interpreter())
|
||||
};
|
||||
|
||||
let target = LockTarget::from(project.workspace());
|
||||
let lock_target = match &target {
|
||||
SyncTarget::Project(project) => LockTarget::from(project.workspace()),
|
||||
SyncTarget::Script(script) => LockTarget::from(script),
|
||||
};
|
||||
|
||||
let lock = match do_safe_lock(
|
||||
mode,
|
||||
target,
|
||||
lock_target,
|
||||
settings.as_ref().into(),
|
||||
&state,
|
||||
Box::new(DefaultResolveLogger),
|
||||
|
|
@ -240,7 +376,7 @@ pub(crate) async fn sync(
|
|||
"{}",
|
||||
format!(
|
||||
"Found up-to-date lockfile at: {}",
|
||||
target.lock_path().user_display().bold()
|
||||
lock_target.lock_path().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
|
|
@ -251,7 +387,7 @@ pub(crate) async fn sync(
|
|||
"{}",
|
||||
format!(
|
||||
"Would create lockfile at: {}",
|
||||
target.lock_path().user_display().bold()
|
||||
lock_target.lock_path().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
|
|
@ -262,7 +398,7 @@ pub(crate) async fn sync(
|
|||
"{}",
|
||||
format!(
|
||||
"Would update lockfile at: {}",
|
||||
target.lock_path().user_display().bold()
|
||||
lock_target.lock_path().user_display().bold()
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
|
|
@ -280,55 +416,63 @@ pub(crate) async fn sync(
|
|||
};
|
||||
|
||||
// Identify the installation target.
|
||||
let target = match &project {
|
||||
VirtualProject::Project(project) => {
|
||||
if all_packages {
|
||||
InstallTarget::Workspace {
|
||||
workspace: project.workspace(),
|
||||
lock: &lock,
|
||||
let sync_target = match &target {
|
||||
SyncTarget::Project(project) => {
|
||||
match &project {
|
||||
VirtualProject::Project(project) => {
|
||||
if all_packages {
|
||||
InstallTarget::Workspace {
|
||||
workspace: project.workspace(),
|
||||
lock: &lock,
|
||||
}
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
InstallTarget::Project {
|
||||
workspace: project.workspace(),
|
||||
name: package,
|
||||
lock: &lock,
|
||||
}
|
||||
} else {
|
||||
// By default, install the root package.
|
||||
InstallTarget::Project {
|
||||
workspace: project.workspace(),
|
||||
name: project.project_name(),
|
||||
lock: &lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
InstallTarget::Project {
|
||||
workspace: project.workspace(),
|
||||
name: package,
|
||||
lock: &lock,
|
||||
}
|
||||
} else {
|
||||
// By default, install the root package.
|
||||
InstallTarget::Project {
|
||||
workspace: project.workspace(),
|
||||
name: project.project_name(),
|
||||
lock: &lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
if all_packages {
|
||||
InstallTarget::NonProjectWorkspace {
|
||||
workspace,
|
||||
lock: &lock,
|
||||
}
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
InstallTarget::Project {
|
||||
workspace,
|
||||
name: package,
|
||||
lock: &lock,
|
||||
}
|
||||
} else {
|
||||
// By default, install the entire workspace.
|
||||
InstallTarget::NonProjectWorkspace {
|
||||
workspace,
|
||||
lock: &lock,
|
||||
VirtualProject::NonProject(workspace) => {
|
||||
if all_packages {
|
||||
InstallTarget::NonProjectWorkspace {
|
||||
workspace,
|
||||
lock: &lock,
|
||||
}
|
||||
} else if let Some(package) = package.as_ref() {
|
||||
InstallTarget::Project {
|
||||
workspace,
|
||||
name: package,
|
||||
lock: &lock,
|
||||
}
|
||||
} else {
|
||||
// By default, install the entire workspace.
|
||||
InstallTarget::NonProjectWorkspace {
|
||||
workspace,
|
||||
lock: &lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SyncTarget::Script(script) => InstallTarget::Script {
|
||||
script,
|
||||
lock: &lock,
|
||||
},
|
||||
};
|
||||
|
||||
let state = state.fork();
|
||||
|
||||
// Perform the sync operation.
|
||||
match do_sync(
|
||||
target,
|
||||
sync_target,
|
||||
&environment,
|
||||
&extras,
|
||||
&dev.with_defaults(defaults),
|
||||
|
|
@ -362,6 +506,33 @@ pub(crate) async fn sync(
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum SyncTarget {
|
||||
/// Sync a project environment.
|
||||
Project(VirtualProject),
|
||||
/// Sync a PEP 723 script environment.
|
||||
Script(Pep723Script),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SyncEnvironment {
|
||||
/// A Python environment for a project.
|
||||
Project(ProjectEnvironment),
|
||||
/// A Python environment for a script.
|
||||
Script(ScriptEnvironment),
|
||||
}
|
||||
|
||||
impl Deref for SyncEnvironment {
|
||||
type Target = PythonEnvironment;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Project(environment) => Deref::deref(environment),
|
||||
Self::Script(environment) => Deref::deref(environment),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync a lockfile with an environment.
|
||||
#[allow(clippy::fn_params_excessive_bools)]
|
||||
pub(super) async fn do_sync(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use tracing::{debug, trace};
|
|||
use uv_cache::{Cache, Refresh};
|
||||
use uv_cache_info::Timestamp;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{Concurrency, PreviewMode, Reinstall, TrustedHost, Upgrade};
|
||||
use uv_configuration::{Concurrency, DryRun, PreviewMode, Reinstall, TrustedHost, Upgrade};
|
||||
use uv_distribution_types::{NameRequirementSpecification, UnresolvedRequirementSpecification};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||
|
|
@ -410,6 +410,7 @@ pub(crate) async fn install(
|
|||
native_tls,
|
||||
allow_insecure_host,
|
||||
&cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use tracing::debug;
|
|||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
|
||||
use uv_configuration::{Concurrency, DryRun, PreviewMode, TrustedHost};
|
||||
use uv_fs::CWD;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pypi_types::Requirement;
|
||||
|
|
@ -352,6 +352,7 @@ async fn upgrade_tool(
|
|||
native_tls,
|
||||
allow_insecure_host,
|
||||
cache,
|
||||
DryRun::Disabled,
|
||||
printer,
|
||||
preview,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -201,6 +201,10 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
script: Some(script),
|
||||
..
|
||||
})
|
||||
| ProjectCommand::Sync(uv_cli::SyncArgs {
|
||||
script: Some(script),
|
||||
..
|
||||
})
|
||||
| ProjectCommand::Tree(uv_cli::TreeArgs {
|
||||
script: Some(script),
|
||||
..
|
||||
|
|
@ -1527,7 +1531,14 @@ async fn run_project(
|
|||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
||||
);
|
||||
|
||||
commands::sync(
|
||||
// Unwrap the script.
|
||||
let script = script.map(|script| match script {
|
||||
Pep723Item::Script(script) => script,
|
||||
Pep723Item::Stdin(..) => unreachable!("`uv lock` does not support stdin"),
|
||||
Pep723Item::Remote(..) => unreachable!("`uv lock` does not support remote files"),
|
||||
});
|
||||
|
||||
Box::pin(commands::sync(
|
||||
project_dir,
|
||||
args.locked,
|
||||
args.frozen,
|
||||
|
|
@ -1545,6 +1556,7 @@ async fn run_project(
|
|||
globals.python_preference,
|
||||
globals.python_downloads,
|
||||
args.settings,
|
||||
script,
|
||||
globals.installer_metadata,
|
||||
globals.connectivity,
|
||||
globals.concurrency,
|
||||
|
|
@ -1554,7 +1566,7 @@ async fn run_project(
|
|||
&cache,
|
||||
printer,
|
||||
globals.preview,
|
||||
)
|
||||
))
|
||||
.await
|
||||
}
|
||||
ProjectCommand::Lock(args) => {
|
||||
|
|
|
|||
|
|
@ -959,6 +959,7 @@ pub(crate) struct SyncSettings {
|
|||
pub(crate) locked: bool,
|
||||
pub(crate) frozen: bool,
|
||||
pub(crate) dry_run: DryRun,
|
||||
pub(crate) script: Option<PathBuf>,
|
||||
pub(crate) active: Option<bool>,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) dev: DevGroupsSpecification,
|
||||
|
|
@ -1006,6 +1007,7 @@ impl SyncSettings {
|
|||
refresh,
|
||||
all_packages,
|
||||
package,
|
||||
script,
|
||||
python,
|
||||
} = args;
|
||||
let install_mirrors = filesystem
|
||||
|
|
@ -1022,6 +1024,7 @@ impl SyncSettings {
|
|||
locked,
|
||||
frozen,
|
||||
dry_run: DryRun::from_args(dry_run),
|
||||
script,
|
||||
active: flag(active, no_active),
|
||||
extras: ExtrasSpecification::from_args(
|
||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
|
|
|
|||
|
|
@ -6684,3 +6684,379 @@ fn sync_dry_run() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_script() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
|
||||
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain(vec![(
|
||||
r"environments-v1/script-\w+",
|
||||
"environments-v1/script-[HASH]",
|
||||
)])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Creating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// If a lockfile didn't exist already, `uv sync --script` shouldn't create one.
|
||||
assert!(!context.temp_dir.child("uv.lock").exists());
|
||||
|
||||
// Modify the script's dependencies.
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# "iniconfig",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
|
||||
// Modify the `requires-python`.
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.8, <3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# "iniconfig",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Recreating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ iniconfig==2.0.0
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
"###);
|
||||
|
||||
// `--locked` and `--frozen` should fail with helpful error messages.
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
error: `uv sync --locked` requires a script lockfile; run `uv lock --script script.py` to lock the script
|
||||
"###);
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--frozen"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
error: `uv sync --frozen` requires a script lockfile; run `uv lock --script script.py` to lock the script
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_locked_script() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
|
||||
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
let filters = context
|
||||
.filters()
|
||||
.into_iter()
|
||||
.chain(vec![(
|
||||
r"environments-v1/script-\w+",
|
||||
"environments-v1/script-[HASH]",
|
||||
)])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Lock the script.
|
||||
uv_snapshot!(&filters, context.lock().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
"###);
|
||||
|
||||
let lock = context.read("script.py.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
requirements = [{ name = "anyio" }]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Creating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 3 packages in [TIME]
|
||||
Installed 3 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// Modify the script's dependencies.
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# "iniconfig",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 4 packages in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 4 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
|
||||
let lock = context.read("script.py.lock");
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
lock, @r###"
|
||||
version = 1
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[options]
|
||||
exclude-newer = "2024-03-25T00:00:00Z"
|
||||
|
||||
[manifest]
|
||||
requirements = [
|
||||
{ name = "anyio" },
|
||||
{ name = "iniconfig" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Modify the `requires-python`.
|
||||
script.write_str(indoc! { r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.8, <3.11"
|
||||
# dependencies = [
|
||||
# "anyio",
|
||||
# "iniconfig",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import anyio
|
||||
"#
|
||||
})?;
|
||||
|
||||
// Re-run with `--locked`.
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Recreating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 6 packages in [TIME]
|
||||
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
|
||||
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 2 packages in [TIME]
|
||||
Installed 6 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ exceptiongroup==1.2.0
|
||||
+ idna==3.6
|
||||
+ iniconfig==2.0.0
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1868,6 +1868,10 @@ uv sync [OPTIONS]
|
|||
|
||||
<li><code>lowest-direct</code>: Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies</li>
|
||||
</ul>
|
||||
</dd><dt><code>--script</code> <i>script</i></dt><dd><p>Sync the environment for a Python script, rather than the current project.</p>
|
||||
|
||||
<p>If provided, uv will sync the dependencies based on the script’s inline metadata table, in adherence with PEP 723.</p>
|
||||
|
||||
</dd><dt><code>--upgrade</code>, <code>-U</code></dt><dd><p>Allow package upgrades, ignoring pinned versions in any existing output file. Implies <code>--refresh</code></p>
|
||||
|
||||
</dd><dt><code>--upgrade-package</code>, <code>-P</code> <i>upgrade-package</i></dt><dd><p>Allow upgrades for a specific package, ignoring pinned versions in any existing output file. Implies <code>--refresh-package</code></p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue