mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-17 02:52:45 +00:00
Add an experimental uv format command (#15017)
As a frontend to Ruff's formatter. There are some interesting choices here, some of which may just be temporary: 1. We pin a default version of Ruff, so `uv format` is stable for a given uv version 2. We install Ruff from GitHub instead of PyPI, which means we don't need a Python interpreter or environment 3. We do not read the Ruff version from the dependency tree See https://github.com/astral-sh/ruff/pull/19665 for a prototype of the LSP integration.
This commit is contained in:
parent
8d6ea3f2ea
commit
e31f000da7
22 changed files with 1110 additions and 5 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -5043,6 +5043,7 @@ dependencies = [
|
|||
"unicode-width 0.2.1",
|
||||
"url",
|
||||
"uv-auth",
|
||||
"uv-bin-install",
|
||||
"uv-build-backend",
|
||||
"uv-build-frontend",
|
||||
"uv-cache",
|
||||
|
|
@ -5154,6 +5155,29 @@ dependencies = [
|
|||
"uv-workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uv-bin-install"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"fs-err",
|
||||
"futures",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
"reqwest-retry",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"uv-cache",
|
||||
"uv-client",
|
||||
"uv-distribution-filename",
|
||||
"uv-extract",
|
||||
"uv-pep440",
|
||||
"uv-platform",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uv-build"
|
||||
version = "0.8.12"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ license = "MIT OR Apache-2.0"
|
|||
|
||||
[workspace.dependencies]
|
||||
uv-auth = { path = "crates/uv-auth" }
|
||||
uv-bin-install = { path = "crates/uv-bin-install" }
|
||||
uv-build-backend = { path = "crates/uv-build-backend" }
|
||||
uv-build-frontend = { path = "crates/uv-build-frontend" }
|
||||
uv-cache = { path = "crates/uv-cache" }
|
||||
|
|
|
|||
37
crates/uv-bin-install/Cargo.toml
Normal file
37
crates/uv-bin-install/Cargo.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
[package]
|
||||
name = "uv-bin-install"
|
||||
version = "0.0.1"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
license = { workspace = true }
|
||||
description = "Binary download and installation utilities for uv"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
uv-cache = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-extract = { workspace = true }
|
||||
uv-pep440 = { workspace = true }
|
||||
uv-platform = { workspace = true }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
futures = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
reqwest-retry = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
431
crates/uv-bin-install/src/lib.rs
Normal file
431
crates/uv-bin-install/src/lib.rs
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
//! Binary download and installation utilities for uv.
|
||||
//!
|
||||
//! These utilities are specifically for consuming distributions that are _not_ Python packages,
|
||||
//! e.g., `ruff` (which does have a Python package, but also has standalone binaries on GitHub).
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use reqwest_retry::RetryPolicy;
|
||||
use std::fmt;
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, ReadBuf};
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
use uv_distribution_filename::SourceDistExtension;
|
||||
|
||||
use uv_cache::{Cache, CacheBucket, CacheEntry};
|
||||
use uv_client::{BaseClient, is_extended_transient_error};
|
||||
use uv_extract::{Error as ExtractError, stream};
|
||||
use uv_pep440::Version;
|
||||
use uv_platform::Platform;
|
||||
|
||||
/// Binary tools that can be installed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Binary {
|
||||
Ruff,
|
||||
}
|
||||
|
||||
impl Binary {
|
||||
/// Get the default version for this binary.
|
||||
pub fn default_version(&self) -> Version {
|
||||
match self {
|
||||
// TODO(zanieb): Figure out a nice way to automate updating this
|
||||
Self::Ruff => Version::new([0, 12, 5]),
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the binary.
|
||||
///
|
||||
/// See [`Binary::executable`] for the platform-specific executable name.
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Ruff => "ruff",
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the download URL for a specific version and platform.
|
||||
pub fn download_url(
|
||||
&self,
|
||||
version: &Version,
|
||||
platform: &str,
|
||||
format: ArchiveFormat,
|
||||
) -> Result<Url, Error> {
|
||||
match self {
|
||||
Self::Ruff => {
|
||||
let url = format!(
|
||||
"https://github.com/astral-sh/ruff/releases/download/{version}/ruff-{platform}.{}",
|
||||
format.extension()
|
||||
);
|
||||
Url::parse(&url).map_err(|err| Error::UrlParse { url, source: err })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the executable name
|
||||
pub fn executable(&self) -> String {
|
||||
format!("{}{}", self.name(), std::env::consts::EXE_SUFFIX)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Binary {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Archive formats for binary downloads.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ArchiveFormat {
|
||||
Zip,
|
||||
TarGz,
|
||||
}
|
||||
|
||||
impl ArchiveFormat {
|
||||
/// Get the file extension for this archive format.
|
||||
pub fn extension(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Zip => "zip",
|
||||
Self::TarGz => "tar.gz",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ArchiveFormat> for SourceDistExtension {
|
||||
fn from(val: ArchiveFormat) -> Self {
|
||||
match val {
|
||||
ArchiveFormat::Zip => Self::Zip,
|
||||
ArchiveFormat::TarGz => Self::TarGz,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur during binary download and installation.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Failed to download from: {url}")]
|
||||
Download {
|
||||
url: Url,
|
||||
#[source]
|
||||
source: reqwest_middleware::Error,
|
||||
},
|
||||
|
||||
#[error("Failed to parse URL: {url}")]
|
||||
UrlParse {
|
||||
url: String,
|
||||
#[source]
|
||||
source: url::ParseError,
|
||||
},
|
||||
|
||||
#[error("Failed to extract archive")]
|
||||
Extract {
|
||||
#[source]
|
||||
source: ExtractError,
|
||||
},
|
||||
|
||||
#[error("Binary not found in archive at expected location: {expected}")]
|
||||
BinaryNotFound { expected: PathBuf },
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to detect platform")]
|
||||
Platform(#[from] uv_platform::Error),
|
||||
|
||||
#[error("Attempt failed after {retries} retries")]
|
||||
RetriedError {
|
||||
#[source]
|
||||
err: Box<Error>,
|
||||
retries: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Return the number of attempts that were made to complete this request before this error was
|
||||
/// returned. Note that e.g. 3 retries equates to 4 attempts.
|
||||
fn attempts(&self) -> u32 {
|
||||
if let Self::RetriedError { retries, .. } = self {
|
||||
return retries + 1;
|
||||
}
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Install the given binary.
|
||||
pub async fn bin_install(
|
||||
binary: Binary,
|
||||
version: &Version,
|
||||
client: &BaseClient,
|
||||
cache: &Cache,
|
||||
reporter: &dyn Reporter,
|
||||
) -> Result<PathBuf, Error> {
|
||||
let platform = Platform::from_env()?;
|
||||
let platform_name = platform.as_cargo_dist_triple();
|
||||
|
||||
// Check the cache first
|
||||
let cache_entry = CacheEntry::new(
|
||||
cache
|
||||
.bucket(CacheBucket::Binaries)
|
||||
.join(binary.name())
|
||||
.join(version.to_string())
|
||||
.join(&platform_name),
|
||||
binary.executable(),
|
||||
);
|
||||
|
||||
if cache_entry.path().exists() {
|
||||
return Ok(cache_entry.into_path_buf());
|
||||
}
|
||||
|
||||
let format = if platform.os.is_windows() {
|
||||
ArchiveFormat::Zip
|
||||
} else {
|
||||
ArchiveFormat::TarGz
|
||||
};
|
||||
|
||||
let download_url = binary.download_url(version, &platform_name, format)?;
|
||||
|
||||
let cache_dir = cache_entry.dir();
|
||||
fs_err::tokio::create_dir_all(&cache_dir).await?;
|
||||
|
||||
let path = download_and_unpack_with_retry(
|
||||
binary,
|
||||
version,
|
||||
client,
|
||||
cache,
|
||||
reporter,
|
||||
&platform_name,
|
||||
format,
|
||||
&download_url,
|
||||
&cache_entry,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add executable bit
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::fs::Permissions;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let permissions = fs_err::tokio::metadata(&path).await?.permissions();
|
||||
if permissions.mode() & 0o111 != 0o111 {
|
||||
fs_err::tokio::set_permissions(
|
||||
&path,
|
||||
Permissions::from_mode(permissions.mode() | 0o111),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Download and unpack a binary with retry on stream failures.
|
||||
async fn download_and_unpack_with_retry(
|
||||
binary: Binary,
|
||||
version: &Version,
|
||||
client: &BaseClient,
|
||||
cache: &Cache,
|
||||
reporter: &dyn Reporter,
|
||||
platform_name: &str,
|
||||
format: ArchiveFormat,
|
||||
download_url: &Url,
|
||||
cache_entry: &CacheEntry,
|
||||
) -> Result<PathBuf, Error> {
|
||||
let mut total_attempts = 0;
|
||||
let mut retried_here = false;
|
||||
let start_time = SystemTime::now();
|
||||
let retry_policy = client.retry_policy();
|
||||
|
||||
loop {
|
||||
let result = download_and_unpack(
|
||||
binary,
|
||||
version,
|
||||
client,
|
||||
cache,
|
||||
reporter,
|
||||
platform_name,
|
||||
format,
|
||||
download_url,
|
||||
cache_entry,
|
||||
)
|
||||
.await;
|
||||
|
||||
let result = match result {
|
||||
Ok(path) => Ok(path),
|
||||
Err(err) => {
|
||||
total_attempts += err.attempts();
|
||||
let past_retries = total_attempts - 1;
|
||||
|
||||
if is_extended_transient_error(&err) {
|
||||
let retry_decision = retry_policy.should_retry(start_time, past_retries);
|
||||
if let reqwest_retry::RetryDecision::Retry { execute_after } = retry_decision {
|
||||
debug!(
|
||||
"Transient failure while installing {} {}; retrying...",
|
||||
binary.name(),
|
||||
version
|
||||
);
|
||||
let duration = execute_after
|
||||
.duration_since(SystemTime::now())
|
||||
.unwrap_or_else(|_| Duration::default());
|
||||
tokio::time::sleep(duration).await;
|
||||
retried_here = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if retried_here {
|
||||
Err(Error::RetriedError {
|
||||
err: Box::new(err),
|
||||
retries: past_retries,
|
||||
})
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// Download and unpackage a binary,
|
||||
///
|
||||
/// NOTE [`download_and_unpack_with_retry`] should be used instead.
|
||||
async fn download_and_unpack(
|
||||
binary: Binary,
|
||||
version: &Version,
|
||||
client: &BaseClient,
|
||||
cache: &Cache,
|
||||
reporter: &dyn Reporter,
|
||||
platform_name: &str,
|
||||
format: ArchiveFormat,
|
||||
download_url: &Url,
|
||||
cache_entry: &CacheEntry,
|
||||
) -> Result<PathBuf, Error> {
|
||||
// Create a temporary directory for extraction
|
||||
let temp_dir = tempfile::tempdir_in(cache.bucket(CacheBucket::Binaries))?;
|
||||
|
||||
let response = client
|
||||
.for_host(&download_url.clone().into())
|
||||
.get(download_url.clone())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| Error::Download {
|
||||
url: download_url.clone(),
|
||||
source: err,
|
||||
})?;
|
||||
|
||||
let inner_retries = response
|
||||
.extensions()
|
||||
.get::<reqwest_retry::RetryCount>()
|
||||
.map(|retries| retries.value());
|
||||
|
||||
if let Err(status_error) = response.error_for_status_ref() {
|
||||
let err = Error::Download {
|
||||
url: download_url.clone(),
|
||||
source: reqwest_middleware::Error::from(status_error),
|
||||
};
|
||||
if let Some(retries) = inner_retries {
|
||||
return Err(Error::RetriedError {
|
||||
err: Box::new(err),
|
||||
retries,
|
||||
});
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Get the download size from headers if available
|
||||
let size = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_LENGTH)
|
||||
.and_then(|val| val.to_str().ok())
|
||||
.and_then(|val| val.parse::<u64>().ok());
|
||||
|
||||
// Stream download directly to extraction
|
||||
let reader = response
|
||||
.bytes_stream()
|
||||
.map_err(std::io::Error::other)
|
||||
.into_async_read()
|
||||
.compat();
|
||||
|
||||
let id = reporter.on_download_start(binary.name(), version, size);
|
||||
let mut progress_reader = ProgressReader::new(reader, id, reporter);
|
||||
stream::archive(&mut progress_reader, format.into(), temp_dir.path())
|
||||
.await
|
||||
.map_err(|e| Error::Extract { source: e })?;
|
||||
reporter.on_download_complete(id);
|
||||
|
||||
// Find the binary in the extracted files
|
||||
let extracted_binary = match format {
|
||||
ArchiveFormat::Zip => {
|
||||
// Windows ZIP archives contain the binary directly in the root
|
||||
temp_dir.path().join(binary.executable())
|
||||
}
|
||||
ArchiveFormat::TarGz => {
|
||||
// tar.gz archives contain the binary in a subdirectory
|
||||
temp_dir
|
||||
.path()
|
||||
.join(format!("{}-{platform_name}", binary.name()))
|
||||
.join(binary.executable())
|
||||
}
|
||||
};
|
||||
|
||||
if !extracted_binary.exists() {
|
||||
return Err(Error::BinaryNotFound {
|
||||
expected: extracted_binary,
|
||||
});
|
||||
}
|
||||
|
||||
// Move the binary to its final location before the temp directory is dropped
|
||||
fs_err::tokio::rename(&extracted_binary, cache_entry.path()).await?;
|
||||
|
||||
Ok(cache_entry.path().to_path_buf())
|
||||
}
|
||||
|
||||
/// Progress reporter for binary downloads.
|
||||
pub trait Reporter: Send + Sync {
|
||||
/// Called when a download starts.
|
||||
fn on_download_start(&self, name: &str, version: &Version, size: Option<u64>) -> usize;
|
||||
/// Called when download progress is made.
|
||||
fn on_download_progress(&self, id: usize, inc: u64);
|
||||
/// Called when a download completes.
|
||||
fn on_download_complete(&self, id: usize);
|
||||
}
|
||||
|
||||
/// An asynchronous reader that reports progress as bytes are read.
|
||||
struct ProgressReader<'a, R> {
|
||||
reader: R,
|
||||
index: usize,
|
||||
reporter: &'a dyn Reporter,
|
||||
}
|
||||
|
||||
impl<'a, R> ProgressReader<'a, R> {
|
||||
/// Create a new [`ProgressReader`] that wraps another reader.
|
||||
fn new(reader: R, index: usize, reporter: &'a dyn Reporter) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
index,
|
||||
reporter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsyncRead for ProgressReader<'_, R>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
Pin::new(&mut self.as_mut().reader)
|
||||
.poll_read(cx, buf)
|
||||
.map_ok(|()| {
|
||||
self.reporter
|
||||
.on_download_progress(self.index, buf.filled().len() as u64);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -987,6 +987,8 @@ pub enum CacheBucket {
|
|||
Environments,
|
||||
/// Cached Python downloads
|
||||
Python,
|
||||
/// Downloaded tool binaries (e.g., Ruff).
|
||||
Binaries,
|
||||
}
|
||||
|
||||
impl CacheBucket {
|
||||
|
|
@ -1010,6 +1012,7 @@ impl CacheBucket {
|
|||
Self::Builds => "builds-v0",
|
||||
Self::Environments => "environments-v2",
|
||||
Self::Python => "python-v0",
|
||||
Self::Binaries => "binaries-v0",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1116,7 +1119,8 @@ impl CacheBucket {
|
|||
| Self::Archive
|
||||
| Self::Builds
|
||||
| Self::Environments
|
||||
| Self::Python => {
|
||||
| Self::Python
|
||||
| Self::Binaries => {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
|
@ -1135,6 +1139,7 @@ impl CacheBucket {
|
|||
Self::Archive,
|
||||
Self::Builds,
|
||||
Self::Environments,
|
||||
Self::Binaries,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
|
|
|
|||
|
|
@ -1006,6 +1006,21 @@ pub enum ProjectCommand {
|
|||
Export(ExportArgs),
|
||||
/// Display the project's dependency tree.
|
||||
Tree(TreeArgs),
|
||||
/// Format Python code in the project.
|
||||
///
|
||||
/// Formats Python code using the Ruff formatter. By default, all Python files in the project
|
||||
/// are formatted. This command has the same behavior as running `ruff format` in the project
|
||||
/// root.
|
||||
///
|
||||
/// To check if files are formatted without modifying them, use `--check`. To see a diff of
|
||||
/// formatting changes, use `--diff`.
|
||||
///
|
||||
/// By default, Additional arguments can be passed to Ruff after `--`.
|
||||
#[command(
|
||||
after_help = "Use `uv help format` for more details.",
|
||||
after_long_help = ""
|
||||
)]
|
||||
Format(FormatArgs),
|
||||
}
|
||||
|
||||
/// A re-implementation of `Option`, used to avoid Clap's automatic `Option` flattening in
|
||||
|
|
@ -4281,6 +4296,32 @@ pub struct ExportArgs {
|
|||
pub python: Option<Maybe<String>>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct FormatArgs {
|
||||
/// Check if files are formatted without applying changes.
|
||||
#[arg(long)]
|
||||
pub check: bool,
|
||||
|
||||
/// Show a diff of formatting changes without applying them.
|
||||
///
|
||||
/// Implies `--check`.
|
||||
#[arg(long)]
|
||||
pub diff: bool,
|
||||
|
||||
/// The version of Ruff to use for formatting.
|
||||
///
|
||||
/// By default, a version of Ruff pinned by uv will be used.
|
||||
#[arg(long)]
|
||||
pub version: Option<String>,
|
||||
|
||||
/// Additional arguments to pass to Ruff.
|
||||
///
|
||||
/// For example, use `uv format -- --line-length 100` to set the line length or
|
||||
/// `uv format -- src/module/foo.py` to format a specific file.
|
||||
#[arg(last = true)]
|
||||
pub extra_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct ToolNamespace {
|
||||
#[command(subcommand)]
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ bitflags::bitflags! {
|
|||
const PACKAGE_CONFLICTS = 1 << 5;
|
||||
const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
|
||||
const DETECT_MODULE_CONFLICTS = 1 << 7;
|
||||
const FORMAT = 1 << 8;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ impl PreviewFeatures {
|
|||
Self::PACKAGE_CONFLICTS => "package-conflicts",
|
||||
Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
|
||||
Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts",
|
||||
Self::FORMAT => "format",
|
||||
_ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +81,7 @@ impl FromStr for PreviewFeatures {
|
|||
"package-conflicts" => Self::PACKAGE_CONFLICTS,
|
||||
"extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
|
||||
"detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS,
|
||||
"format" => Self::FORMAT,
|
||||
_ => {
|
||||
warn_user_once!("Unknown preview feature: `{part}`");
|
||||
continue;
|
||||
|
|
@ -253,6 +256,7 @@ mod tests {
|
|||
PreviewFeatures::DETECT_MODULE_CONFLICTS.flag_as_str(),
|
||||
"detect-module-conflicts"
|
||||
);
|
||||
assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -120,6 +120,49 @@ impl Platform {
|
|||
|
||||
true
|
||||
}
|
||||
|
||||
/// Convert this platform to a `cargo-dist` style triple string.
|
||||
pub fn as_cargo_dist_triple(&self) -> String {
|
||||
use target_lexicon::{
|
||||
Architecture, ArmArchitecture, OperatingSystem, Riscv64Architecture, X86_32Architecture,
|
||||
};
|
||||
|
||||
let Self { os, arch, libc } = &self;
|
||||
|
||||
let arch_name = match arch.family() {
|
||||
// Special cases where Display doesn't match target triple
|
||||
Architecture::X86_32(X86_32Architecture::I686) => "i686".to_string(),
|
||||
Architecture::Riscv64(Riscv64Architecture::Riscv64) => "riscv64gc".to_string(),
|
||||
_ => arch.to_string(),
|
||||
};
|
||||
let vendor = match &**os {
|
||||
OperatingSystem::Darwin(_) => "apple",
|
||||
OperatingSystem::Windows => "pc",
|
||||
_ => "unknown",
|
||||
};
|
||||
let os_name = match &**os {
|
||||
OperatingSystem::Darwin(_) => "darwin",
|
||||
_ => &os.to_string(),
|
||||
};
|
||||
|
||||
let abi = match (&**os, libc) {
|
||||
(OperatingSystem::Windows, _) => Some("msvc".to_string()),
|
||||
(OperatingSystem::Linux, Libc::Some(env)) => Some({
|
||||
// Special suffix for ARM with hardware float
|
||||
if matches!(arch.family(), Architecture::Arm(ArmArchitecture::Armv7)) {
|
||||
format!("{env}eabihf")
|
||||
} else {
|
||||
env.to_string()
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
format!(
|
||||
"{arch_name}-{vendor}-{os_name}{abi}",
|
||||
abi = abi.map(|abi| format!("-{abi}")).unwrap_or_default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Platform {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
uv-auth = { workspace = true }
|
||||
uv-bin-install = { workspace = true }
|
||||
uv-build-backend = { workspace = true }
|
||||
uv-build-frontend = { workspace = true }
|
||||
uv-cache = { workspace = true }
|
||||
|
|
@ -90,6 +91,7 @@ rkyv = { workspace = true }
|
|||
rustc-hash = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ pub(crate) use pip::tree::pip_tree;
|
|||
pub(crate) use pip::uninstall::pip_uninstall;
|
||||
pub(crate) use project::add::add;
|
||||
pub(crate) use project::export::export;
|
||||
pub(crate) use project::format::format;
|
||||
pub(crate) use project::init::{InitKind, InitProjectKind, init};
|
||||
pub(crate) use project::lock::lock;
|
||||
pub(crate) use project::remove::remove;
|
||||
|
|
|
|||
70
crates/uv/src/commands/project/format.rs
Normal file
70
crates/uv/src/commands/project/format.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::process::Command;
|
||||
|
||||
use uv_bin_install::{Binary, bin_install};
|
||||
use uv_cache::Cache;
|
||||
use uv_client::BaseClientBuilder;
|
||||
use uv_configuration::{Preview, PreviewFeatures};
|
||||
use uv_pep440::Version;
|
||||
use uv_warnings::warn_user;
|
||||
|
||||
use crate::child::run_to_completion;
|
||||
use crate::commands::ExitStatus;
|
||||
use crate::commands::reporters::BinaryDownloadReporter;
|
||||
use crate::printer::Printer;
|
||||
use crate::settings::NetworkSettings;
|
||||
|
||||
/// Run the formatter.
|
||||
pub(crate) async fn format(
|
||||
check: bool,
|
||||
diff: bool,
|
||||
extra_args: Vec<String>,
|
||||
version: Option<String>,
|
||||
network_settings: NetworkSettings,
|
||||
cache: Cache,
|
||||
printer: Printer,
|
||||
preview: Preview,
|
||||
) -> Result<ExitStatus> {
|
||||
// Check if the format feature is in preview
|
||||
if !preview.is_enabled(PreviewFeatures::FORMAT) {
|
||||
warn_user!(
|
||||
"`uv format` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
|
||||
PreviewFeatures::FORMAT
|
||||
);
|
||||
}
|
||||
// Parse version if provided
|
||||
let version = version.as_deref().map(Version::from_str).transpose()?;
|
||||
|
||||
let client = BaseClientBuilder::new()
|
||||
.retries_from_env()?
|
||||
.connectivity(network_settings.connectivity)
|
||||
.native_tls(network_settings.native_tls)
|
||||
.allow_insecure_host(network_settings.allow_insecure_host.clone())
|
||||
.build();
|
||||
|
||||
// Get the path to Ruff, downloading it if necessary
|
||||
let reporter = BinaryDownloadReporter::single(printer);
|
||||
let default_version = Binary::Ruff.default_version();
|
||||
let version = version.as_ref().unwrap_or(&default_version);
|
||||
let ruff_path = bin_install(Binary::Ruff, version, &client, &cache, &reporter)
|
||||
.await
|
||||
.context("Failed to install ruff {version}")?;
|
||||
|
||||
let mut command = Command::new(&ruff_path);
|
||||
command.arg("format");
|
||||
|
||||
if check {
|
||||
command.arg("--check");
|
||||
}
|
||||
if diff {
|
||||
command.arg("--diff");
|
||||
}
|
||||
|
||||
// Add any additional arguments passed after `--`
|
||||
command.args(extra_args.iter());
|
||||
|
||||
let handle = command.spawn().context("Failed to spawn `ruff format`")?;
|
||||
run_to_completion(handle).await
|
||||
}
|
||||
|
|
@ -63,6 +63,7 @@ use crate::settings::{
|
|||
pub(crate) mod add;
|
||||
pub(crate) mod environment;
|
||||
pub(crate) mod export;
|
||||
pub(crate) mod format;
|
||||
pub(crate) mod init;
|
||||
mod install_target;
|
||||
pub(crate) mod lock;
|
||||
|
|
|
|||
|
|
@ -832,3 +832,32 @@ impl ColorDisplay for BuildableSource<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BinaryDownloadReporter {
|
||||
reporter: ProgressReporter,
|
||||
}
|
||||
|
||||
impl BinaryDownloadReporter {
|
||||
/// Initialize a [`BinaryDownloadReporter`] for a single binary download.
|
||||
pub(crate) fn single(printer: Printer) -> Self {
|
||||
let multi_progress = MultiProgress::with_draw_target(printer.target());
|
||||
let root = multi_progress.add(ProgressBar::with_draw_target(Some(1), printer.target()));
|
||||
let reporter = ProgressReporter::new(root, multi_progress, printer);
|
||||
Self { reporter }
|
||||
}
|
||||
}
|
||||
|
||||
impl uv_bin_install::Reporter for BinaryDownloadReporter {
|
||||
fn on_download_start(&self, name: &str, version: &Version, size: Option<u64>) -> usize {
|
||||
self.reporter
|
||||
.on_request_start(Direction::Download, format!("{name} v{version}"), size)
|
||||
}
|
||||
|
||||
fn on_download_progress(&self, id: usize, inc: u64) {
|
||||
self.reporter.on_request_progress(id, inc);
|
||||
}
|
||||
|
||||
fn on_download_complete(&self, id: usize) {
|
||||
self.reporter.on_request_complete(Direction::Download, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2187,6 +2187,26 @@ async fn run_project(
|
|||
.boxed_local()
|
||||
.await
|
||||
}
|
||||
ProjectCommand::Format(args) => {
|
||||
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||
let args = settings::FormatSettings::resolve(args, filesystem);
|
||||
show_settings!(args);
|
||||
|
||||
// Initialize the cache.
|
||||
let cache = cache.init()?;
|
||||
|
||||
Box::pin(commands::format(
|
||||
args.check,
|
||||
args.diff,
|
||||
args.extra_args,
|
||||
args.version,
|
||||
globals.network_settings,
|
||||
cache,
|
||||
printer,
|
||||
globals.preview,
|
||||
))
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use uv_cli::{
|
|||
ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBump, VersionFormat,
|
||||
};
|
||||
use uv_cli::{
|
||||
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs,
|
||||
ToolUpgradeArgs,
|
||||
AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs,
|
||||
ResolverInstallerArgs, ToolUpgradeArgs,
|
||||
options::{flag, resolver_installer_options, resolver_options},
|
||||
};
|
||||
use uv_client::Connectivity;
|
||||
|
|
@ -1873,6 +1873,34 @@ impl ExportSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `format` invocation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct FormatSettings {
|
||||
pub(crate) check: bool,
|
||||
pub(crate) diff: bool,
|
||||
pub(crate) extra_args: Vec<String>,
|
||||
pub(crate) version: Option<String>,
|
||||
}
|
||||
|
||||
impl FormatSettings {
|
||||
/// Resolve the [`FormatSettings`] from the CLI and filesystem configuration.
|
||||
pub(crate) fn resolve(args: FormatArgs, _filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let FormatArgs {
|
||||
check,
|
||||
diff,
|
||||
extra_args,
|
||||
version,
|
||||
} = args;
|
||||
|
||||
Self {
|
||||
check,
|
||||
diff,
|
||||
extra_args,
|
||||
version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolved settings to use for a `pip compile` invocation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PipCompileSettings {
|
||||
|
|
|
|||
|
|
@ -996,6 +996,14 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
/// Create a `uv format` command with options shared across scenarios.
|
||||
pub fn format(&self) -> Command {
|
||||
let mut command = Self::new_command();
|
||||
command.arg("format");
|
||||
self.add_shared_options(&mut command, false);
|
||||
command
|
||||
}
|
||||
|
||||
/// Create a `uv build` command with options shared across scenarios.
|
||||
pub fn build(&self) -> Command {
|
||||
let mut command = Self::new_command();
|
||||
|
|
|
|||
273
crates/uv/tests/it/format.rs
Normal file
273
crates/uv/tests/it/format.rs
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
use anyhow::Result;
|
||||
use assert_fs::prelude::*;
|
||||
use indoc::indoc;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::common::{TestContext, uv_snapshot};
|
||||
|
||||
#[test]
|
||||
fn format_project() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Create an unformatted Python file
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r#"
|
||||
import sys
|
||||
def hello():
|
||||
print( "Hello, World!" )
|
||||
if __name__=="__main__":
|
||||
hello( )
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.format(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
");
|
||||
|
||||
// Check that the file was formatted
|
||||
let formatted_content = fs_err::read_to_string(&main_py)?;
|
||||
assert_snapshot!(formatted_content, @r#"
|
||||
import sys
|
||||
|
||||
|
||||
def hello():
|
||||
print("Hello, World!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
hello()
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_check() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Create an unformatted Python file
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r#"
|
||||
def hello():
|
||||
print( "Hello, World!" )
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.format().arg("--check"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Would reformat: main.py
|
||||
1 file would be reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
");
|
||||
|
||||
// Verify the file wasn't modified
|
||||
let content = fs_err::read_to_string(&main_py)?;
|
||||
assert_snapshot!(content, @r#"
|
||||
def hello():
|
||||
print( "Hello, World!" )
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_diff() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Create an unformatted Python file
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r#"
|
||||
def hello():
|
||||
print( "Hello, World!" )
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.format().arg("--diff"), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
--- main.py
|
||||
+++ main.py
|
||||
@@ -1,2 +1,2 @@
|
||||
-def hello():
|
||||
- print( "Hello, World!" )
|
||||
+def hello():
|
||||
+ print("Hello, World!")
|
||||
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
1 file would be reformatted
|
||||
"#);
|
||||
|
||||
// Verify the file wasn't modified
|
||||
let content = fs_err::read_to_string(&main_py)?;
|
||||
assert_snapshot!(content, @r#"
|
||||
def hello():
|
||||
print( "Hello, World!" )
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_with_ruff_args() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Create a Python file with a long line
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r#"
|
||||
def hello():
|
||||
print("This is a very long line that should normally be wrapped by the formatter but we will configure it to have a longer line length")
|
||||
"#})?;
|
||||
|
||||
// Run format with custom line length
|
||||
uv_snapshot!(context.filters(), context.format().arg("--").arg("main.py").arg("--line-length").arg("200"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file left unchanged
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
");
|
||||
|
||||
// Check that the line wasn't wrapped (since we set a long line length)
|
||||
let formatted_content = fs_err::read_to_string(&main_py)?;
|
||||
assert_snapshot!(formatted_content, @r#"
|
||||
def hello():
|
||||
print("This is a very long line that should normally be wrapped by the formatter but we will configure it to have a longer line length")
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_specific_files() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
// Create multiple unformatted Python files
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r#"
|
||||
def main():
|
||||
print( "Main" )
|
||||
"#})?;
|
||||
|
||||
let utils_py = context.temp_dir.child("utils.py");
|
||||
utils_py.write_str(indoc! {r#"
|
||||
def utils():
|
||||
print( "utils" )
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.format().arg("--").arg("main.py"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
");
|
||||
|
||||
let main_content = fs_err::read_to_string(&main_py)?;
|
||||
assert_snapshot!(main_content, @r#"
|
||||
def main():
|
||||
print("Main")
|
||||
"#);
|
||||
|
||||
// Unchanged
|
||||
let utils_content = fs_err::read_to_string(&utils_py)?;
|
||||
assert_snapshot!(utils_content, @r#"
|
||||
def utils():
|
||||
print( "utils" )
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_version_option() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&[]);
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(indoc! {r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = []
|
||||
"#})?;
|
||||
|
||||
let main_py = context.temp_dir.child("main.py");
|
||||
main_py.write_str(indoc! {r"
|
||||
def hello(): pass
|
||||
"})?;
|
||||
|
||||
// Run format with specific Ruff version
|
||||
// TODO(zanieb): It'd be nice to assert on the version used here somehow? Maybe we should emit
|
||||
// the version we're using to stderr? Alas there's not a way to get the Ruff version from the
|
||||
// format command :)
|
||||
uv_snapshot!(context.filters(), context.format().arg("--version").arg("0.8.2"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
1 file reformatted
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning.
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ fn help() {
|
|||
lock Update the project's lockfile
|
||||
export Export the project's lockfile to an alternate format
|
||||
tree Display the project's dependency tree
|
||||
format Format Python code in the project
|
||||
tool Run and install commands provided by Python packages
|
||||
python Manage Python versions and installations
|
||||
pip Manage Python packages with a pip-compatible interface
|
||||
|
|
@ -105,6 +106,7 @@ fn help_flag() {
|
|||
lock Update the project's lockfile
|
||||
export Export the project's lockfile to an alternate format
|
||||
tree Display the project's dependency tree
|
||||
format Format Python code in the project
|
||||
tool Run and install commands provided by Python packages
|
||||
python Manage Python versions and installations
|
||||
pip Manage Python packages with a pip-compatible interface
|
||||
|
|
@ -183,6 +185,7 @@ fn help_short_flag() {
|
|||
lock Update the project's lockfile
|
||||
export Export the project's lockfile to an alternate format
|
||||
tree Display the project's dependency tree
|
||||
format Format Python code in the project
|
||||
tool Run and install commands provided by Python packages
|
||||
python Manage Python versions and installations
|
||||
pip Manage Python packages with a pip-compatible interface
|
||||
|
|
@ -880,6 +883,7 @@ fn help_unknown_subcommand() {
|
|||
lock
|
||||
export
|
||||
tree
|
||||
format
|
||||
tool
|
||||
python
|
||||
pip
|
||||
|
|
@ -907,6 +911,7 @@ fn help_unknown_subcommand() {
|
|||
lock
|
||||
export
|
||||
tree
|
||||
format
|
||||
tool
|
||||
python
|
||||
pip
|
||||
|
|
@ -963,6 +968,7 @@ fn help_with_global_option() {
|
|||
lock Update the project's lockfile
|
||||
export Export the project's lockfile to an alternate format
|
||||
tree Display the project's dependency tree
|
||||
format Format Python code in the project
|
||||
tool Run and install commands provided by Python packages
|
||||
python Manage Python versions and installations
|
||||
pip Manage Python packages with a pip-compatible interface
|
||||
|
|
@ -1084,6 +1090,7 @@ fn help_with_no_pager() {
|
|||
lock Update the project's lockfile
|
||||
export Export the project's lockfile to an alternate format
|
||||
tree Display the project's dependency tree
|
||||
format Format Python code in the project
|
||||
tool Run and install commands provided by Python packages
|
||||
python Manage Python versions and installations
|
||||
pip Manage Python packages with a pip-compatible interface
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ mod edit;
|
|||
#[cfg(all(feature = "python", feature = "pypi"))]
|
||||
mod export;
|
||||
|
||||
#[cfg(all(feature = "python", feature = "pypi"))]
|
||||
mod format;
|
||||
|
||||
mod help;
|
||||
|
||||
#[cfg(all(feature = "python", feature = "pypi", feature = "git"))]
|
||||
|
|
|
|||
|
|
@ -7720,7 +7720,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
@ -7946,7 +7946,7 @@ fn preview_features() {
|
|||
show_settings: true,
|
||||
preview: Preview {
|
||||
flags: PreviewFeatures(
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS,
|
||||
PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT,
|
||||
),
|
||||
},
|
||||
python_preference: Managed,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ The following preview features are available:
|
|||
[installing `python` and `python3` executables](./python-versions.md#installing-python-executables).
|
||||
- `python-upgrade`: Allows
|
||||
[transparent Python version upgrades](./python-versions.md#upgrading-python-versions).
|
||||
- `format`: Allows using `uv format`.
|
||||
|
||||
## Disabling preview features
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ uv [OPTIONS] <COMMAND>
|
|||
<dt><a href="#uv-lock"><code>uv lock</code></a></dt><dd><p>Update the project's lockfile</p></dd>
|
||||
<dt><a href="#uv-export"><code>uv export</code></a></dt><dd><p>Export the project's lockfile to an alternate format</p></dd>
|
||||
<dt><a href="#uv-tree"><code>uv tree</code></a></dt><dd><p>Display the project's dependency tree</p></dd>
|
||||
<dt><a href="#uv-format"><code>uv format</code></a></dt><dd><p>Format Python code in the project</p></dd>
|
||||
<dt><a href="#uv-tool"><code>uv tool</code></a></dt><dd><p>Run and install commands provided by Python packages</p></dd>
|
||||
<dt><a href="#uv-python"><code>uv python</code></a></dt><dd><p>Manage Python versions and installations</p></dd>
|
||||
<dt><a href="#uv-pip"><code>uv pip</code></a></dt><dd><p>Manage Python packages with a pip-compatible interface</p></dd>
|
||||
|
|
@ -1841,6 +1842,81 @@ interpreter. Use <code>--universal</code> to display the tree for all platforms,
|
|||
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
|
||||
</dd></dl>
|
||||
|
||||
## uv format
|
||||
|
||||
Format Python code in the project.
|
||||
|
||||
Formats Python code using the Ruff formatter. By default, all Python files in the project are formatted. This command has the same behavior as running `ruff format` in the project root.
|
||||
|
||||
To check if files are formatted without modifying them, use `--check`. To see a diff of formatting changes, use `--diff`.
|
||||
|
||||
By default, Additional arguments can be passed to Ruff after `--`.
|
||||
|
||||
<h3 class="cli-reference">Usage</h3>
|
||||
|
||||
```
|
||||
uv format [OPTIONS] [-- <EXTRA_ARGS>...]
|
||||
```
|
||||
|
||||
<h3 class="cli-reference">Arguments</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="uv-format--extra_args"><a href="#uv-format--extra_args"<code>EXTRA_ARGS</code></a></dt><dd><p>Additional arguments to pass to Ruff.</p>
|
||||
<p>For example, use <code>uv format -- --line-length 100</code> to set the line length or <code>uv format -- src/module/foo.py</code> to format a specific file.</p>
|
||||
</dd></dl>
|
||||
|
||||
<h3 class="cli-reference">Options</h3>
|
||||
|
||||
<dl class="cli-reference"><dt id="uv-format--allow-insecure-host"><a href="#uv-format--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>
|
||||
<p>Can be provided multiple times.</p>
|
||||
<p>Expects to receive either a hostname (e.g., <code>localhost</code>), a host-port pair (e.g., <code>localhost:8080</code>), or a URL (e.g., <code>https://localhost</code>).</p>
|
||||
<p>WARNING: Hosts included in this list will not be verified against the system's certificate store. Only use <code>--allow-insecure-host</code> in a secure network with verified sources, as it bypasses SSL verification and could expose you to MITM attacks.</p>
|
||||
<p>May also be set with the <code>UV_INSECURE_HOST</code> environment variable.</p></dd><dt id="uv-format--cache-dir"><a href="#uv-format--cache-dir"><code>--cache-dir</code></a> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
||||
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
|
||||
<p>To view the location of the cache directory, run <code>uv cache dir</code>.</p>
|
||||
<p>May also be set with the <code>UV_CACHE_DIR</code> environment variable.</p></dd><dt id="uv-format--check"><a href="#uv-format--check"><code>--check</code></a></dt><dd><p>Check if files are formatted without applying changes</p>
|
||||
</dd><dt id="uv-format--color"><a href="#uv-format--color"><code>--color</code></a> <i>color-choice</i></dt><dd><p>Control the use of color in output.</p>
|
||||
<p>By default, uv will automatically detect support for colors when writing to a terminal.</p>
|
||||
<p>Possible values:</p>
|
||||
<ul>
|
||||
<li><code>auto</code>: Enables colored output only when the output is going to a terminal or TTY with support</li>
|
||||
<li><code>always</code>: Enables colored output regardless of the detected environment</li>
|
||||
<li><code>never</code>: Disables colored output</li>
|
||||
</ul></dd><dt id="uv-format--config-file"><a href="#uv-format--config-file"><code>--config-file</code></a> <i>config-file</i></dt><dd><p>The path to a <code>uv.toml</code> file to use for configuration.</p>
|
||||
<p>While uv configuration can be included in a <code>pyproject.toml</code> file, it is not allowed in this context.</p>
|
||||
<p>May also be set with the <code>UV_CONFIG_FILE</code> environment variable.</p></dd><dt id="uv-format--diff"><a href="#uv-format--diff"><code>--diff</code></a></dt><dd><p>Show a diff of formatting changes without applying them.</p>
|
||||
<p>Implies <code>--check</code>.</p>
|
||||
</dd><dt id="uv-format--directory"><a href="#uv-format--directory"><code>--directory</code></a> <i>directory</i></dt><dd><p>Change to the given directory prior to running the command.</p>
|
||||
<p>Relative paths are resolved with the given directory as the base.</p>
|
||||
<p>See <code>--project</code> to only change the project root directory.</p>
|
||||
</dd><dt id="uv-format--help"><a href="#uv-format--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
|
||||
</dd><dt id="uv-format--managed-python"><a href="#uv-format--managed-python"><code>--managed-python</code></a></dt><dd><p>Require use of uv-managed Python versions.</p>
|
||||
<p>By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.</p>
|
||||
<p>May also be set with the <code>UV_MANAGED_PYTHON</code> environment variable.</p></dd><dt id="uv-format--native-tls"><a href="#uv-format--native-tls"><code>--native-tls</code></a></dt><dd><p>Whether to load TLS certificates from the platform's native certificate store.</p>
|
||||
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>
|
||||
<p>However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store.</p>
|
||||
<p>May also be set with the <code>UV_NATIVE_TLS</code> environment variable.</p></dd><dt id="uv-format--no-cache"><a href="#uv-format--no-cache"><code>--no-cache</code></a>, <code>--no-cache-dir</code>, <code>-n</code></dt><dd><p>Avoid reading from or writing to the cache, instead using a temporary directory for the duration of the operation</p>
|
||||
<p>May also be set with the <code>UV_NO_CACHE</code> environment variable.</p></dd><dt id="uv-format--no-config"><a href="#uv-format--no-config"><code>--no-config</code></a></dt><dd><p>Avoid discovering configuration files (<code>pyproject.toml</code>, <code>uv.toml</code>).</p>
|
||||
<p>Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.</p>
|
||||
<p>May also be set with the <code>UV_NO_CONFIG</code> environment variable.</p></dd><dt id="uv-format--no-managed-python"><a href="#uv-format--no-managed-python"><code>--no-managed-python</code></a></dt><dd><p>Disable use of uv-managed Python versions.</p>
|
||||
<p>Instead, uv will search for a suitable Python version on the system.</p>
|
||||
<p>May also be set with the <code>UV_NO_MANAGED_PYTHON</code> environment variable.</p></dd><dt id="uv-format--no-progress"><a href="#uv-format--no-progress"><code>--no-progress</code></a></dt><dd><p>Hide all progress outputs.</p>
|
||||
<p>For example, spinners or progress bars.</p>
|
||||
<p>May also be set with the <code>UV_NO_PROGRESS</code> environment variable.</p></dd><dt id="uv-format--no-python-downloads"><a href="#uv-format--no-python-downloads"><code>--no-python-downloads</code></a></dt><dd><p>Disable automatic downloads of Python.</p>
|
||||
</dd><dt id="uv-format--offline"><a href="#uv-format--offline"><code>--offline</code></a></dt><dd><p>Disable network access.</p>
|
||||
<p>When disabled, uv will only use locally cached data and locally available files.</p>
|
||||
<p>May also be set with the <code>UV_OFFLINE</code> environment variable.</p></dd><dt id="uv-format--project"><a href="#uv-format--project"><code>--project</code></a> <i>project</i></dt><dd><p>Run the command within the given project directory.</p>
|
||||
<p>All <code>pyproject.toml</code>, <code>uv.toml</code>, and <code>.python-version</code> files will be discovered by walking up the directory tree from the project root, as will the project's virtual environment (<code>.venv</code>).</p>
|
||||
<p>Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.</p>
|
||||
<p>See <code>--directory</code> to change the working directory entirely.</p>
|
||||
<p>This setting has no effect when used in the <code>uv pip</code> interface.</p>
|
||||
<p>May also be set with the <code>UV_PROJECT</code> environment variable.</p></dd><dt id="uv-format--quiet"><a href="#uv-format--quiet"><code>--quiet</code></a>, <code>-q</code></dt><dd><p>Use quiet output.</p>
|
||||
<p>Repeating this option, e.g., <code>-qq</code>, will enable a silent mode in which uv will write no output to stdout.</p>
|
||||
</dd><dt id="uv-format--verbose"><a href="#uv-format--verbose"><code>--verbose</code></a>, <code>-v</code></dt><dd><p>Use verbose output.</p>
|
||||
<p>You can configure fine-grained logging using the <code>RUST_LOG</code> environment variable. (<a href="https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives">https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives</a>)</p>
|
||||
</dd><dt id="uv-format--version"><a href="#uv-format--version"><code>--version</code></a> <i>version</i></dt><dd><p>The version of Ruff to use for formatting.</p>
|
||||
<p>By default, a version of Ruff pinned by uv will be used.</p>
|
||||
</dd></dl>
|
||||
|
||||
## uv tool
|
||||
|
||||
Run and install commands provided by Python packages
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue