diff --git a/Cargo.lock b/Cargo.lock index 5140b9061..1b561f029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4427,11 +4427,11 @@ dependencies = [ "uv-normalize", "uv-requirements", "uv-resolver", + "uv-settings", "uv-toolchain", "uv-types", "uv-virtualenv", "uv-warnings", - "uv-workspace", ] [[package]] @@ -4618,9 +4618,9 @@ dependencies = [ "uv-git", "uv-installer", "uv-resolver", + "uv-settings", "uv-toolchain", "uv-types", - "uv-workspace", "walkdir", ] @@ -4886,6 +4886,28 @@ dependencies = [ "uv-warnings", ] +[[package]] +name = "uv-settings" +version = "0.0.1" +dependencies = [ + "dirs-sys", + "distribution-types", + "fs-err", + "install-wheel-rs", + "pep508_rs", + "pypi-types", + "schemars", + "serde", + "thiserror", + "toml", + "tracing", + "uv-configuration", + "uv-fs", + "uv-normalize", + "uv-resolver", + "uv-toolchain", +] + [[package]] name = "uv-state" version = "0.0.1" @@ -4990,29 +5012,6 @@ dependencies = [ "rustc-hash", ] -[[package]] -name = "uv-workspace" -version = "0.0.1" -dependencies = [ - "dirs-sys", - "distribution-types", - "fs-err", - "install-wheel-rs", - "pep508_rs", - "pypi-types", - "schemars", - "serde", - "thiserror", - "toml", - "tracing", - "uv-configuration", - "uv-fs", - "uv-normalize", - "uv-resolver", - "uv-toolchain", - "uv-warnings", -] - [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 75cbc1f53..9e4d203d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,16 +38,16 @@ uv-extract = { path = "crates/uv-extract" } uv-fs = { path = "crates/uv-fs" } uv-git = { path = "crates/uv-git" } uv-installer = { path = "crates/uv-installer" } -uv-toolchain = { path = "crates/uv-toolchain" } uv-normalize = { path = "crates/uv-normalize" } uv-requirements = { path = "crates/uv-requirements" } uv-resolver = { path = "crates/uv-resolver" } +uv-settings = { path = "crates/uv-settings" } uv-state = { path = "crates/uv-state" } +uv-toolchain = { path = "crates/uv-toolchain" } uv-types = { path = "crates/uv-types" } uv-version = { path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" } uv-warnings = { path = "crates/uv-warnings" } -uv-workspace = { path = "crates/uv-workspace" } anstream = { version = "0.6.13" } anyhow = { version = "1.0.80" } diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 564a095df..2720ee9c5 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -30,10 +30,10 @@ uv-distribution = { workspace = true, features = ["schemars"] } uv-fs = { workspace = true } uv-git = { workspace = true } uv-installer = { workspace = true } -uv-toolchain = { workspace = true } uv-resolver = { workspace = true } +uv-settings = { workspace = true, features = ["schemars"] } +uv-toolchain = { workspace = true } uv-types = { workspace = true } -uv-workspace = { workspace = true, features = ["schemars"] } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace # dependencies, to ensure that we're forced to think twice before including them in other crates. diff --git a/crates/uv-dev/src/generate_json_schema.rs b/crates/uv-dev/src/generate_json_schema.rs index 354908f09..d3d342d0b 100644 --- a/crates/uv-dev/src/generate_json_schema.rs +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -6,7 +6,7 @@ use pretty_assertions::StrComparison; use schemars::{schema_for, JsonSchema}; use serde::Deserialize; -use uv_workspace::Options; +use uv_settings::Options; use crate::ROOT_DIR; diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-settings/Cargo.toml similarity index 94% rename from crates/uv-workspace/Cargo.toml rename to crates/uv-settings/Cargo.toml index cd579e888..abff2d85e 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "uv-workspace" +name = "uv-settings" version = "0.0.1" edition = { workspace = true } rust-version = { workspace = true } @@ -22,7 +22,6 @@ uv-fs = { workspace = true } uv-normalize = { workspace = true, features = ["schemars"] } uv-resolver = { workspace = true, features = ["schemars"] } uv-toolchain = { workspace = true, features = ["schemars"] } -uv-warnings = { workspace = true } dirs-sys = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-workspace/src/combine.rs b/crates/uv-settings/src/combine.rs similarity index 94% rename from crates/uv-workspace/src/combine.rs rename to crates/uv-settings/src/combine.rs index ea36715e3..b8a7de8c3 100644 --- a/crates/uv-workspace/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -7,7 +7,7 @@ use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, Targe use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_toolchain::PythonVersion; -use crate::{GlobalOptions, Options, PipOptions, ResolverInstallerOptions, Workspace}; +use crate::{FilesystemOptions, GlobalOptions, Options, PipOptions, ResolverInstallerOptions}; pub trait Combine { /// Combine two values, preferring the values in `self`. @@ -25,14 +25,13 @@ pub trait Combine { fn combine(self, other: Self) -> Self; } -impl Combine for Option { - /// Combine the options used in two [`Workspace`]s. Retains the root of `self`. - fn combine(self, other: Option) -> Option { +impl Combine for Option { + /// Combine the options used in two [`FilesystemOptions`]s. Retains the root of `self`. + fn combine(self, other: Option) -> Option { match (self, other) { - (Some(mut a), Some(b)) => { - a.options = a.options.combine(b.options); - Some(a) - } + (Some(a), Some(b)) => Some(FilesystemOptions( + a.into_options().combine(b.into_options()), + )), (a, b) => a.or(b), } } diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs new file mode 100644 index 000000000..a5bfb2f5b --- /dev/null +++ b/crates/uv-settings/src/lib.rs @@ -0,0 +1,167 @@ +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +use tracing::debug; + +use uv_fs::Simplified; + +pub use crate::combine::*; +pub use crate::settings::*; + +mod combine; +mod settings; + +/// The [`Options`] as loaded from a configuration file on disk. +#[derive(Debug, Clone)] +pub struct FilesystemOptions(Options); + +impl FilesystemOptions { + /// Convert the [`FilesystemOptions`] into [`Options`]. + pub fn into_options(self) -> Options { + self.0 + } +} + +impl Deref for FilesystemOptions { + type Target = Options; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FilesystemOptions { + /// Load the user [`FilesystemOptions`]. + pub fn user() -> Result, Error> { + let Some(dir) = config_dir() else { + return Ok(None); + }; + let root = dir.join("uv"); + let file = root.join("uv.toml"); + + debug!("Loading user configuration from: `{}`", file.display()); + match read_file(&file) { + Ok(options) => Ok(Some(Self(options))), + Err(Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(_) if !dir.is_dir() => { + // Ex) `XDG_CONFIG_HOME=/dev/null` + debug!( + "User configuration directory `{}` does not exist or is not a directory", + dir.display() + ); + Ok(None) + } + Err(err) => Err(err), + } + } + + /// Find the [`FilesystemOptions`] for the given path. + /// + /// The search starts at the given path and goes up the directory tree until a `uv.toml` file is + /// found. + pub fn find(path: impl AsRef) -> Result, Error> { + for ancestor in path.as_ref().ancestors() { + // Read a `uv.toml` file in the current directory. + let path = ancestor.join("uv.toml"); + match fs_err::read_to_string(&path) { + Ok(content) => { + let options: Options = toml::from_str(&content) + .map_err(|err| Error::UvToml(path.user_display().to_string(), err))?; + + debug!("Found workspace configuration at `{}`", path.display()); + return Ok(Some(Self(options))); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + } + Ok(None) + } + + /// Load a [`FilesystemOptions`] from a directory, preferring a `uv.toml` file over a + /// `pyproject.toml` file. + pub fn from_directory(dir: impl AsRef) -> Result, Error> { + // Read a `uv.toml` file in the current directory. + let path = dir.as_ref().join("uv.toml"); + match fs_err::read_to_string(&path) { + Ok(content) => { + let options: Options = toml::from_str(&content) + .map_err(|err| Error::UvToml(path.user_display().to_string(), err))?; + + debug!("Found workspace configuration at `{}`", path.display()); + return Ok(Some(Self(options))); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + + // Read a `pyproject.toml` file in the current directory. + let path = dir.as_ref().join("pyproject.toml"); + match fs_err::read_to_string(&path) { + Ok(content) => { + // Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section. + let pyproject: PyProjectToml = toml::from_str(&content) + .map_err(|err| Error::PyprojectToml(path.user_display().to_string(), err))?; + let Some(tool) = pyproject.tool else { + return Ok(None); + }; + let Some(options) = tool.uv else { + return Ok(None); + }; + + debug!("Found workspace configuration at `{}`", path.display()); + return Ok(Some(Self(options))); + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(err.into()), + } + + Ok(None) + } + + /// Load a [`FilesystemOptions`] from a `uv.toml` file. + pub fn from_file(path: impl AsRef) -> Result { + Ok(Self(read_file(path.as_ref())?)) + } +} + +/// Returns the path to the user configuration directory. +/// +/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the +/// `XDG_CONFIG_HOME` environment variable on both Linux _and_ macOS, rather than the +/// `Application Support` directory on macOS. +fn config_dir() -> Option { + // On Windows, use, e.g., C:\Users\Alice\AppData\Roaming + #[cfg(windows)] + { + dirs_sys::known_folder_roaming_app_data() + } + + // On Linux and macOS, use, e.g., /home/alice/.config. + #[cfg(not(windows))] + { + std::env::var_os("XDG_CONFIG_HOME") + .and_then(dirs_sys::is_absolute_path) + .or_else(|| dirs_sys::home_dir().map(|path| path.join(".config"))) + } +} + +/// Load [`Options`] from a `uv.toml` file. +fn read_file(path: &Path) -> Result { + let content = fs_err::read_to_string(path)?; + let options: Options = toml::from_str(&content) + .map_err(|err| Error::UvToml(path.user_display().to_string(), err))?; + Ok(options) +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error("Failed to parse: `{0}`")] + PyprojectToml(String, #[source] toml::de::Error), + + #[error("Failed to parse: `{0}`")] + UvToml(String, #[source] toml::de::Error), +} diff --git a/crates/uv-workspace/src/settings.rs b/crates/uv-settings/src/settings.rs similarity index 100% rename from crates/uv-workspace/src/settings.rs rename to crates/uv-settings/src/settings.rs diff --git a/crates/uv-workspace/src/lib.rs b/crates/uv-workspace/src/lib.rs deleted file mode 100644 index 8412bb26a..000000000 --- a/crates/uv-workspace/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub use crate::combine::*; -pub use crate::settings::*; -pub use crate::workspace::*; - -mod combine; -mod settings; -mod workspace; diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs deleted file mode 100644 index 8a0831754..000000000 --- a/crates/uv-workspace/src/workspace.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::path::{Path, PathBuf}; -use tracing::debug; - -use uv_fs::Simplified; -use uv_warnings::warn_user; - -use crate::{Options, PyProjectToml}; - -/// Represents a project workspace that contains a set of options and a root path. -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub struct Workspace { - pub options: Options, - pub root: PathBuf, -} - -impl Workspace { - /// Load the user [`Workspace`]. - pub fn user() -> Result, WorkspaceError> { - let Some(dir) = config_dir() else { - return Ok(None); - }; - let root = dir.join("uv"); - let file = root.join("uv.toml"); - - debug!("Loading user configuration from: `{}`", file.display()); - match read_file(&file) { - Ok(options) => Ok(Some(Self { options, root })), - Err(WorkspaceError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(_) if !dir.is_dir() => { - // Ex) `XDG_CONFIG_HOME=/dev/null` - debug!( - "User configuration directory `{}` does not exist or is not a directory", - dir.display() - ); - Ok(None) - } - Err(err) => Err(err), - } - } - - /// Find the [`Workspace`] for the given path. - /// - /// The search starts at the given path and goes up the directory tree until a workspace is - /// found. - pub fn find(path: impl AsRef) -> Result, WorkspaceError> { - for ancestor in path.as_ref().ancestors() { - match find_in_directory(ancestor) { - Ok(Some(options)) => { - return Ok(Some(Self { - options, - root: ancestor.to_path_buf(), - })) - } - Ok(None) => { - // Continue traversing the directory tree. - } - Err(WorkspaceError::PyprojectToml(file, err)) => { - // If we see an invalid `pyproject.toml`, warn but continue. - warn_user!("Failed to parse `{file}`: {err}"); - } - Err(err) => { - // Otherwise, warn and stop. - return Err(err); - } - } - } - Ok(None) - } - - /// Load a [`Workspace`] from a `uv.toml` file. - pub fn from_file(path: impl AsRef) -> Result { - Ok(Self { - options: read_file(path.as_ref())?, - root: path.as_ref().parent().unwrap().to_path_buf(), - }) - } -} - -/// Returns the path to the user configuration directory. -/// -/// This is similar to the `config_dir()` returned by the `dirs` crate, but it uses the -/// `XDG_CONFIG_HOME` environment variable on both Linux _and_ macOS, rather than the -/// `Application Support` directory on macOS. -fn config_dir() -> Option { - // On Windows, use, e.g., C:\Users\Alice\AppData\Roaming - #[cfg(windows)] - { - dirs_sys::known_folder_roaming_app_data() - } - - // On Linux and macOS, use, e.g., /home/alice/.config. - #[cfg(not(windows))] - { - std::env::var_os("XDG_CONFIG_HOME") - .and_then(dirs_sys::is_absolute_path) - .or_else(|| dirs_sys::home_dir().map(|path| path.join(".config"))) - } -} - -/// Read a `uv.toml` or `pyproject.toml` file in the given directory. -fn find_in_directory(dir: &Path) -> Result, WorkspaceError> { - // Read a `uv.toml` file in the current directory. - let path = dir.join("uv.toml"); - match fs_err::read_to_string(&path) { - Ok(content) => { - let options: Options = toml::from_str(&content) - .map_err(|err| WorkspaceError::UvToml(path.user_display().to_string(), err))?; - - debug!("Found workspace configuration at `{}`", path.display()); - return Ok(Some(options)); - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), - } - - // Read a `pyproject.toml` file in the current directory. - let path = dir.join("pyproject.toml"); - match fs_err::read_to_string(&path) { - Ok(content) => { - // Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section. - let pyproject: PyProjectToml = toml::from_str(&content).map_err(|err| { - WorkspaceError::PyprojectToml(path.user_display().to_string(), err) - })?; - let Some(tool) = pyproject.tool else { - debug!( - "Skipping `pyproject.toml` in `{}` (no `[tool]` section)", - dir.display() - ); - return Ok(None); - }; - let Some(options) = tool.uv else { - debug!( - "Skipping `pyproject.toml` in `{}` (no `[tool.uv]` section)", - dir.display() - ); - return Ok(None); - }; - - debug!("Found workspace configuration at `{}`", path.display()); - return Ok(Some(options)); - } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), - } - - Ok(None) -} - -/// Load [`Options`] from a `uv.toml` file. -fn read_file(path: &Path) -> Result { - let content = fs_err::read_to_string(path)?; - let options: Options = toml::from_str(&content) - .map_err(|err| WorkspaceError::UvToml(path.user_display().to_string(), err))?; - Ok(options) -} - -#[derive(thiserror::Error, Debug)] -pub enum WorkspaceError { - #[error(transparent)] - Io(#[from] std::io::Error), - - #[error("Failed to parse: `{0}`")] - PyprojectToml(String, #[source] toml::de::Error), - - #[error("Failed to parse: `{0}`")] - UvToml(String, #[source] toml::de::Error), -} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 3cc27d395..30c727c69 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -29,14 +29,14 @@ uv-distribution = { workspace = true } uv-fs = { workspace = true } uv-git = { workspace = true } uv-installer = { workspace = true } -uv-toolchain = { workspace = true } uv-normalize = { workspace = true } uv-requirements = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } +uv-settings = { workspace = true, features = ["schemars"] } +uv-toolchain = { workspace = true } uv-types = { workspace = true } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } -uv-workspace = { workspace = true, features = ["schemars"] } anstream = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index e659879fb..3e59bb708 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -13,8 +13,9 @@ use tracing::{debug, instrument}; use cli::{ToolCommand, ToolNamespace, ToolchainCommand, ToolchainNamespace}; use uv_cache::Cache; use uv_configuration::Concurrency; +use uv_distribution::Workspace; use uv_requirements::RequirementsSource; -use uv_workspace::Combine; +use uv_settings::Combine; use crate::cli::{ CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, ProjectCommand, @@ -115,24 +116,30 @@ async fn run() -> Result { uv_warnings::enable(); } - // Load the workspace settings, prioritizing (in order): + // Load configuration from the filesystem, prioritizing (in order): // 1. The configuration file specified on the command-line. - // 2. The configuration file in the current directory. - // 3. The user configuration file. - let workspace = if let Some(config_file) = cli.config_file.as_ref() { - Some(uv_workspace::Workspace::from_file(config_file)?) + // 2. The configuration file in the current workspace (i.e., the `pyproject.toml` or `uv.toml` + // file in the workspace root directory). If found, this file is combined with the user + // configuration file. + // 3. The nearest `uv.toml` file in the directory tree, starting from the current directory. If + // found, this file is combined with the user configuration file. In this case, we don't + // search for `pyproject.toml` files, since we're not in a workspace. + let filesystem = if let Some(config_file) = cli.config_file.as_ref() { + Some(uv_settings::FilesystemOptions::from_file(config_file)?) } else if cli.global_args.isolated { None + } else if let Ok(project) = Workspace::discover(&env::current_dir()?, None).await { + let project = uv_settings::FilesystemOptions::from_directory(project.root())?; + let user = uv_settings::FilesystemOptions::user()?; + project.combine(user) } else { - // TODO(charlie): This needs to discover settings from the workspace _root_. Right now, it - // discovers the closest `pyproject.toml`, which could be a workspace _member_. - let project = uv_workspace::Workspace::find(env::current_dir()?)?; - let user = uv_workspace::Workspace::user()?; + let project = uv_settings::FilesystemOptions::find(env::current_dir()?)?; + let user = uv_settings::FilesystemOptions::user()?; project.combine(user) }; // Resolve the global settings. - let globals = GlobalSettings::resolve(cli.global_args, workspace.as_ref()); + let globals = GlobalSettings::resolve(cli.global_args, filesystem.as_ref()); // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] @@ -180,7 +187,7 @@ async fn run() -> Result { debug!("uv {}", version::version()); // Resolve the cache settings. - let cache = CacheSettings::resolve(cli.cache_args, workspace.as_ref()); + let cache = CacheSettings::resolve(cli.cache_args, filesystem.as_ref()); let cache = Cache::from_settings(cache.no_cache, cache.cache_dir)?; match cli.command { @@ -190,7 +197,7 @@ async fn run() -> Result { args.compat_args.validate()?; // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipCompileSettings::resolve(args, workspace); + let args = PipCompileSettings::resolve(args, filesystem); rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -267,7 +274,7 @@ async fn run() -> Result { args.compat_args.validate()?; // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipSyncSettings::resolve(args, workspace); + let args = PipSyncSettings::resolve(args, filesystem); rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -327,7 +334,7 @@ async fn run() -> Result { args.compat_args.validate()?; // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipInstallSettings::resolve(args, workspace); + let args = PipInstallSettings::resolve(args, filesystem); rayon::ThreadPoolBuilder::new() .num_threads(args.settings.concurrency.installs) .build_global() @@ -402,7 +409,7 @@ async fn run() -> Result { command: PipCommand::Uninstall(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipUninstallSettings::resolve(args, workspace); + let args = PipUninstallSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -437,7 +444,7 @@ async fn run() -> Result { command: PipCommand::Freeze(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipFreezeSettings::resolve(args, workspace); + let args = PipFreezeSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -458,7 +465,7 @@ async fn run() -> Result { args.compat_args.validate()?; // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipListSettings::resolve(args, workspace); + let args = PipListSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -480,7 +487,7 @@ async fn run() -> Result { command: PipCommand::Show(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipShowSettings::resolve(args, workspace); + let args = PipShowSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -499,7 +506,7 @@ async fn run() -> Result { command: PipCommand::Check(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = PipCheckSettings::resolve(args, workspace); + let args = PipCheckSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -529,7 +536,7 @@ async fn run() -> Result { args.compat_args.validate()?; // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::VenvSettings::resolve(args, workspace); + let args = settings::VenvSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -565,7 +572,7 @@ async fn run() -> Result { } Commands::Project(ProjectCommand::Run(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::RunSettings::resolve(args, workspace); + let args = settings::RunSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -612,7 +619,7 @@ async fn run() -> Result { } Commands::Project(ProjectCommand::Sync(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::SyncSettings::resolve(args, workspace); + let args = settings::SyncSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -633,7 +640,7 @@ async fn run() -> Result { } Commands::Project(ProjectCommand::Lock(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::LockSettings::resolve(args, workspace); + let args = settings::LockSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?.with_refresh(args.refresh); @@ -653,7 +660,7 @@ async fn run() -> Result { } Commands::Project(ProjectCommand::Add(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::AddSettings::resolve(args, workspace); + let args = settings::AddSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -672,7 +679,7 @@ async fn run() -> Result { } Commands::Project(ProjectCommand::Remove(args)) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::RemoveSettings::resolve(args, workspace); + let args = settings::RemoveSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -705,7 +712,7 @@ async fn run() -> Result { command: ToolCommand::Run(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::ToolRunSettings::resolve(args, workspace); + let args = settings::ToolRunSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -731,7 +738,7 @@ async fn run() -> Result { command: ToolchainCommand::List(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::ToolchainListSettings::resolve(args, workspace); + let args = settings::ToolchainListSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; @@ -750,7 +757,7 @@ async fn run() -> Result { command: ToolchainCommand::Install(args), }) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = settings::ToolchainInstallSettings::resolve(args, workspace); + let args = settings::ToolchainInstallSettings::resolve(args, filesystem); // Initialize the cache. let cache = cache.init()?; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 1bddca8e7..4dfeef838 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -17,10 +17,11 @@ use uv_configuration::{ }; use uv_normalize::PackageName; use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode}; -use uv_toolchain::{Prefix, PythonVersion, Target}; -use uv_workspace::{ - Combine, InstallerOptions, PipOptions, ResolverInstallerOptions, ResolverOptions, Workspace, +use uv_settings::{ + Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions, + ResolverOptions, }; +use uv_toolchain::{Prefix, PythonVersion, Target}; use crate::cli::{ AddArgs, ColorChoice, GlobalArgs, IndexArgs, InstallerArgs, LockArgs, Maybe, PipCheckArgs, @@ -44,8 +45,8 @@ pub(crate) struct GlobalSettings { } impl GlobalSettings { - /// Resolve the [`GlobalSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: GlobalArgs, workspace: Option<&Workspace>) -> Self { + /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { Self { quiet: args.quiet, verbose: args.verbose, @@ -67,10 +68,10 @@ impl GlobalSettings { args.color }, native_tls: flag(args.native_tls, args.no_native_tls) - .combine(workspace.and_then(|workspace| workspace.options.globals.native_tls)) + .combine(workspace.and_then(|workspace| workspace.globals.native_tls)) .unwrap_or(false), connectivity: if flag(args.offline, args.no_offline) - .combine(workspace.and_then(|workspace| workspace.options.globals.offline)) + .combine(workspace.and_then(|workspace| workspace.globals.offline)) .unwrap_or(false) { Connectivity::Offline @@ -80,7 +81,7 @@ impl GlobalSettings { isolated: args.isolated, preview: PreviewMode::from( flag(args.preview, args.no_preview) - .combine(workspace.and_then(|workspace| workspace.options.globals.preview)) + .combine(workspace.and_then(|workspace| workspace.globals.preview)) .unwrap_or(false), ), } @@ -96,16 +97,16 @@ pub(crate) struct CacheSettings { } impl CacheSettings { - /// Resolve the [`CacheSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: CacheArgs, workspace: Option<&Workspace>) -> Self { + /// Resolve the [`CacheSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: CacheArgs, workspace: Option<&FilesystemOptions>) -> Self { Self { no_cache: args.no_cache || workspace - .and_then(|workspace| workspace.options.globals.no_cache) + .and_then(|workspace| workspace.globals.no_cache) .unwrap_or(false), - cache_dir: args.cache_dir.or_else(|| { - workspace.and_then(|workspace| workspace.options.globals.cache_dir.clone()) - }), + cache_dir: args + .cache_dir + .or_else(|| workspace.and_then(|workspace| workspace.globals.cache_dir.clone())), } } } @@ -127,9 +128,9 @@ pub(crate) struct RunSettings { } impl RunSettings { - /// Resolve the [`RunSettings`] from the CLI and workspace configuration. + /// Resolve the [`RunSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: RunArgs, workspace: Option) -> Self { + pub(crate) fn resolve(args: RunArgs, filesystem: Option) -> Self { let RunArgs { extra, all_extras, @@ -165,7 +166,7 @@ impl RunSettings { package, settings: ResolverInstallerSettings::combine( ResolverInstallerOptions::from(installer), - workspace, + filesystem, ), } } @@ -184,9 +185,9 @@ pub(crate) struct ToolRunSettings { } impl ToolRunSettings { - /// Resolve the [`ToolRunSettings`] from the CLI and workspace configuration. + /// Resolve the [`ToolRunSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolRunArgs, workspace: Option) -> Self { + pub(crate) fn resolve(args: ToolRunArgs, filesystem: Option) -> Self { let ToolRunArgs { target, args, @@ -204,7 +205,7 @@ impl ToolRunSettings { python, settings: ResolverInstallerSettings::combine( ResolverInstallerOptions::from(installer), - workspace, + filesystem, ), } } @@ -227,9 +228,9 @@ pub(crate) struct ToolchainListSettings { } impl ToolchainListSettings { - /// Resolve the [`ToolchainListSettings`] from the CLI and workspace configuration. + /// Resolve the [`ToolchainListSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolchainListArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: ToolchainListArgs, _filesystem: Option) -> Self { let ToolchainListArgs { all_versions, all_platforms, @@ -259,9 +260,12 @@ pub(crate) struct ToolchainInstallSettings { } impl ToolchainInstallSettings { - /// Resolve the [`ToolchainInstallSettings`] from the CLI and workspace configuration. + /// Resolve the [`ToolchainInstallSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolchainInstallArgs, _workspace: Option) -> Self { + pub(crate) fn resolve( + args: ToolchainInstallArgs, + _filesystem: Option, + ) -> Self { let ToolchainInstallArgs { target, force } = args; Self { target, force } @@ -280,9 +284,9 @@ pub(crate) struct SyncSettings { } impl SyncSettings { - /// Resolve the [`SyncSettings`] from the CLI and workspace configuration. + /// Resolve the [`SyncSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: SyncArgs, workspace: Option) -> Self { + pub(crate) fn resolve(args: SyncArgs, filesystem: Option) -> Self { let SyncArgs { extra, all_extras, @@ -304,7 +308,7 @@ impl SyncSettings { ), dev: flag(dev, no_dev).unwrap_or(true), python, - settings: InstallerSettings::combine(InstallerOptions::from(installer), workspace), + settings: InstallerSettings::combine(InstallerOptions::from(installer), filesystem), } } } @@ -320,9 +324,9 @@ pub(crate) struct LockSettings { } impl LockSettings { - /// Resolve the [`LockSettings`] from the CLI and workspace configuration. + /// Resolve the [`LockSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: LockArgs, workspace: Option) -> Self { + pub(crate) fn resolve(args: LockArgs, filesystem: Option) -> Self { let LockArgs { refresh, no_refresh, @@ -338,7 +342,7 @@ impl LockSettings { refresh: Refresh::from_args(flag(refresh, no_refresh), refresh_package), upgrade: Upgrade::from_args(flag(upgrade, no_upgrade), upgrade_package), python, - settings: ResolverSettings::combine(ResolverOptions::from(resolver), workspace), + settings: ResolverSettings::combine(ResolverOptions::from(resolver), filesystem), } } } @@ -352,9 +356,9 @@ pub(crate) struct AddSettings { } impl AddSettings { - /// Resolve the [`AddSettings`] from the CLI and workspace configuration. + /// Resolve the [`AddSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: AddArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: AddArgs, _filesystem: Option) -> Self { let AddArgs { requirements, python, @@ -376,9 +380,9 @@ pub(crate) struct RemoveSettings { } impl RemoveSettings { - /// Resolve the [`RemoveSettings`] from the CLI and workspace configuration. + /// Resolve the [`RemoveSettings`] from the CLI and filesystem configuration. #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: RemoveArgs, _workspace: Option) -> Self { + pub(crate) fn resolve(args: RemoveArgs, _filesystem: Option) -> Self { let RemoveArgs { requirements, python, @@ -405,8 +409,8 @@ pub(crate) struct PipCompileSettings { } impl PipCompileSettings { - /// Resolve the [`PipCompileSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipCompileArgs, workspace: Option) -> Self { + /// Resolve the [`PipCompileSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipCompileArgs, filesystem: Option) -> Self { let PipCompileArgs { src_file, constraint, @@ -459,9 +463,8 @@ impl PipCompileSettings { compat_args: _, } = args; - let overrides_from_workspace = if let Some(workspace) = &workspace { - workspace - .options + let overrides_from_workspace = if let Some(configuration) = &filesystem { + configuration .override_dependencies .clone() .unwrap_or_default() @@ -515,7 +518,7 @@ impl PipCompileSettings { concurrent_installs: env(env::CONCURRENT_INSTALLS), ..PipOptions::from(resolver) }, - workspace, + filesystem, ), } } @@ -534,8 +537,8 @@ pub(crate) struct PipSyncSettings { } impl PipSyncSettings { - /// Resolve the [`PipSyncSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipSyncArgs, workspace: Option) -> Self { + /// Resolve the [`PipSyncSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipSyncArgs, filesystem: Option) -> Self { let PipSyncArgs { src_file, constraint, @@ -603,7 +606,7 @@ impl PipSyncSettings { concurrent_installs: env(env::CONCURRENT_INSTALLS), ..PipOptions::from(installer) }, - workspace, + filesystem, ), } } @@ -627,8 +630,8 @@ pub(crate) struct PipInstallSettings { } impl PipInstallSettings { - /// Resolve the [`PipInstallSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipInstallArgs, workspace: Option) -> Self { + /// Resolve the [`PipInstallSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipInstallArgs, filesystem: Option) -> Self { let PipInstallArgs { package, requirement, @@ -675,9 +678,8 @@ impl PipInstallSettings { compat_args: _, } = args; - let overrides_from_workspace = if let Some(workspace) = &workspace { - workspace - .options + let overrides_from_workspace = if let Some(configuration) = &filesystem { + configuration .override_dependencies .clone() .unwrap_or_default() @@ -728,7 +730,7 @@ impl PipInstallSettings { concurrent_installs: env(env::CONCURRENT_INSTALLS), ..PipOptions::from(installer) }, - workspace, + filesystem, ), } } @@ -744,8 +746,8 @@ pub(crate) struct PipUninstallSettings { } impl PipUninstallSettings { - /// Resolve the [`PipUninstallSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipUninstallArgs, workspace: Option) -> Self { + /// Resolve the [`PipUninstallSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipUninstallArgs, filesystem: Option) -> Self { let PipUninstallArgs { package, requirement, @@ -772,7 +774,7 @@ impl PipUninstallSettings { keyring_provider, ..PipOptions::default() }, - workspace, + filesystem, ), } } @@ -787,8 +789,8 @@ pub(crate) struct PipFreezeSettings { } impl PipFreezeSettings { - /// Resolve the [`PipFreezeSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipFreezeArgs, workspace: Option) -> Self { + /// Resolve the [`PipFreezeSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipFreezeArgs, filesystem: Option) -> Self { let PipFreezeArgs { exclude_editable, strict, @@ -807,7 +809,7 @@ impl PipFreezeSettings { strict: flag(strict, no_strict), ..PipOptions::default() }, - workspace, + filesystem, ), } } @@ -825,8 +827,8 @@ pub(crate) struct PipListSettings { } impl PipListSettings { - /// Resolve the [`PipListSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipListArgs, workspace: Option) -> Self { + /// Resolve the [`PipListSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipListArgs, filesystem: Option) -> Self { let PipListArgs { editable, exclude_editable, @@ -852,7 +854,7 @@ impl PipListSettings { strict: flag(strict, no_strict), ..PipOptions::default() }, - workspace, + filesystem, ), } } @@ -867,8 +869,8 @@ pub(crate) struct PipShowSettings { } impl PipShowSettings { - /// Resolve the [`PipShowSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipShowArgs, workspace: Option) -> Self { + /// Resolve the [`PipShowSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipShowArgs, filesystem: Option) -> Self { let PipShowArgs { package, strict, @@ -887,7 +889,7 @@ impl PipShowSettings { strict: flag(strict, no_strict), ..PipOptions::default() }, - workspace, + filesystem, ), } } @@ -901,8 +903,8 @@ pub(crate) struct PipCheckSettings { } impl PipCheckSettings { - /// Resolve the [`PipCheckSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: PipCheckArgs, workspace: Option) -> Self { + /// Resolve the [`PipCheckSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: PipCheckArgs, filesystem: Option) -> Self { let PipCheckArgs { python, system, @@ -916,7 +918,7 @@ impl PipCheckSettings { system: flag(system, no_system), ..PipOptions::default() }, - workspace, + filesystem, ), } } @@ -935,8 +937,8 @@ pub(crate) struct VenvSettings { } impl VenvSettings { - /// Resolve the [`VenvSettings`] from the CLI and workspace configuration. - pub(crate) fn resolve(args: VenvArgs, workspace: Option) -> Self { + /// Resolve the [`VenvSettings`] from the CLI and filesystem configuration. + pub(crate) fn resolve(args: VenvArgs, filesystem: Option) -> Self { let VenvArgs { python, system, @@ -970,7 +972,7 @@ impl VenvSettings { link_mode, ..PipOptions::from(index_args) }, - workspace, + filesystem, ), } } @@ -992,8 +994,8 @@ pub(crate) struct InstallerSettings { } impl InstallerSettings { - /// Resolve the [`InstallerSettings`] from the CLI and workspace configuration. - pub(crate) fn combine(args: InstallerOptions, workspace: Option) -> Self { + /// Resolve the [`InstallerSettings`] from the CLI and filesystem configuration. + pub(crate) fn combine(args: InstallerOptions, filesystem: Option) -> Self { let ResolverInstallerOptions { index_url, extra_index_url, @@ -1007,8 +1009,9 @@ impl InstallerSettings { exclude_newer: _, link_mode, compile_bytecode, - } = workspace - .map(|workspace| workspace.options.top_level) + } = filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) .unwrap_or_default(); Self { @@ -1059,8 +1062,8 @@ pub(crate) struct ResolverSettings { } impl ResolverSettings { - /// Resolve the [`ResolverSettings`] from the CLI and workspace configuration. - pub(crate) fn combine(args: ResolverOptions, workspace: Option) -> Self { + /// Resolve the [`ResolverSettings`] from the CLI and filesystem configuration. + pub(crate) fn combine(args: ResolverOptions, filesystem: Option) -> Self { let ResolverInstallerOptions { index_url, extra_index_url, @@ -1074,8 +1077,9 @@ impl ResolverSettings { exclude_newer, link_mode, compile_bytecode: _, - } = workspace - .map(|workspace| workspace.options.top_level) + } = filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) .unwrap_or_default(); Self { @@ -1127,8 +1131,11 @@ pub(crate) struct ResolverInstallerSettings { } impl ResolverInstallerSettings { - /// Resolve the [`ResolverInstallerSettings`] from the CLI and workspace configuration. - pub(crate) fn combine(args: ResolverInstallerOptions, workspace: Option) -> Self { + /// Resolve the [`ResolverInstallerSettings`] from the CLI and filesystem configuration. + pub(crate) fn combine( + args: ResolverInstallerOptions, + filesystem: Option, + ) -> Self { let ResolverInstallerOptions { index_url, extra_index_url, @@ -1142,8 +1149,9 @@ impl ResolverInstallerSettings { exclude_newer, link_mode, compile_bytecode, - } = workspace - .map(|workspace| workspace.options.top_level) + } = filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) .unwrap_or_default(); Self { @@ -1226,8 +1234,8 @@ pub(crate) struct PipSettings { } impl PipSettings { - /// Resolve the [`PipSettings`] from the CLI and workspace configuration. - pub(crate) fn combine(args: PipOptions, workspace: Option) -> Self { + /// Resolve the [`PipSettings`] from the CLI and filesystem configuration. + pub(crate) fn combine(args: PipOptions, filesystem: Option) -> Self { let PipOptions { python, system, @@ -1273,8 +1281,9 @@ impl PipSettings { concurrent_builds, concurrent_downloads, concurrent_installs, - } = workspace - .map(|workspace| workspace.options.pip()) + } = filesystem + .map(FilesystemOptions::into_options) + .map(Options::pip) .unwrap_or_default(); Self { diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index bf2eccce4..f548378c2 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3200,30 +3200,13 @@ fn override_dependency_from_workspace_invalid_syntax() -> Result<()> { .arg("pyproject.toml") .current_dir(&context.temp_dir) , @r###" - success: true - exit_code: 0 + success: false + exit_code: 2 ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml - blinker==1.7.0 - # via flask - click==8.1.7 - # via flask - flask==3.0.0 - # via example (pyproject.toml) - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask ----- stderr ----- - warning: Failed to parse `pyproject.toml`: TOML parse error at line 9, column 29 + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 9, column 29 | 9 | override-dependencies = [ | ^ @@ -3231,7 +3214,6 @@ fn override_dependency_from_workspace_invalid_syntax() -> Result<()> { werkzeug=2.3.0 ^^^^^^ - Resolved 7 packages in [TIME] "### ); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 056110a90..fdd52623b 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -143,12 +143,6 @@ fn invalid_pyproject_toml_syntax() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: Failed to parse `pyproject.toml`: TOML parse error at line 1, column 5 - | - 1 | 123 - 456 - | ^ - expected `.`, `=` - error: Failed to parse: `pyproject.toml` Caused by: TOML parse error at line 1, column 5 |