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")]
pub package: Option<PackageName>,
/// Sync the environment for a Python script, rather than the current project.
///
/// If provided, uv will sync the dependencies based on the script's inline metadata table, in
/// adherence with PEP 723.
#[arg(
long,
conflicts_with = "active",
conflicts_with = "all_packages",
conflicts_with = "package",
conflicts_with = "no_install_project",
conflicts_with = "no_install_workspace",
conflicts_with = "extra",
conflicts_with = "all_extras",
conflicts_with = "no_extra",
conflicts_with = "no_all_extras",
conflicts_with = "dev",
conflicts_with = "no_dev",
conflicts_with = "only_dev",
conflicts_with = "group",
conflicts_with = "no_group",
conflicts_with = "no_default_groups",
conflicts_with = "only_group",
conflicts_with = "all_groups"
)]
pub script: Option<PathBuf>,
/// The Python interpreter to use for the project environment.
///
/// By default, the first interpreter that meets the project's `requires-python` constraint is

View File

@ -3,8 +3,8 @@ pub use download::LocalWheel;
pub use error::Error;
pub use index::{BuiltWheelIndex, RegistryWheelIndex};
pub use metadata::{
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, Metadata, MetadataError,
RequiresDist,
ArchiveMetadata, BuildRequires, FlatRequiresDist, LoweredRequirement, LoweringError, Metadata,
MetadataError, RequiresDist,
};
pub use reporter::Reporter;
pub use source::prune;

View File

@ -13,7 +13,7 @@ use uv_workspace::WorkspaceError;
pub use crate::metadata::build_requires::BuildRequires;
pub use crate::metadata::lowering::LoweredRequirement;
use crate::metadata::lowering::LoweringError;
pub use crate::metadata::lowering::LoweringError;
pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
mod build_requires;

View File

@ -1,7 +1,6 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@ -14,10 +13,10 @@ use uv_cache_key::cache_digest;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DryRun,
ExtrasSpecification, PreviewMode, Reinstall, TrustedHost, Upgrade,
ExtrasSpecification, PreviewMode, Reinstall, SourceStrategy, TrustedHost, Upgrade,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::DistributionDatabase;
use uv_distribution::{DistributionDatabase, LoweredRequirement};
use uv_distribution_types::{
Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification,
};
@ -227,6 +226,9 @@ pub(crate) enum ProjectError {
#[error(transparent)]
Metadata(#[from] uv_distribution::MetadataError),
#[error(transparent)]
Lowering(#[from] uv_distribution::LoweringError),
#[error(transparent)]
PyprojectMut(#[from] uv_workspace::pyproject_mut::Error),
@ -1224,7 +1226,7 @@ impl ProjectEnvironment {
}
}
impl Deref for ProjectEnvironment {
impl std::ops::Deref for ProjectEnvironment {
type Target = PythonEnvironment;
fn deref(&self) -> &Self::Target {
@ -1240,7 +1242,30 @@ impl Deref for ProjectEnvironment {
/// The Python environment for a script.
#[derive(Debug)]
struct ScriptEnvironment(PythonEnvironment);
enum ScriptEnvironment {
/// An existing [`PythonEnvironment`] was discovered, which satisfies the script's requirements.
Existing(PythonEnvironment),
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the script's
/// requirements, and so was replaced.
Replaced(PythonEnvironment),
/// A new [`PythonEnvironment`] was created for the script.
Created(PythonEnvironment),
/// An existing [`PythonEnvironment`] was discovered, but did not satisfy the script's
/// requirements. A new environment would've been created, but `--dry-run` mode is enabled; as
/// such, a temporary environment was created instead.
WouldReplace(
PathBuf,
PythonEnvironment,
#[allow(unused)] tempfile::TempDir,
),
/// A new [`PythonEnvironment`] would've been created, but `--dry-run` mode is enabled; as such,
/// a temporary environment was created instead.
WouldCreate(
PathBuf,
PythonEnvironment,
#[allow(unused)] tempfile::TempDir,
),
}
impl ScriptEnvironment {
/// Initialize a virtual environment for a PEP 723 script.
@ -1255,6 +1280,7 @@ impl ScriptEnvironment {
install_mirrors: &PythonInstallMirrors,
no_config: bool,
cache: &Cache,
dry_run: DryRun,
printer: Printer,
) -> Result<Self, ProjectError> {
// Lock the script environment to avoid synchronization issues.
@ -1276,29 +1302,12 @@ impl ScriptEnvironment {
.await?
{
// If we found an existing, compatible environment, use it.
ScriptInterpreter::Environment(environment) => Ok(Self(environment)),
ScriptInterpreter::Environment(environment) => Ok(Self::Existing(environment)),
// Otherwise, create a virtual environment with the discovered interpreter.
ScriptInterpreter::Interpreter(interpreter) => {
let root = ScriptInterpreter::root(script, cache);
// Remove the existing virtual environment.
match fs_err::remove_dir_all(&root) {
Ok(()) => {
debug!(
"Removed virtual environment at: {}",
root.user_display().cyan()
);
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
};
debug!(
"Creating script environment at: {}",
root.user_display().cyan()
);
// Determine a prompt for the environment, in order of preference:
//
// 1) The name of the script
@ -1310,6 +1319,43 @@ impl ScriptEnvironment {
.map(uv_virtualenv::Prompt::Static)
.unwrap_or(uv_virtualenv::Prompt::None);
// Under `--dry-run`, avoid modifying the environment.
if dry_run.enabled() {
let temp_dir = cache.venv_dir()?;
let environment = uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
prompt,
false,
false,
false,
false,
)?;
return Ok(if root.exists() {
Self::WouldReplace(root, environment, temp_dir)
} else {
Self::WouldCreate(root, environment, temp_dir)
});
}
// Remove the existing virtual environment.
let replaced = match fs_err::remove_dir_all(&root) {
Ok(()) => {
debug!(
"Removed virtual environment at: {}",
root.user_display().cyan()
);
true
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
Err(err) => return Err(err.into()),
};
debug!(
"Creating script environment at: {}",
root.user_display().cyan()
);
let environment = uv_virtualenv::create_venv(
&root,
interpreter,
@ -1320,14 +1366,42 @@ impl ScriptEnvironment {
false,
)?;
Ok(Self(environment))
Ok(if replaced {
Self::Replaced(environment)
} else {
Self::Created(environment)
})
}
}
}
/// Convert the [`ScriptEnvironment`] into a [`PythonEnvironment`].
pub(crate) fn into_environment(self) -> PythonEnvironment {
self.0
///
/// Returns an error if the environment was created in `--dry-run` mode, as dropping the
/// associated temporary directory could lead to errors downstream.
#[allow(clippy::result_large_err)]
pub(crate) fn into_environment(self) -> Result<PythonEnvironment, ProjectError> {
match self {
Self::Existing(environment) => Ok(environment),
Self::Replaced(environment) => Ok(environment),
Self::Created(environment) => Ok(environment),
Self::WouldReplace(..) => Err(ProjectError::DroppedEnvironment),
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
}
}
}
impl std::ops::Deref for ScriptEnvironment {
type Target = PythonEnvironment;
fn deref(&self) -> &Self::Target {
match self {
Self::Existing(environment) => environment,
Self::Replaced(environment) => environment,
Self::Created(environment) => environment,
Self::WouldReplace(_, environment, _) => environment,
Self::WouldCreate(_, environment, _) => environment,
}
}
}
@ -1839,6 +1913,7 @@ pub(crate) async fn update_environment(
native_tls: bool,
allow_insecure_host: &[TrustedHost],
cache: &Cache,
dry_run: DryRun,
printer: Printer,
preview: PreviewMode,
) -> Result<EnvironmentUpdate, ProjectError> {
@ -1954,7 +2029,6 @@ pub(crate) async fn update_environment(
// optional on the downstream APIs.
let build_constraints = Constraints::default();
let build_hasher = HashStrategy::default();
let dry_run = DryRun::default();
let extras = ExtrasSpecification::default();
let groups = DevGroupsSpecification::default();
let hasher = HashStrategy::default();
@ -2225,6 +2299,113 @@ pub(crate) fn detect_conflicts(
Ok(())
}
/// Determine the [`RequirementsSpecification`] for a script.
#[allow(clippy::result_large_err)]
pub(crate) fn script_specification(
script: Pep723ItemRef<'_>,
settings: ResolverSettingsRef,
) -> Result<Option<RequirementsSpecification>, ProjectError> {
let Some(dependencies) = script.metadata().dependencies.as_ref() else {
return Ok(None);
};
// Determine the working directory for the script.
let script_dir = match &script {
Pep723ItemRef::Script(script) => std::path::absolute(&script.path)?
.parent()
.expect("script path has no parent")
.to_owned(),
Pep723ItemRef::Stdin(..) | Pep723ItemRef::Remote(..) => std::env::current_dir()?,
};
// Collect any `tool.uv.index` from the script.
let empty = Vec::default();
let script_indexes = match settings.sources {
SourceStrategy::Enabled => script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
// Collect any `tool.uv.sources` from the script.
let empty = BTreeMap::default();
let script_sources = match settings.sources {
SourceStrategy::Enabled => script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
let requirements = dependencies
.iter()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<_, _>>()?;
let constraints = script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.constraint_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<Vec<_>, _>>()?;
let overrides = script
.metadata()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.override_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(RequirementsSpecification::from_overrides(
requirements,
constraints,
overrides,
)))
}
/// Warn if the user provides (e.g.) an `--index-url` in a requirements file.
fn warn_on_requirements_txt_setting(
spec: &RequirementsSpecification,

View File

@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::Read;
@ -18,9 +17,8 @@ use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{
Concurrency, DevGroupsSpecification, DryRun, EditableMode, ExtrasSpecification, InstallOptions,
PreviewMode, SourceStrategy, TrustedHost,
PreviewMode, TrustedHost,
};
use uv_distribution::LoweredRequirement;
use uv_fs::which::is_executable;
use uv_fs::{PythonExt, Simplified};
use uv_installer::{SatisfiesResult, SitePackages};
@ -46,9 +44,10 @@ use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, update_environment, validate_project_requires_python,
DependencyGroupsTarget, EnvironmentSpecification, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
default_dependency_groups, script_specification, update_environment,
validate_project_requires_python, DependencyGroupsTarget, EnvironmentSpecification,
ProjectEnvironment, ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState,
WorkspacePython,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::run::run_to_completion;
@ -211,10 +210,11 @@ pub(crate) async fn run(
&install_mirrors,
no_config,
cache,
DryRun::Disabled,
printer,
)
.await?
.into_environment();
.into_environment()?;
// Determine the lock mode.
let mode = if frozen {
@ -317,98 +317,8 @@ pub(crate) async fn run(
);
}
// Determine the working directory for the script.
let script_dir = match &script {
Pep723Item::Script(script) => std::path::absolute(&script.path)?
.parent()
.expect("script path has no parent")
.to_owned(),
Pep723Item::Stdin(..) | Pep723Item::Remote(..) => std::env::current_dir()?,
};
let metadata = script.metadata();
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
if let Some(dependencies) = metadata.dependencies.as_ref() {
// Collect any `tool.uv.index` from the script.
let empty = Vec::default();
let script_indexes = match settings.sources {
SourceStrategy::Enabled => metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.top_level.index.as_deref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
// Collect any `tool.uv.sources` from the script.
let empty = BTreeMap::default();
let script_sources = match settings.sources {
SourceStrategy::Enabled => metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.sources.as_ref())
.unwrap_or(&empty),
SourceStrategy::Disabled => &empty,
};
let requirements = dependencies
.iter()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
&settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<_, _>>()?;
let constraints = metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.constraint_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
&settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<Vec<_>, _>>()?;
let overrides = metadata
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.override_dependencies.as_ref())
.into_iter()
.flatten()
.cloned()
.flat_map(|requirement| {
LoweredRequirement::from_non_workspace_requirement(
requirement,
script_dir.as_ref(),
script_sources,
script_indexes,
&settings.index_locations,
)
.map_ok(LoweredRequirement::into_inner)
})
.collect::<Result<Vec<_>, _>>()?;
let spec =
RequirementsSpecification::from_overrides(requirements, constraints, overrides);
if let Some(spec) = script_specification((&script).into(), settings.as_ref().into())? {
let environment = ScriptEnvironment::get_or_init(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
@ -420,10 +330,11 @@ pub(crate) async fn run(
&install_mirrors,
no_config,
cache,
DryRun::Disabled,
printer,
)
.await?
.into_environment();
.into_environment()?;
match update_environment(
environment,
@ -447,6 +358,7 @@ pub(crate) async fn run(
native_tls,
allow_insecure_host,
cache,
DryRun::Disabled,
printer,
preview,
)

View File

@ -1,4 +1,5 @@
use std::fmt::Write;
use std::ops::Deref;
use std::path::Path;
use std::sync::Arc;
@ -23,6 +24,7 @@ use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
use uv_python::{PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest};
use uv_resolver::{FlatIndex, Installable};
use uv_scripts::{Pep723ItemRef, Pep723Script};
use uv_settings::PythonInstallMirrors;
use uv_types::{BuildIsolation, HashStrategy};
use uv_warnings::warn_user;
@ -36,8 +38,9 @@ use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::{do_safe_lock, LockMode, LockResult};
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
default_dependency_groups, detect_conflicts, DependencyGroupsTarget, PlatformState,
ProjectEnvironment, ProjectError, UniversalState,
default_dependency_groups, detect_conflicts, script_specification, update_environment,
DependencyGroupsTarget, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment,
UniversalState,
};
use crate::commands::{diagnostics, ExitStatus};
use crate::printer::Printer;
@ -63,6 +66,7 @@ pub(crate) async fn sync(
python_preference: PythonPreference,
python_downloads: PythonDownloads,
settings: ResolverInstallerSettings,
script: Option<Pep723Script>,
installer_metadata: bool,
connectivity: Connectivity,
concurrency: Concurrency,
@ -73,75 +77,152 @@ pub(crate) async fn sync(
printer: Printer,
preview: PreviewMode,
) -> Result<ExitStatus> {
// Identify the project.
let project = if frozen {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
members: MemberDiscovery::None,
..DiscoveryOptions::default()
},
)
.await?
} else if let Some(package) = package.as_ref() {
VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
// Identify the target.
let target = if let Some(script) = script {
SyncTarget::Script(script)
} else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
// Identify the project.
let project = if frozen {
VirtualProject::discover(
project_dir,
&DiscoveryOptions {
members: MemberDiscovery::None,
..DiscoveryOptions::default()
},
)
.await?
} else if let Some(package) = package.as_ref() {
VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default())
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
)
} else {
VirtualProject::discover(project_dir, &DiscoveryOptions::default()).await?
};
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
{
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
}
SyncTarget::Project(project)
};
// Validate that any referenced dependency groups are defined in the workspace.
if !frozen {
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
match &target {
SyncTarget::Project(project) => {
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
DependencyGroupsTarget::Workspace(project.workspace())
} else {
DependencyGroupsTarget::Project(project)
}
}
VirtualProject::NonProject(workspace) => {
DependencyGroupsTarget::Workspace(workspace)
}
};
target.validate(&dev)?;
}
VirtualProject::NonProject(workspace) => DependencyGroupsTarget::Workspace(workspace),
};
target.validate(&dev)?;
SyncTarget::Script(..) => {}
}
}
// Determine the default groups to include.
let defaults = default_dependency_groups(project.pyproject_toml())?;
// TODO(lucab): improve warning content
// <https://github.com/astral-sh/uv/issues/7428>
if project.workspace().pyproject_toml().has_scripts()
&& !project.workspace().pyproject_toml().is_package()
{
warn_user!("Skipping installation of entry points (`project.scripts`) because this project is not packaged; to install entry points, set `tool.uv.package = true` or define a `build-system`");
}
let defaults = match &target {
SyncTarget::Project(project) => default_dependency_groups(project.pyproject_toml())?,
SyncTarget::Script(..) => Vec::new(),
};
// Discover or create the virtual environment.
let environment = ProjectEnvironment::get_or_init(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
no_config,
active,
cache,
dry_run,
printer,
)
.await?;
let environment = match &target {
SyncTarget::Project(project) => SyncEnvironment::Project(
ProjectEnvironment::get_or_init(
project.workspace(),
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
no_config,
active,
cache,
dry_run,
printer,
)
.await?,
),
SyncTarget::Script(script) => SyncEnvironment::Script(
ScriptEnvironment::get_or_init(
Pep723ItemRef::Script(script),
python.as_deref().map(PythonRequest::parse),
python_preference,
python_downloads,
connectivity,
native_tls,
allow_insecure_host,
&install_mirrors,
no_config,
cache,
dry_run,
printer,
)
.await?,
),
};
// In `--dry-run` mode, print the environment discovery or creation.
if dry_run.enabled() {
match &environment {
ProjectEnvironment::Existing(environment) => {
// Notify the user of any environment changes.
match &environment {
SyncEnvironment::Project(ProjectEnvironment::Existing(environment))
if dry_run.enabled() =>
{
writeln!(
printer.stderr(),
"{}",
format!(
"Discovered existing environment at: {}",
environment.root().user_display().bold()
)
.dimmed()
)?;
}
SyncEnvironment::Project(ProjectEnvironment::WouldReplace(root, ..))
if dry_run.enabled() =>
{
writeln!(
printer.stderr(),
"{}",
format!(
"Would replace existing virtual environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
}
SyncEnvironment::Project(ProjectEnvironment::WouldCreate(root, ..))
if dry_run.enabled() =>
{
writeln!(
printer.stderr(),
"{}",
format!(
"Would create virtual environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
}
SyncEnvironment::Script(ScriptEnvironment::Existing(environment)) => {
if dry_run.enabled() {
writeln!(
printer.stderr(),
"{}",
@ -151,50 +232,102 @@ pub(crate) async fn sync(
)
.dimmed()
)?;
}
ProjectEnvironment::Replaced(environment) => {
} else {
writeln!(
printer.stderr(),
"{}",
format!(
"Replaced existing environment at: {}",
environment.root().user_display().bold()
)
.dimmed()
"Using script environment at: {}",
environment.root().user_display().cyan()
)?;
}
ProjectEnvironment::Created(environment) => {
writeln!(
printer.stderr(),
"{}",
format!(
"Created new environment at: {}",
environment.root().user_display().bold()
)
.dimmed()
)?;
}
SyncEnvironment::Script(ScriptEnvironment::Replaced(environment)) if !dry_run.enabled() => {
writeln!(
printer.stderr(),
"Recreating script environment at: {}",
environment.root().user_display().cyan()
)?;
}
SyncEnvironment::Script(ScriptEnvironment::Created(environment)) if !dry_run.enabled() => {
writeln!(
printer.stderr(),
"Creating script environment at: {}",
environment.root().user_display().cyan()
)?;
}
SyncEnvironment::Script(ScriptEnvironment::WouldReplace(root, ..)) if dry_run.enabled() => {
writeln!(
printer.stderr(),
"{}",
format!(
"Would replace existing script environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
}
SyncEnvironment::Script(ScriptEnvironment::WouldCreate(root, ..)) if dry_run.enabled() => {
writeln!(
printer.stderr(),
"{}",
format!(
"Would create script environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
}
_ => {}
}
// Special-case: we're syncing a script that doesn't have an associated lockfile. In that case,
// we don't create a lockfile, so the resolve-and-install semantics are different.
if let SyncTarget::Script(script) = &target {
let lockfile = LockTarget::from(script).lock_path();
if !lockfile.is_file() {
if frozen {
return Err(anyhow::anyhow!(
"`uv sync --frozen` requires a script lockfile; run `{}` to lock the script",
format!("uv lock --script {}", script.path.user_display()).green(),
));
}
ProjectEnvironment::WouldReplace(root, ..) => {
writeln!(
printer.stderr(),
"{}",
format!(
"Would replace existing virtual environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
if locked {
return Err(anyhow::anyhow!(
"`uv sync --locked` requires a script lockfile; run `{}` to lock the script",
format!("uv lock --script {}", script.path.user_display()).green(),
));
}
ProjectEnvironment::WouldCreate(root, ..) => {
writeln!(
printer.stderr(),
"{}",
format!(
"Would create virtual environment at: {}",
root.user_display().bold()
)
.dimmed()
)?;
let spec =
script_specification(Pep723ItemRef::Script(script), settings.as_ref().into())?
.unwrap_or_default();
match update_environment(
Deref::deref(&environment).clone(),
spec,
modifications,
&settings,
&PlatformState::default(),
Box::new(DefaultResolveLogger),
Box::new(DefaultInstallLogger),
installer_metadata,
connectivity,
concurrency,
native_tls,
allow_insecure_host,
cache,
dry_run,
printer,
preview,
)
.await
{
Ok(..) => return Ok(ExitStatus::Success),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(native_tls)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()))
}
Err(err) => return Err(err.into()),
}
}
}
@ -213,11 +346,14 @@ pub(crate) async fn sync(
LockMode::Write(environment.interpreter())
};
let target = LockTarget::from(project.workspace());
let lock_target = match &target {
SyncTarget::Project(project) => LockTarget::from(project.workspace()),
SyncTarget::Script(script) => LockTarget::from(script),
};
let lock = match do_safe_lock(
mode,
target,
lock_target,
settings.as_ref().into(),
&state,
Box::new(DefaultResolveLogger),
@ -240,7 +376,7 @@ pub(crate) async fn sync(
"{}",
format!(
"Found up-to-date lockfile at: {}",
target.lock_path().user_display().bold()
lock_target.lock_path().user_display().bold()
)
.dimmed()
)?;
@ -251,7 +387,7 @@ pub(crate) async fn sync(
"{}",
format!(
"Would create lockfile at: {}",
target.lock_path().user_display().bold()
lock_target.lock_path().user_display().bold()
)
.dimmed()
)?;
@ -262,7 +398,7 @@ pub(crate) async fn sync(
"{}",
format!(
"Would update lockfile at: {}",
target.lock_path().user_display().bold()
lock_target.lock_path().user_display().bold()
)
.dimmed()
)?;
@ -280,55 +416,63 @@ pub(crate) async fn sync(
};
// Identify the installation target.
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
InstallTarget::Workspace {
workspace: project.workspace(),
lock: &lock,
let sync_target = match &target {
SyncTarget::Project(project) => {
match &project {
VirtualProject::Project(project) => {
if all_packages {
InstallTarget::Workspace {
workspace: project.workspace(),
lock: &lock,
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace: project.workspace(),
name: package,
lock: &lock,
}
} else {
// By default, install the root package.
InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
}
}
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace: project.workspace(),
name: package,
lock: &lock,
}
} else {
// By default, install the root package.
InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: &lock,
}
}
}
VirtualProject::NonProject(workspace) => {
if all_packages {
InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace,
name: package,
lock: &lock,
}
} else {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
VirtualProject::NonProject(workspace) => {
if all_packages {
InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace,
name: package,
lock: &lock,
}
} else {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace {
workspace,
lock: &lock,
}
}
}
}
}
SyncTarget::Script(script) => InstallTarget::Script {
script,
lock: &lock,
},
};
let state = state.fork();
// Perform the sync operation.
match do_sync(
target,
sync_target,
&environment,
&extras,
&dev.with_defaults(defaults),
@ -362,6 +506,33 @@ pub(crate) async fn sync(
Ok(ExitStatus::Success)
}
#[derive(Debug, Clone)]
enum SyncTarget {
/// Sync a project environment.
Project(VirtualProject),
/// Sync a PEP 723 script environment.
Script(Pep723Script),
}
#[derive(Debug)]
enum SyncEnvironment {
/// A Python environment for a project.
Project(ProjectEnvironment),
/// A Python environment for a script.
Script(ScriptEnvironment),
}
impl Deref for SyncEnvironment {
type Target = PythonEnvironment;
fn deref(&self) -> &Self::Target {
match self {
Self::Project(environment) => Deref::deref(environment),
Self::Script(environment) => Deref::deref(environment),
}
}
}
/// Sync a lockfile with an environment.
#[allow(clippy::fn_params_excessive_bools)]
pub(super) async fn do_sync(

View File

@ -8,7 +8,7 @@ use tracing::{debug, trace};
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, PreviewMode, Reinstall, TrustedHost, Upgrade};
use uv_configuration::{Concurrency, DryRun, PreviewMode, Reinstall, TrustedHost, Upgrade};
use uv_distribution_types::{NameRequirementSpecification, UnresolvedRequirementSpecification};
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
@ -410,6 +410,7 @@ pub(crate) async fn install(
native_tls,
allow_insecure_host,
&cache,
DryRun::Disabled,
printer,
preview,
)

View File

@ -7,7 +7,7 @@ use tracing::debug;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity};
use uv_configuration::{Concurrency, PreviewMode, TrustedHost};
use uv_configuration::{Concurrency, DryRun, PreviewMode, TrustedHost};
use uv_fs::CWD;
use uv_normalize::PackageName;
use uv_pypi_types::Requirement;
@ -352,6 +352,7 @@ async fn upgrade_tool(
native_tls,
allow_insecure_host,
cache,
DryRun::Disabled,
printer,
preview,
)

View File

@ -201,6 +201,10 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
script: Some(script),
..
})
| ProjectCommand::Sync(uv_cli::SyncArgs {
script: Some(script),
..
})
| ProjectCommand::Tree(uv_cli::TreeArgs {
script: Some(script),
..
@ -1527,7 +1531,14 @@ async fn run_project(
.combine(Refresh::from(args.settings.upgrade.clone())),
);
commands::sync(
// Unwrap the script.
let script = script.map(|script| match script {
Pep723Item::Script(script) => script,
Pep723Item::Stdin(..) => unreachable!("`uv lock` does not support stdin"),
Pep723Item::Remote(..) => unreachable!("`uv lock` does not support remote files"),
});
Box::pin(commands::sync(
project_dir,
args.locked,
args.frozen,
@ -1545,6 +1556,7 @@ async fn run_project(
globals.python_preference,
globals.python_downloads,
args.settings,
script,
globals.installer_metadata,
globals.connectivity,
globals.concurrency,
@ -1554,7 +1566,7 @@ async fn run_project(
&cache,
printer,
globals.preview,
)
))
.await
}
ProjectCommand::Lock(args) => {

View File

@ -959,6 +959,7 @@ pub(crate) struct SyncSettings {
pub(crate) locked: bool,
pub(crate) frozen: bool,
pub(crate) dry_run: DryRun,
pub(crate) script: Option<PathBuf>,
pub(crate) active: Option<bool>,
pub(crate) extras: ExtrasSpecification,
pub(crate) dev: DevGroupsSpecification,
@ -1006,6 +1007,7 @@ impl SyncSettings {
refresh,
all_packages,
package,
script,
python,
} = args;
let install_mirrors = filesystem
@ -1022,6 +1024,7 @@ impl SyncSettings {
locked,
frozen,
dry_run: DryRun::from_args(dry_run),
script,
active: flag(active, no_active),
extras: ExtrasSpecification::from_args(
flag(all_extras, no_all_extras).unwrap_or_default(),

View File

@ -6684,3 +6684,379 @@ fn sync_dry_run() -> Result<()> {
Ok(())
}
#[test]
fn sync_script() -> Result<()> {
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
let script = context.temp_dir.child("script.py");
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# ]
# ///
import anyio
"#
})?;
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v1/script-\w+",
"environments-v1/script-[HASH]",
)])
.collect::<Vec<_>>();
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"###);
// If a lockfile didn't exist already, `uv sync --script` shouldn't create one.
assert!(!context.temp_dir.child("uv.lock").exists());
// Modify the script's dependencies.
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# "iniconfig",
# ]
# ///
import anyio
"#
})?;
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 4 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
// Modify the `requires-python`.
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.8, <3.11"
# dependencies = [
# "anyio",
# "iniconfig",
# ]
# ///
import anyio
"#
})?;
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Recreating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
// `--locked` and `--frozen` should fail with helpful error messages.
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
error: `uv sync --locked` requires a script lockfile; run `uv lock --script script.py` to lock the script
"###);
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--frozen"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
error: `uv sync --frozen` requires a script lockfile; run `uv lock --script script.py` to lock the script
"###);
Ok(())
}
#[test]
fn sync_locked_script() -> Result<()> {
let context = TestContext::new_with_versions(&["3.8", "3.12"]);
let script = context.temp_dir.child("script.py");
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# ]
# ///
import anyio
"#
})?;
let filters = context
.filters()
.into_iter()
.chain(vec![(
r"environments-v1/script-\w+",
"environments-v1/script-[HASH]",
)])
.collect::<Vec<_>>();
// Lock the script.
uv_snapshot!(&filters, context.lock().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 3 packages in [TIME]
"###);
let lock = context.read("script.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.11"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [{ name = "anyio" }]
[[package]]
name = "anyio"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
"###
);
});
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Creating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
"###);
// Modify the script's dependencies.
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "anyio",
# "iniconfig",
# ]
# ///
import anyio
"#
})?;
// Re-run with `--locked`.
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 4 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
"###);
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 4 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ iniconfig==2.0.0
"###);
let lock = context.read("script.py.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.11"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [
{ name = "anyio" },
{ name = "iniconfig" },
]
[[package]]
name = "anyio"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/4d/3970183622f0330d3c23d9b8a5f52e365e50381fd484d08e3285104333d3/anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6", size = 159642 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8", size = 85584 },
]
[[package]]
name = "idna"
version = "3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
"###
);
});
// Modify the `requires-python`.
script.write_str(indoc! { r#"
# /// script
# requires-python = ">=3.8, <3.11"
# dependencies = [
# "anyio",
# "iniconfig",
# ]
# ///
import anyio
"#
})?;
// Re-run with `--locked`.
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
Recreating script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 6 packages in [TIME]
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
"###);
uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using script environment at: [CACHE_DIR]/environments-v1/script-[HASH]
Resolved 6 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 6 packages in [TIME]
+ anyio==4.3.0
+ exceptiongroup==1.2.0
+ idna==3.6
+ iniconfig==2.0.0
+ sniffio==1.3.1
+ typing-extensions==4.10.0
"###);
Ok(())
}

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