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:
Charlie Marsh 2025-02-12 11:02:16 -05:00 committed by GitHub
parent 0b4a349173
commit 792dc9d1c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 961 additions and 274 deletions

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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,
) )

View File

@ -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(

View File

@ -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,
) )

View File

@ -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,
) )

View File

@ -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) => {

View File

@ -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(),

View File

@ -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(())
}

View File

@ -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&#8217;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>