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")]
|
#[arg(long, conflicts_with = "all_packages")]
|
||||||
pub package: Option<PackageName>,
|
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.
|
/// The Python interpreter to use for the project environment.
|
||||||
///
|
///
|
||||||
/// By default, the first interpreter that meets the project's `requires-python` constraint is
|
/// 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 error::Error;
|
||||||
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
|
||||||
pub use metadata::{
|
pub use metadata::{
|
||||||
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, Metadata, MetadataError,
|
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata,
|
||||||
RequiresDist,
|
MetadataError, RequiresDist,
|
||||||
};
|
};
|
||||||
pub use reporter::Reporter;
|
pub use reporter::Reporter;
|
||||||
pub use source::prune;
|
pub use source::prune;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use uv_workspace::WorkspaceError;
|
||||||
|
|
||||||
pub use crate::metadata::build_requires::BuildRequires;
|
pub use crate::metadata::build_requires::BuildRequires;
|
||||||
pub use crate::metadata::lowering::LoweredRequirement;
|
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};
|
pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
|
||||||
|
|
||||||
mod build_requires;
|
mod build_requires;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::ops::Deref;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -14,10 +13,10 @@ use uv_cache_key::cache_digest;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DryRun,
|
Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DryRun,
|
||||||
ExtrasSpecification, PreviewMode, Reinstall, TrustedHost, Upgrade,
|
ExtrasSpecification, PreviewMode, Reinstall, SourceStrategy, TrustedHost, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_dispatch::{BuildDispatch, SharedState};
|
use uv_dispatch::{BuildDispatch, SharedState};
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::{DistributionDatabase, LoweredRequirement};
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
|
||||||
};
|
};
|
||||||
|
|
@ -227,6 +226,9 @@ pub(crate) enum ProjectError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Metadata(#[from] uv_distribution::MetadataError),
|
Metadata(#[from] uv_distribution::MetadataError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Lowering(#[from] uv_distribution::LoweringError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
|
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;
|
type Target = PythonEnvironment;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
|
@ -1240,7 +1242,30 @@ impl Deref for ProjectEnvironment {
|
||||||
|
|
||||||
/// The Python environment for a script.
|
/// The Python environment for a script.
|
||||||
#[derive(Debug)]
|
#[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 {
|
impl ScriptEnvironment {
|
||||||
/// Initialize a virtual environment for a PEP 723 script.
|
/// Initialize a virtual environment for a PEP 723 script.
|
||||||
|
|
@ -1255,6 +1280,7 @@ impl ScriptEnvironment {
|
||||||
install_mirrors: &PythonInstallMirrors,
|
install_mirrors: &PythonInstallMirrors,
|
||||||
no_config: bool,
|
no_config: bool,
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
|
dry_run: DryRun,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<Self, ProjectError> {
|
) -> Result<Self, ProjectError> {
|
||||||
// Lock the script environment to avoid synchronization issues.
|
// Lock the script environment to avoid synchronization issues.
|
||||||
|
|
@ -1276,29 +1302,12 @@ impl ScriptEnvironment {
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
// If we found an existing, compatible environment, use it.
|
// 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.
|
// Otherwise, create a virtual environment with the discovered interpreter.
|
||||||
ScriptInterpreter::Interpreter(interpreter) => {
|
ScriptInterpreter::Interpreter(interpreter) => {
|
||||||
let root = ScriptInterpreter::root(script, cache);
|
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:
|
// Determine a prompt for the environment, in order of preference:
|
||||||
//
|
//
|
||||||
// 1) The name of the script
|
// 1) The name of the script
|
||||||
|
|
@ -1310,6 +1319,43 @@ impl ScriptEnvironment {
|
||||||
.map(uv_virtualenv::Prompt::Static)
|
.map(uv_virtualenv::Prompt::Static)
|
||||||
.unwrap_or(uv_virtualenv::Prompt::None);
|
.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(
|
let environment = uv_virtualenv::create_venv(
|
||||||
&root,
|
&root,
|
||||||
interpreter,
|
interpreter,
|
||||||
|
|
@ -1320,14 +1366,42 @@ impl ScriptEnvironment {
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(Self(environment))
|
Ok(if replaced {
|
||||||
|
Self::Replaced(environment)
|
||||||
|
} else {
|
||||||
|
Self::Created(environment)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert the [`ScriptEnvironment`] into a [`PythonEnvironment`].
|
/// 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,
|
native_tls: bool,
|
||||||
allow_insecure_host: &[TrustedHost],
|
allow_insecure_host: &[TrustedHost],
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
|
dry_run: DryRun,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<EnvironmentUpdate, ProjectError> {
|
) -> Result<EnvironmentUpdate, ProjectError> {
|
||||||
|
|
@ -1954,7 +2029,6 @@ pub(crate) async fn update_environment(
|
||||||
// optional on the downstream APIs.
|
// optional on the downstream APIs.
|
||||||
let build_constraints = Constraints::default();
|
let build_constraints = Constraints::default();
|
||||||
let build_hasher = HashStrategy::default();
|
let build_hasher = HashStrategy::default();
|
||||||
let dry_run = DryRun::default();
|
|
||||||
let extras = ExtrasSpecification::default();
|
let extras = ExtrasSpecification::default();
|
||||||
let groups = DevGroupsSpecification::default();
|
let groups = DevGroupsSpecification::default();
|
||||||
let hasher = HashStrategy::default();
|
let hasher = HashStrategy::default();
|
||||||
|
|
@ -2225,6 +2299,113 @@ pub(crate) fn detect_conflicts(
|
||||||
Ok(())
|
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.
|
/// Warn if the user provides (e.g.) an `--index-url` in a requirements file.
|
||||||
fn warn_on_requirements_txt_setting(
|
fn warn_on_requirements_txt_setting(
|
||||||
spec: &RequirementsSpecification,
|
spec: &RequirementsSpecification,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
@ -18,9 +17,8 @@ use uv_cli::ExternalCommand;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
Concurrency, DevGroupsSpecification, DryRun, EditableMode, ExtrasSpecification, InstallOptions,
|
Concurrency, DevGroupsSpecification, DryRun, EditableMode, ExtrasSpecification, InstallOptions,
|
||||||
PreviewMode, SourceStrategy, TrustedHost,
|
PreviewMode, TrustedHost,
|
||||||
};
|
};
|
||||||
use uv_distribution::LoweredRequirement;
|
|
||||||
use uv_fs::which::is_executable;
|
use uv_fs::which::is_executable;
|
||||||
use uv_fs::{PythonExt, Simplified};
|
use uv_fs::{PythonExt, Simplified};
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
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::LockMode;
|
||||||
use crate::commands::project::lock_target::LockTarget;
|
use crate::commands::project::lock_target::LockTarget;
|
||||||
use crate::commands::project::{
|
use crate::commands::project::{
|
||||||
default_dependency_groups, update_environment, validate_project_requires_python,
|
default_dependency_groups, script_specification, update_environment,
|
||||||
DependencyGroupsTarget, EnvironmentSpecification, ProjectEnvironment, ProjectError,
|
validate_project_requires_python, DependencyGroupsTarget, EnvironmentSpecification,
|
||||||
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
|
ProjectEnvironment, ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState,
|
||||||
|
WorkspacePython,
|
||||||
};
|
};
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
use crate::commands::run::run_to_completion;
|
use crate::commands::run::run_to_completion;
|
||||||
|
|
@ -211,10 +210,11 @@ pub(crate) async fn run(
|
||||||
&install_mirrors,
|
&install_mirrors,
|
||||||
no_config,
|
no_config,
|
||||||
cache,
|
cache,
|
||||||
|
DryRun::Disabled,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_environment();
|
.into_environment()?;
|
||||||
|
|
||||||
// Determine the lock mode.
|
// Determine the lock mode.
|
||||||
let mode = if frozen {
|
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.
|
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
|
||||||
if let Some(dependencies) = metadata.dependencies.as_ref() {
|
if let Some(spec) = script_specification((&script).into(), settings.as_ref().into())? {
|
||||||
// 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);
|
|
||||||
|
|
||||||
let environment = ScriptEnvironment::get_or_init(
|
let environment = ScriptEnvironment::get_or_init(
|
||||||
(&script).into(),
|
(&script).into(),
|
||||||
python.as_deref().map(PythonRequest::parse),
|
python.as_deref().map(PythonRequest::parse),
|
||||||
|
|
@ -420,10 +330,11 @@ pub(crate) async fn run(
|
||||||
&install_mirrors,
|
&install_mirrors,
|
||||||
no_config,
|
no_config,
|
||||||
cache,
|
cache,
|
||||||
|
DryRun::Disabled,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_environment();
|
.into_environment()?;
|
||||||
|
|
||||||
match update_environment(
|
match update_environment(
|
||||||
environment,
|
environment,
|
||||||
|
|
@ -447,6 +358,7 @@ pub(crate) async fn run(
|
||||||
native_tls,
|
native_tls,
|
||||||
allow_insecure_host,
|
allow_insecure_host,
|
||||||
cache,
|
cache,
|
||||||
|
DryRun::Disabled,
|
||||||
printer,
|
printer,
|
||||||
preview,
|
preview,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -23,6 +24,7 @@ use uv_pep508::{MarkerTree, VersionOrUrl};
|
||||||
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
|
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
|
||||||
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
|
||||||
use uv_resolver::{FlatIndex, Installable};
|
use uv_resolver::{FlatIndex, Installable};
|
||||||
|
use uv_scripts::{Pep723ItemRef, Pep723Script};
|
||||||
use uv_settings::PythonInstallMirrors;
|
use uv_settings::PythonInstallMirrors;
|
||||||
use uv_types::{BuildIsolation, HashStrategy};
|
use uv_types::{BuildIsolation, HashStrategy};
|
||||||
use uv_warnings::warn_user;
|
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::{do_safe_lock, LockMode, LockResult};
|
||||||
use crate::commands::project::lock_target::LockTarget;
|
use crate::commands::project::lock_target::LockTarget;
|
||||||
use crate::commands::project::{
|
use crate::commands::project::{
|
||||||
default_dependency_groups, detect_conflicts, DependencyGroupsTarget, PlatformState,
|
default_dependency_groups, detect_conflicts, script_specification, update_environment,
|
||||||
ProjectEnvironment, ProjectError, UniversalState,
|
DependencyGroupsTarget, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
|
||||||
|
UniversalState,
|
||||||
};
|
};
|
||||||
use crate::commands::{diagnostics, ExitStatus};
|
use crate::commands::{diagnostics, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
@ -63,6 +66,7 @@ pub(crate) async fn sync(
|
||||||
python_preference: PythonPreference,
|
python_preference: PythonPreference,
|
||||||
python_downloads: PythonDownloads,
|
python_downloads: PythonDownloads,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
|
script: Option<Pep723Script>,
|
||||||
installer_metadata: bool,
|
installer_metadata: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
concurrency: Concurrency,
|
concurrency: Concurrency,
|
||||||
|
|
@ -73,6 +77,10 @@ pub(crate) async fn sync(
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
// Identify the target.
|
||||||
|
let target = if let Some(script) = script {
|
||||||
|
SyncTarget::Script(script)
|
||||||
|
} else {
|
||||||
// Identify the project.
|
// Identify the project.
|
||||||
let project = if frozen {
|
let project = if frozen {
|
||||||
VirtualProject::discover(
|
VirtualProject::discover(
|
||||||
|
|
@ -94,24 +102,6 @@ pub(crate) async fn sync(
|
||||||
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace),
|
|
||||||
};
|
|
||||||
target.validate(&dev)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the default groups to include.
|
|
||||||
let defaults = default_dependency_groups(project.pyproject_toml())?;
|
|
||||||
|
|
||||||
// TODO(lucab): improve warning content
|
// TODO(lucab): improve warning content
|
||||||
// <https://github.com/astral-sh/uv/issues/7428>
|
// <https://github.com/astral-sh/uv/issues/7428>
|
||||||
if project.workspace().pyproject_toml().has_scripts()
|
if project.workspace().pyproject_toml().has_scripts()
|
||||||
|
|
@ -120,8 +110,41 @@ pub(crate) async fn sync(
|
||||||
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`");
|
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 {
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
SyncTarget::Script(..) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the default groups to include.
|
||||||
|
let defaults = match &target {
|
||||||
|
SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
|
||||||
|
SyncTarget::Script(..) => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
// Discover or create the virtual environment.
|
// Discover or create the virtual environment.
|
||||||
let environment = ProjectEnvironment::get_or_init(
|
let environment = match &target {
|
||||||
|
SyncTarget::Project(project) => SyncEnvironment::Project(
|
||||||
|
ProjectEnvironment::get_or_init(
|
||||||
project.workspace(),
|
project.workspace(),
|
||||||
python.as_deref().map(PythonRequest::parse),
|
python.as_deref().map(PythonRequest::parse),
|
||||||
&install_mirrors,
|
&install_mirrors,
|
||||||
|
|
@ -136,12 +159,32 @@ pub(crate) async fn sync(
|
||||||
dry_run,
|
dry_run,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await?;
|
.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.
|
// Notify the user of any environment changes.
|
||||||
if dry_run.enabled() {
|
|
||||||
match &environment {
|
match &environment {
|
||||||
ProjectEnvironment::Existing(environment) => {
|
SyncEnvironment::Project(ProjectEnvironment::Existing(environment))
|
||||||
|
if dry_run.enabled() =>
|
||||||
|
{
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -152,29 +195,9 @@ pub(crate) async fn sync(
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ProjectEnvironment::Replaced(environment) => {
|
SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..))
|
||||||
writeln!(
|
if dry_run.enabled() =>
|
||||||
printer.stderr(),
|
{
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Replaced existing environment at: {}",
|
|
||||||
environment.root().user_display().bold()
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
ProjectEnvironment::Created(environment) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Created new environment at: {}",
|
|
||||||
environment.root().user_display().bold()
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
ProjectEnvironment::WouldReplace(root, ..) => {
|
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -185,7 +208,9 @@ pub(crate) async fn sync(
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ProjectEnvironment::WouldCreate(root, ..) => {
|
SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..))
|
||||||
|
if dry_run.enabled() =>
|
||||||
|
{
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -196,6 +221,114 @@ pub(crate) async fn sync(
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => {
|
||||||
|
if dry_run.enabled() {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Discovered existing environment at: {}",
|
||||||
|
environment.root().user_display().bold()
|
||||||
|
)
|
||||||
|
.dimmed()
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"Using script environment at: {}",
|
||||||
|
environment.root().user_display().cyan()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
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(
|
let lock = match do_safe_lock(
|
||||||
mode,
|
mode,
|
||||||
target,
|
lock_target,
|
||||||
settings.as_ref().into(),
|
settings.as_ref().into(),
|
||||||
&state,
|
&state,
|
||||||
Box::new(DefaultResolveLogger),
|
Box::new(DefaultResolveLogger),
|
||||||
|
|
@ -240,7 +376,7 @@ pub(crate) async fn sync(
|
||||||
"{}",
|
"{}",
|
||||||
format!(
|
format!(
|
||||||
"Found up-to-date lockfile at: {}",
|
"Found up-to-date lockfile at: {}",
|
||||||
target.lock_path().user_display().bold()
|
lock_target.lock_path().user_display().bold()
|
||||||
)
|
)
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -251,7 +387,7 @@ pub(crate) async fn sync(
|
||||||
"{}",
|
"{}",
|
||||||
format!(
|
format!(
|
||||||
"Would create lockfile at: {}",
|
"Would create lockfile at: {}",
|
||||||
target.lock_path().user_display().bold()
|
lock_target.lock_path().user_display().bold()
|
||||||
)
|
)
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -262,7 +398,7 @@ pub(crate) async fn sync(
|
||||||
"{}",
|
"{}",
|
||||||
format!(
|
format!(
|
||||||
"Would update lockfile at: {}",
|
"Would update lockfile at: {}",
|
||||||
target.lock_path().user_display().bold()
|
lock_target.lock_path().user_display().bold()
|
||||||
)
|
)
|
||||||
.dimmed()
|
.dimmed()
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -280,7 +416,9 @@ pub(crate) async fn sync(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Identify the installation target.
|
// Identify the installation target.
|
||||||
let target = match &project {
|
let sync_target = match &target {
|
||||||
|
SyncTarget::Project(project) => {
|
||||||
|
match &project {
|
||||||
VirtualProject::Project(project) => {
|
VirtualProject::Project(project) => {
|
||||||
if all_packages {
|
if all_packages {
|
||||||
InstallTarget::Workspace {
|
InstallTarget::Workspace {
|
||||||
|
|
@ -322,13 +460,19 @@ pub(crate) async fn sync(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SyncTarget::Script(script) => InstallTarget::Script {
|
||||||
|
script,
|
||||||
|
lock: &lock,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = state.fork();
|
let state = state.fork();
|
||||||
|
|
||||||
// Perform the sync operation.
|
// Perform the sync operation.
|
||||||
match do_sync(
|
match do_sync(
|
||||||
target,
|
sync_target,
|
||||||
&environment,
|
&environment,
|
||||||
&extras,
|
&extras,
|
||||||
&dev.with_defaults(defaults),
|
&dev.with_defaults(defaults),
|
||||||
|
|
@ -362,6 +506,33 @@ pub(crate) async fn sync(
|
||||||
Ok(ExitStatus::Success)
|
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.
|
/// Sync a lockfile with an environment.
|
||||||
#[allow(clippy::fn_params_excessive_bools)]
|
#[allow(clippy::fn_params_excessive_bools)]
|
||||||
pub(super) async fn do_sync(
|
pub(super) async fn do_sync(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use tracing::{debug, trace};
|
||||||
use uv_cache::{Cache, Refresh};
|
use uv_cache::{Cache, Refresh};
|
||||||
use uv_cache_info::Timestamp;
|
use uv_cache_info::Timestamp;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
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_distribution_types::{NameRequirementSpecification, UnresolvedRequirementSpecification};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||||
|
|
@ -410,6 +410,7 @@ pub(crate) async fn install(
|
||||||
native_tls,
|
native_tls,
|
||||||
allow_insecure_host,
|
allow_insecure_host,
|
||||||
&cache,
|
&cache,
|
||||||
|
DryRun::Disabled,
|
||||||
printer,
|
printer,
|
||||||
preview,
|
preview,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use tracing::debug;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
|
use uv_configuration::{Concurrency, DryRun, PreviewMode, TrustedHost};
|
||||||
use uv_fs::CWD;
|
use uv_fs::CWD;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_pypi_types::Requirement;
|
use uv_pypi_types::Requirement;
|
||||||
|
|
@ -352,6 +352,7 @@ async fn upgrade_tool(
|
||||||
native_tls,
|
native_tls,
|
||||||
allow_insecure_host,
|
allow_insecure_host,
|
||||||
cache,
|
cache,
|
||||||
|
DryRun::Disabled,
|
||||||
printer,
|
printer,
|
||||||
preview,
|
preview,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,10 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
script: Some(script),
|
script: Some(script),
|
||||||
..
|
..
|
||||||
})
|
})
|
||||||
|
| ProjectCommand::Sync(uv_cli::SyncArgs {
|
||||||
|
script: Some(script),
|
||||||
|
..
|
||||||
|
})
|
||||||
| ProjectCommand::Tree(uv_cli::TreeArgs {
|
| ProjectCommand::Tree(uv_cli::TreeArgs {
|
||||||
script: Some(script),
|
script: Some(script),
|
||||||
..
|
..
|
||||||
|
|
@ -1527,7 +1531,14 @@ async fn run_project(
|
||||||
.combine(Refresh::from(args.settings.upgrade.clone())),
|
.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,
|
project_dir,
|
||||||
args.locked,
|
args.locked,
|
||||||
args.frozen,
|
args.frozen,
|
||||||
|
|
@ -1545,6 +1556,7 @@ async fn run_project(
|
||||||
globals.python_preference,
|
globals.python_preference,
|
||||||
globals.python_downloads,
|
globals.python_downloads,
|
||||||
args.settings,
|
args.settings,
|
||||||
|
script,
|
||||||
globals.installer_metadata,
|
globals.installer_metadata,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
globals.concurrency,
|
globals.concurrency,
|
||||||
|
|
@ -1554,7 +1566,7 @@ async fn run_project(
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
)
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
ProjectCommand::Lock(args) => {
|
ProjectCommand::Lock(args) => {
|
||||||
|
|
|
||||||
|
|
@ -959,6 +959,7 @@ pub(crate) struct SyncSettings {
|
||||||
pub(crate) locked: bool,
|
pub(crate) locked: bool,
|
||||||
pub(crate) frozen: bool,
|
pub(crate) frozen: bool,
|
||||||
pub(crate) dry_run: DryRun,
|
pub(crate) dry_run: DryRun,
|
||||||
|
pub(crate) script: Option<PathBuf>,
|
||||||
pub(crate) active: Option<bool>,
|
pub(crate) active: Option<bool>,
|
||||||
pub(crate) extras: ExtrasSpecification,
|
pub(crate) extras: ExtrasSpecification,
|
||||||
pub(crate) dev: DevGroupsSpecification,
|
pub(crate) dev: DevGroupsSpecification,
|
||||||
|
|
@ -1006,6 +1007,7 @@ impl SyncSettings {
|
||||||
refresh,
|
refresh,
|
||||||
all_packages,
|
all_packages,
|
||||||
package,
|
package,
|
||||||
|
script,
|
||||||
python,
|
python,
|
||||||
} = args;
|
} = args;
|
||||||
let install_mirrors = filesystem
|
let install_mirrors = filesystem
|
||||||
|
|
@ -1022,6 +1024,7 @@ impl SyncSettings {
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
dry_run: DryRun::from_args(dry_run),
|
dry_run: DryRun::from_args(dry_run),
|
||||||
|
script,
|
||||||
active: flag(active, no_active),
|
active: flag(active, no_active),
|
||||||
extras: ExtrasSpecification::from_args(
|
extras: ExtrasSpecification::from_args(
|
||||||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||||
|
|
|
||||||
|
|
@ -6684,3 +6684,379 @@ fn sync_dry_run() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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>
|
<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>
|
</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</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>
|
</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