Skip to content

Commit

Permalink
Load configuration options from workspace root
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jun 13, 2024
1 parent cfb022a commit 623d2da
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 257 deletions.
50 changes: 25 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
4 changes: 2 additions & 2 deletions crates/uv-dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-dev/src/generate_json_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "uv-workspace"
name = "uv-settings"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, InstallerOptions, Options, PipOptions, Workspace};
use crate::{GlobalOptions, InstallerOptions, Options, PipOptions, ResolvedOptions};

pub trait Combine {
/// Combine two values, preferring the values in `self`.
Expand All @@ -25,14 +25,11 @@ pub trait Combine {
fn combine(self, other: Self) -> Self;
}

impl Combine for Option<Workspace> {
/// Combine the options used in two [`Workspace`]s. Retains the root of `self`.
fn combine(self, other: Option<Workspace>) -> Option<Workspace> {
impl Combine for Option<ResolvedOptions> {
/// Combine the options used in two [`ResolvedOptions`]s. Retains the root of `self`.
fn combine(self, other: Option<ResolvedOptions>) -> Option<ResolvedOptions> {
match (self, other) {
(Some(mut a), Some(b)) => {
a.options = a.options.combine(b.options);
Some(a)
}
(Some(a), Some(b)) => Some(ResolvedOptions(a.into_options().combine(b.into_options()))),
(a, b) => a.or(b),
}
}
Expand Down
167 changes: 167 additions & 0 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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 ResolvedOptions(Options);

impl ResolvedOptions {
/// Convert the [`ResolvedOptions`] into [`Options`].
pub fn into_options(self) -> Options {
self.0
}
}

impl Deref for ResolvedOptions {
type Target = Options;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl ResolvedOptions {
/// Load the user [`ResolvedOptions`].
pub fn user() -> Result<Option<Self>, 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 [`ResolvedOptions`] 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<Path>) -> Result<Option<Self>, 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 [`ResolvedOptions`] from a directory, preferring a `uv.toml` file over a
/// `pyproject.toml` file.
pub fn from_directory(dir: impl AsRef<Path>) -> Result<Option<Self>, 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 [`ResolvedOptions`] from a `uv.toml` file.
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
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<PathBuf> {
// 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<Options, Error> {
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),
}
File renamed without changes.
7 changes: 0 additions & 7 deletions crates/uv-workspace/src/lib.rs

This file was deleted.

Loading

0 comments on commit 623d2da

Please sign in to comment.