mirror of https://github.com/astral-sh/uv
Add experimental `uv sync --output-format json` (#13689)
This is a continuation of the work in * #12405 I have: * moved to an architecture where the human output is derived from the json structs to centralize more of the printing state/logic * cleaned up some of the names/types * added tests * removed the restriction that this output is --dry-run only I have not yet added package info, which was TBD in their design. --------- Co-authored-by: x0rw <mahdi.svt5@gmail.com> Co-authored-by: Zanie Blue <contact@zanie.dev> Co-authored-by: John Mumm <jtfmumm@gmail.com>
This commit is contained in:
parent
df44199ceb
commit
34fbc06ad6
|
|
@ -46,6 +46,15 @@ pub enum PythonListFormat {
|
||||||
Json,
|
Json,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)]
|
||||||
|
pub enum SyncFormat {
|
||||||
|
/// Display the result in a human-readable format.
|
||||||
|
#[default]
|
||||||
|
Text,
|
||||||
|
/// Display the result in JSON format.
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, clap::ValueEnum)]
|
#[derive(Debug, Default, Clone, clap::ValueEnum)]
|
||||||
pub enum ListFormat {
|
pub enum ListFormat {
|
||||||
/// Display the list of packages in a human-readable table.
|
/// Display the list of packages in a human-readable table.
|
||||||
|
|
@ -3207,6 +3216,10 @@ pub struct SyncArgs {
|
||||||
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
|
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
|
||||||
pub extra: Option<Vec<ExtraName>>,
|
pub extra: Option<Vec<ExtraName>>,
|
||||||
|
|
||||||
|
/// Select the output format.
|
||||||
|
#[arg(long, value_enum, default_value_t = SyncFormat::default())]
|
||||||
|
pub output_format: SyncFormat,
|
||||||
|
|
||||||
/// Include all optional dependencies.
|
/// Include all optional dependencies.
|
||||||
///
|
///
|
||||||
/// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag
|
/// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,12 @@ impl From<Box<Path>> for PortablePathBuf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Path> for PortablePathBuf {
|
||||||
|
fn from(path: &'a Path) -> Self {
|
||||||
|
Box::<Path>::from(path).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl serde::Serialize for PortablePathBuf {
|
impl serde::Serialize for PortablePathBuf {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
|
|
||||||
|
|
@ -1408,6 +1408,14 @@ impl ProjectEnvironment {
|
||||||
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
|
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the path to the actual target, if this was a dry run environment.
|
||||||
|
pub(crate) fn dry_run_target(&self) -> Option<&Path> {
|
||||||
|
match self {
|
||||||
|
Self::WouldReplace(path, _, _) | Self::WouldCreate(path, _, _) => Some(path),
|
||||||
|
Self::Created(_) | Self::Existing(_) | Self::Replaced(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ProjectEnvironment {
|
impl std::ops::Deref for ProjectEnvironment {
|
||||||
|
|
@ -1588,6 +1596,14 @@ impl ScriptEnvironment {
|
||||||
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
|
Self::WouldCreate(..) => Err(ProjectError::DroppedEnvironment),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the path to the actual target, if this was a dry run environment.
|
||||||
|
pub(crate) fn dry_run_target(&self) -> Option<&Path> {
|
||||||
|
match self {
|
||||||
|
Self::WouldReplace(path, _, _) | Self::WouldCreate(path, _, _) => Some(path),
|
||||||
|
Self::Created(_) | Self::Existing(_) | Self::Replaced(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ScriptEnvironment {
|
impl std::ops::Deref for ScriptEnvironment {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ use std::sync::Arc;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
use serde::Serialize;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
use uv_cli::SyncFormat;
|
||||||
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_configuration::{
|
use uv_configuration::{
|
||||||
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode,
|
Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode,
|
||||||
|
|
@ -19,7 +20,7 @@ use uv_dispatch::BuildDispatch;
|
||||||
use uv_distribution_types::{
|
use uv_distribution_types::{
|
||||||
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
|
DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
|
||||||
};
|
};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::{PortablePathBuf, Simplified};
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
|
||||||
use uv_pep508::{MarkerTree, VersionOrUrl};
|
use uv_pep508::{MarkerTree, VersionOrUrl};
|
||||||
|
|
@ -77,7 +78,14 @@ pub(crate) async fn sync(
|
||||||
cache: &Cache,
|
cache: &Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
output_format: SyncFormat,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_enabled() && matches!(output_format, SyncFormat::Json) {
|
||||||
|
warn_user!(
|
||||||
|
"The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview` to disable this warning."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Identify the target.
|
// Identify the target.
|
||||||
let workspace_cache = WorkspaceCache::default();
|
let workspace_cache = WorkspaceCache::default();
|
||||||
let target = if let Some(script) = script {
|
let target = if let Some(script) = script {
|
||||||
|
|
@ -180,103 +188,16 @@ pub(crate) async fn sync(
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
// Notify the user of any environment changes.
|
let sync_report = SyncReport {
|
||||||
match &environment {
|
dry_run: dry_run.enabled(),
|
||||||
SyncEnvironment::Project(ProjectEnvironment::Existing(environment))
|
environment: EnvironmentReport::from(&environment),
|
||||||
if dry_run.enabled() =>
|
action: SyncAction::from(&environment),
|
||||||
{
|
target: TargetName::from(&target),
|
||||||
writeln!(
|
};
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
// Show the intermediate results if relevant
|
||||||
format!(
|
if let Some(message) = sync_report.format(output_format) {
|
||||||
"Discovered existing environment at: {}",
|
writeln!(printer.stderr(), "{message}")?;
|
||||||
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(),
|
|
||||||
"{}",
|
|
||||||
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,
|
// Special-case: we're syncing a script that doesn't have an associated lockfile. In that case,
|
||||||
|
|
@ -340,7 +261,23 @@ pub(crate) async fn sync(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(..) => return Ok(ExitStatus::Success),
|
Ok(..) => {
|
||||||
|
// Generate a report for the script without a lockfile
|
||||||
|
let report = Report {
|
||||||
|
schema: SchemaReport::default(),
|
||||||
|
target: TargetName::from(&target),
|
||||||
|
project: None,
|
||||||
|
script: Some(ScriptReport::from(script)),
|
||||||
|
sync: sync_report,
|
||||||
|
lock: None,
|
||||||
|
dry_run: dry_run.enabled(),
|
||||||
|
};
|
||||||
|
if let Some(output) = report.format(output_format) {
|
||||||
|
writeln!(printer.stdout(), "{output}")?;
|
||||||
|
}
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
}
|
||||||
|
// TODO(zanieb): We should respect `--output-format json` for the error case
|
||||||
Err(ProjectError::Operation(err)) => {
|
Err(ProjectError::Operation(err)) => {
|
||||||
return diagnostics::OperationDiagnostic::native_tls(
|
return diagnostics::OperationDiagnostic::native_tls(
|
||||||
network_settings.native_tls,
|
network_settings.native_tls,
|
||||||
|
|
@ -387,46 +324,7 @@ pub(crate) async fn sync(
|
||||||
.execute(lock_target)
|
.execute(lock_target)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => Outcome::Success(result),
|
||||||
if dry_run.enabled() {
|
|
||||||
match result {
|
|
||||||
LockResult::Unchanged(..) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Found up-to-date lockfile at: {}",
|
|
||||||
lock_target.lock_path().user_display().bold()
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
LockResult::Changed(None, ..) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Would create lockfile at: {}",
|
|
||||||
lock_target.lock_path().user_display().bold()
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
LockResult::Changed(Some(..), ..) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Would update lockfile at: {}",
|
|
||||||
lock_target.lock_path().user_display().bold()
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Outcome::Success(result.into_lock())
|
|
||||||
}
|
|
||||||
Err(ProjectError::Operation(err)) => {
|
Err(ProjectError::Operation(err)) => {
|
||||||
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
return diagnostics::OperationDiagnostic::native_tls(network_settings.native_tls)
|
||||||
.report(err)
|
.report(err)
|
||||||
|
|
@ -440,6 +338,25 @@ pub(crate) async fn sync(
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let lock_report = LockReport::from((&lock_target, &mode, &outcome));
|
||||||
|
if let Some(message) = lock_report.format(output_format) {
|
||||||
|
writeln!(printer.stderr(), "{message}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = Report {
|
||||||
|
schema: SchemaReport::default(),
|
||||||
|
target: TargetName::from(&target),
|
||||||
|
project: target.project().map(ProjectReport::from),
|
||||||
|
script: target.script().map(ScriptReport::from),
|
||||||
|
sync: sync_report,
|
||||||
|
lock: Some(lock_report),
|
||||||
|
dry_run: dry_run.enabled(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(output) = report.format(output_format) {
|
||||||
|
writeln!(printer.stdout(), "{output}")?;
|
||||||
|
}
|
||||||
|
|
||||||
// Identify the installation target.
|
// Identify the installation target.
|
||||||
let sync_target =
|
let sync_target =
|
||||||
identify_installation_target(&target, outcome.lock(), all_packages, package.as_ref());
|
identify_installation_target(&target, outcome.lock(), all_packages, package.as_ref());
|
||||||
|
|
@ -490,7 +407,7 @@ pub(crate) async fn sync(
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
enum Outcome {
|
enum Outcome {
|
||||||
/// The `lock` operation was successful.
|
/// The `lock` operation was successful.
|
||||||
Success(Lock),
|
Success(LockResult),
|
||||||
/// The `lock` operation successfully resolved, but failed due to a mismatch (e.g., with `--locked`).
|
/// The `lock` operation successfully resolved, but failed due to a mismatch (e.g., with `--locked`).
|
||||||
LockMismatch(Box<Lock>),
|
LockMismatch(Box<Lock>),
|
||||||
}
|
}
|
||||||
|
|
@ -499,7 +416,7 @@ impl Outcome {
|
||||||
/// Return the [`Lock`] associated with this outcome.
|
/// Return the [`Lock`] associated with this outcome.
|
||||||
fn lock(&self) -> &Lock {
|
fn lock(&self) -> &Lock {
|
||||||
match self {
|
match self {
|
||||||
Self::Success(lock) => lock,
|
Self::Success(lock) => lock.lock(),
|
||||||
Self::LockMismatch(lock) => lock,
|
Self::LockMismatch(lock) => lock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +480,22 @@ enum SyncTarget {
|
||||||
Script(Pep723Script),
|
Script(Pep723Script),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SyncTarget {
|
||||||
|
fn project(&self) -> Option<&VirtualProject> {
|
||||||
|
match self {
|
||||||
|
Self::Project(project) => Some(project),
|
||||||
|
Self::Script(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(&self) -> Option<&Pep723Script> {
|
||||||
|
match self {
|
||||||
|
Self::Project(_) => None,
|
||||||
|
Self::Script(script) => Some(script),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum SyncEnvironment {
|
enum SyncEnvironment {
|
||||||
/// A Python environment for a project.
|
/// A Python environment for a project.
|
||||||
|
|
@ -571,6 +504,15 @@ enum SyncEnvironment {
|
||||||
Script(ScriptEnvironment),
|
Script(ScriptEnvironment),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SyncEnvironment {
|
||||||
|
fn dry_run_target(&self) -> Option<&Path> {
|
||||||
|
match self {
|
||||||
|
Self::Project(env) => env.dry_run_target(),
|
||||||
|
Self::Script(env) => env.dry_run_target(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for SyncEnvironment {
|
impl Deref for SyncEnvironment {
|
||||||
type Target = PythonEnvironment;
|
type Target = PythonEnvironment;
|
||||||
|
|
||||||
|
|
@ -892,3 +834,392 @@ fn store_credentials_from_target(target: InstallTarget<'_>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct WorkspaceReport {
|
||||||
|
/// The workspace directory path.
|
||||||
|
path: PortablePathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Workspace> for WorkspaceReport {
|
||||||
|
fn from(workspace: &Workspace) -> Self {
|
||||||
|
Self {
|
||||||
|
path: workspace.install_path().as_path().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct ProjectReport {
|
||||||
|
//
|
||||||
|
path: PortablePathBuf,
|
||||||
|
workspace: WorkspaceReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&VirtualProject> for ProjectReport {
|
||||||
|
fn from(project: &VirtualProject) -> Self {
|
||||||
|
Self {
|
||||||
|
path: project.root().into(),
|
||||||
|
workspace: WorkspaceReport::from(project.workspace()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SyncTarget> for TargetName {
|
||||||
|
fn from(target: &SyncTarget) -> Self {
|
||||||
|
match target {
|
||||||
|
SyncTarget::Project(_) => TargetName::Project,
|
||||||
|
SyncTarget::Script(_) => TargetName::Script,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct ScriptReport {
|
||||||
|
/// The path to the script.
|
||||||
|
path: PortablePathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Pep723Script> for ScriptReport {
|
||||||
|
fn from(script: &Pep723Script) -> Self {
|
||||||
|
Self {
|
||||||
|
path: script.path.as_path().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum SchemaVersion {
|
||||||
|
/// An unstable, experimental schema.
|
||||||
|
#[default]
|
||||||
|
Preview,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Default)]
|
||||||
|
struct SchemaReport {
|
||||||
|
/// The version of the schema.
|
||||||
|
version: SchemaVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A report of the uv sync operation
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
struct Report {
|
||||||
|
/// The schema of this report.
|
||||||
|
schema: SchemaReport,
|
||||||
|
/// The target of the sync operation, either a project or a script.
|
||||||
|
target: TargetName,
|
||||||
|
/// The report for a [`TargetName::Project`], if applicable.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
project: Option<ProjectReport>,
|
||||||
|
/// The report for a [`TargetName::Script`], if applicable.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
script: Option<ScriptReport>,
|
||||||
|
/// The report for the sync operation.
|
||||||
|
sync: SyncReport,
|
||||||
|
/// The report for the lock operation.
|
||||||
|
lock: Option<LockReport>,
|
||||||
|
/// Whether this is a dry run.
|
||||||
|
dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The kind of target
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum TargetName {
|
||||||
|
Project,
|
||||||
|
Script,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TargetName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TargetName::Project => write!(f, "project"),
|
||||||
|
TargetName::Script => write!(f, "script"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the action taken during a sync.
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum SyncAction {
|
||||||
|
/// The environment was checked and required no updates.
|
||||||
|
Check,
|
||||||
|
/// The environment was updated.
|
||||||
|
Update,
|
||||||
|
/// The environment was replaced.
|
||||||
|
Replace,
|
||||||
|
/// A new environment was created.
|
||||||
|
Create,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SyncEnvironment> for SyncAction {
|
||||||
|
fn from(env: &SyncEnvironment) -> Self {
|
||||||
|
match &env {
|
||||||
|
SyncEnvironment::Project(ProjectEnvironment::Existing(..)) => SyncAction::Check,
|
||||||
|
SyncEnvironment::Project(ProjectEnvironment::Created(..)) => SyncAction::Create,
|
||||||
|
SyncEnvironment::Project(ProjectEnvironment::WouldCreate(..)) => SyncAction::Create,
|
||||||
|
SyncEnvironment::Project(ProjectEnvironment::WouldReplace(..)) => SyncAction::Replace,
|
||||||
|
SyncEnvironment::Project(ProjectEnvironment::Replaced(..)) => SyncAction::Update,
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::Existing(..)) => SyncAction::Check,
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::Created(..)) => SyncAction::Create,
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::WouldCreate(..)) => SyncAction::Create,
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::WouldReplace(..)) => SyncAction::Replace,
|
||||||
|
SyncEnvironment::Script(ScriptEnvironment::Replaced(..)) => SyncAction::Update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncAction {
|
||||||
|
fn message(&self, target: TargetName, dry_run: bool) -> Option<&'static str> {
|
||||||
|
let message = if dry_run {
|
||||||
|
match self {
|
||||||
|
SyncAction::Check => "Would use",
|
||||||
|
SyncAction::Update => "Would update",
|
||||||
|
SyncAction::Replace => "Would replace",
|
||||||
|
SyncAction::Create => "Would create",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For projects, we omit some of these messages when we're not in dry-run mode
|
||||||
|
let is_project = matches!(target, TargetName::Project);
|
||||||
|
match self {
|
||||||
|
SyncAction::Check | SyncAction::Update | SyncAction::Create if is_project => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
SyncAction::Check => "Using",
|
||||||
|
SyncAction::Update => "Updating",
|
||||||
|
SyncAction::Replace => "Replacing",
|
||||||
|
SyncAction::Create => "Creating",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the action taken during a lock.
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
enum LockAction {
|
||||||
|
/// The lockfile was used without checking.
|
||||||
|
Use,
|
||||||
|
/// The lockfile was checked and required no updates.
|
||||||
|
Check,
|
||||||
|
/// The lockfile was updated.
|
||||||
|
Update,
|
||||||
|
/// A new lockfile was created.
|
||||||
|
Create,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockAction {
|
||||||
|
fn message(&self, dry_run: bool) -> Option<&'static str> {
|
||||||
|
let message = if dry_run {
|
||||||
|
match self {
|
||||||
|
LockAction::Use => return None,
|
||||||
|
LockAction::Check => "Found up-to-date",
|
||||||
|
LockAction::Update => "Would update",
|
||||||
|
LockAction::Create => "Would create",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct PythonReport {
|
||||||
|
path: PortablePathBuf,
|
||||||
|
version: uv_pep508::StringVersion,
|
||||||
|
implementation: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&uv_python::Interpreter> for PythonReport {
|
||||||
|
fn from(interpreter: &uv_python::Interpreter) -> Self {
|
||||||
|
Self {
|
||||||
|
path: interpreter.sys_executable().into(),
|
||||||
|
version: interpreter.python_full_version().clone(),
|
||||||
|
implementation: interpreter.implementation_name().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PythonReport {
|
||||||
|
/// Set the path for this Python report.
|
||||||
|
#[must_use]
|
||||||
|
fn with_path(mut self, path: PortablePathBuf) -> Self {
|
||||||
|
self.path = path;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct EnvironmentReport {
|
||||||
|
/// The path to the environment.
|
||||||
|
path: PortablePathBuf,
|
||||||
|
/// The Python interpreter for the environment.
|
||||||
|
python: PythonReport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PythonEnvironment> for EnvironmentReport {
|
||||||
|
fn from(env: &PythonEnvironment) -> Self {
|
||||||
|
Self {
|
||||||
|
python: PythonReport::from(env.interpreter()),
|
||||||
|
path: env.root().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&SyncEnvironment> for EnvironmentReport {
|
||||||
|
fn from(env: &SyncEnvironment) -> Self {
|
||||||
|
let report = EnvironmentReport::from(&**env);
|
||||||
|
// Replace the path if necessary; we construct a temporary virtual environment during dry
|
||||||
|
// run invocations and want to report the path we _would_ use.
|
||||||
|
if let Some(path) = env.dry_run_target() {
|
||||||
|
report.with_path(path.into())
|
||||||
|
} else {
|
||||||
|
report
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvironmentReport {
|
||||||
|
/// Set the path for this environment report.
|
||||||
|
#[must_use]
|
||||||
|
fn with_path(mut self, path: PortablePathBuf) -> Self {
|
||||||
|
let python_path = &self.python.path;
|
||||||
|
if let Ok(python_path) = python_path.as_ref().strip_prefix(self.path) {
|
||||||
|
let new_path = path.as_ref().to_path_buf().join(python_path);
|
||||||
|
self.python = self.python.with_path(new_path.as_path().into());
|
||||||
|
}
|
||||||
|
self.path = path;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The report for a sync operation.
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct SyncReport {
|
||||||
|
/// The environment.
|
||||||
|
environment: EnvironmentReport,
|
||||||
|
/// The action performed during the sync, e.g., what was done to the environment.
|
||||||
|
action: SyncAction,
|
||||||
|
|
||||||
|
// We store these fields so the report can format itself self-contained, but the outer
|
||||||
|
// [`Report`] is intended to include these in user-facing output
|
||||||
|
#[serde(skip)]
|
||||||
|
dry_run: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
target: TargetName,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncReport {
|
||||||
|
fn format(&self, output_format: SyncFormat) -> Option<String> {
|
||||||
|
match output_format {
|
||||||
|
// This is an intermediate report, when using JSON, it's only rendered at the end
|
||||||
|
SyncFormat::Json => None,
|
||||||
|
SyncFormat::Text => self.to_human_readable_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_human_readable_string(&self) -> Option<String> {
|
||||||
|
let Self {
|
||||||
|
environment,
|
||||||
|
action,
|
||||||
|
dry_run,
|
||||||
|
target,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let action = action.message(*target, *dry_run)?;
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"{action} {target} environment at: {path}",
|
||||||
|
path = environment.path.user_display().cyan(),
|
||||||
|
);
|
||||||
|
if *dry_run {
|
||||||
|
return Some(message.dimmed().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The report for a lock operation.
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct LockReport {
|
||||||
|
/// The path to the lockfile
|
||||||
|
path: PortablePathBuf,
|
||||||
|
/// Whether the lockfile was preserved, created, or updated.
|
||||||
|
action: LockAction,
|
||||||
|
|
||||||
|
// We store this field so the report can format itself self-contained, but the outer
|
||||||
|
// [`Report`] is intended to include this in user-facing output
|
||||||
|
#[serde(skip)]
|
||||||
|
dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(&LockTarget<'_>, &LockMode<'_>, &Outcome)> for LockReport {
|
||||||
|
fn from((target, mode, outcome): (&LockTarget, &LockMode, &Outcome)) -> Self {
|
||||||
|
Self {
|
||||||
|
path: target.lock_path().deref().into(),
|
||||||
|
action: match outcome {
|
||||||
|
Outcome::Success(result) => {
|
||||||
|
match result {
|
||||||
|
LockResult::Unchanged(..) => match mode {
|
||||||
|
// When `--frozen` is used, we don't check the lockfile
|
||||||
|
LockMode::Frozen => LockAction::Use,
|
||||||
|
LockMode::DryRun(_) | LockMode::Locked(_) | LockMode::Write(_) => {
|
||||||
|
LockAction::Check
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LockResult::Changed(None, ..) => LockAction::Create,
|
||||||
|
LockResult::Changed(Some(_), ..) => LockAction::Update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO(zanieb): We don't have a way to report the outcome of the lock yet
|
||||||
|
Outcome::LockMismatch(_) => LockAction::Check,
|
||||||
|
},
|
||||||
|
dry_run: matches!(mode, LockMode::DryRun(_)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockReport {
|
||||||
|
fn format(&self, output_format: SyncFormat) -> Option<String> {
|
||||||
|
match output_format {
|
||||||
|
SyncFormat::Json => None,
|
||||||
|
SyncFormat::Text => self.to_human_readable_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_human_readable_string(&self) -> Option<String> {
|
||||||
|
let Self {
|
||||||
|
path,
|
||||||
|
action,
|
||||||
|
dry_run,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let action = action.message(*dry_run)?;
|
||||||
|
|
||||||
|
let message = format!(
|
||||||
|
"{action} lockfile at: {path}",
|
||||||
|
path = path.user_display().cyan(),
|
||||||
|
);
|
||||||
|
if *dry_run {
|
||||||
|
return Some(message.dimmed().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Report {
|
||||||
|
fn format(&self, output_format: SyncFormat) -> Option<String> {
|
||||||
|
match output_format {
|
||||||
|
SyncFormat::Json => serde_json::to_string_pretty(self).ok(),
|
||||||
|
SyncFormat::Text => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1818,6 +1818,7 @@ async fn run_project(
|
||||||
&cache,
|
&cache,
|
||||||
printer,
|
printer,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
args.output_format,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ use uv_cli::{
|
||||||
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs,
|
||||||
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs,
|
||||||
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
|
PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs,
|
||||||
SyncArgs, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs,
|
SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs,
|
||||||
VenvArgs, VersionArgs, VersionBump, VersionFormat,
|
ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat,
|
||||||
};
|
};
|
||||||
use uv_cli::{
|
use uv_cli::{
|
||||||
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs,
|
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs,
|
||||||
|
|
@ -1154,6 +1154,7 @@ pub(crate) struct SyncSettings {
|
||||||
pub(crate) install_mirrors: PythonInstallMirrors,
|
pub(crate) install_mirrors: PythonInstallMirrors,
|
||||||
pub(crate) refresh: Refresh,
|
pub(crate) refresh: Refresh,
|
||||||
pub(crate) settings: ResolverInstallerSettings,
|
pub(crate) settings: ResolverInstallerSettings,
|
||||||
|
pub(crate) output_format: SyncFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SyncSettings {
|
impl SyncSettings {
|
||||||
|
|
@ -1194,6 +1195,7 @@ impl SyncSettings {
|
||||||
python_platform,
|
python_platform,
|
||||||
check,
|
check,
|
||||||
no_check,
|
no_check,
|
||||||
|
output_format,
|
||||||
} = args;
|
} = args;
|
||||||
let install_mirrors = filesystem
|
let install_mirrors = filesystem
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -1213,6 +1215,7 @@ impl SyncSettings {
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
output_format,
|
||||||
locked,
|
locked,
|
||||||
frozen,
|
frozen,
|
||||||
dry_run,
|
dry_run,
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ impl TestContext {
|
||||||
pub fn with_filtered_python_names(mut self) -> Self {
|
pub fn with_filtered_python_names(mut self) -> Self {
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
self.filters
|
self.filters
|
||||||
.push(("python.exe".to_string(), "python".to_string()));
|
.push((r"python\.exe".to_string(), "python".to_string()));
|
||||||
} else {
|
} else {
|
||||||
self.filters
|
self.filters
|
||||||
.push((r"python\d.\d\d".to_string(), "python".to_string()));
|
.push((r"python\d.\d\d".to_string(), "python".to_string()));
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1114,7 +1114,12 @@ uv sync [OPTIONS]
|
||||||
</dd><dt id="uv-sync--only-group"><a href="#uv-sync--only-group"><code>--only-group</code></a> <i>only-group</i></dt><dd><p>Only include dependencies from the specified dependency group.</p>
|
</dd><dt id="uv-sync--only-group"><a href="#uv-sync--only-group"><code>--only-group</code></a> <i>only-group</i></dt><dd><p>Only include dependencies from the specified dependency group.</p>
|
||||||
<p>The project and its dependencies will be omitted.</p>
|
<p>The project and its dependencies will be omitted.</p>
|
||||||
<p>May be provided multiple times. Implies <code>--no-default-groups</code>.</p>
|
<p>May be provided multiple times. Implies <code>--no-default-groups</code>.</p>
|
||||||
</dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for a specific package in the workspace.</p>
|
</dd><dt id="uv-sync--output-format"><a href="#uv-sync--output-format"><code>--output-format</code></a> <i>output-format</i></dt><dd><p>Select the output format</p>
|
||||||
|
<p>[default: text]</p><p>Possible values:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>text</code>: Display the result in a human-readable format</li>
|
||||||
|
<li><code>json</code>: Display the result in JSON format</li>
|
||||||
|
</ul></dd><dt id="uv-sync--package"><a href="#uv-sync--package"><code>--package</code></a> <i>package</i></dt><dd><p>Sync for a specific package in the workspace.</p>
|
||||||
<p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member package.</p>
|
<p>The workspace's environment (<code>.venv</code>) is updated to reflect the subset of dependencies declared by the specified workspace member package.</p>
|
||||||
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
<p>If the workspace member does not exist, uv will exit with an error.</p>
|
||||||
</dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
</dd><dt id="uv-sync--prerelease"><a href="#uv-sync--prerelease"><code>--prerelease</code></a> <i>prerelease</i></dt><dd><p>The strategy to use when considering pre-release versions.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue