mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 13:14:41 +00:00
Add uv-workspace crate with settings discovery and deserialization (#3007)
## Summary This PR adds basic struct definitions along with a "workspace" concept for discovering settings. (The "workspace" terminology is used to match Ruff; I did not invent it.) A few notes: - We discover any `pyproject.toml` or `uv.toml` file in any parent directory of the current working directory. (We could adjust this to look at the directories of the input files.) - We don't actually do anything with the configuration yet; but those PRs are large and I want this to be reviewed in isolation.
This commit is contained in:
parent
c0efeeddf6
commit
295b58ad37
21 changed files with 433 additions and 20 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
|
@ -1052,6 +1052,12 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
|
@ -3321,6 +3327,30 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.8.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"schemars_derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars_derive"
|
||||||
|
version = "0.8.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
@ -3382,6 +3412,17 @@ dependencies = [
|
||||||
"syn 2.0.58",
|
"syn 2.0.58",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive_internals"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.115"
|
version = "1.0.115"
|
||||||
|
|
@ -4377,6 +4418,7 @@ dependencies = [
|
||||||
"uv-types",
|
"uv-types",
|
||||||
"uv-virtualenv",
|
"uv-virtualenv",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
|
"uv-workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4391,6 +4433,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"rust-netrc",
|
"rust-netrc",
|
||||||
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"test-log",
|
"test-log",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
@ -4833,6 +4876,7 @@ dependencies = [
|
||||||
"requirements-txt",
|
"requirements-txt",
|
||||||
"rkyv",
|
"rkyv",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -4861,6 +4905,7 @@ dependencies = [
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -4929,6 +4974,28 @@ dependencies = [
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uv-workspace"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"distribution-types",
|
||||||
|
"fs-err",
|
||||||
|
"install-wheel-rs",
|
||||||
|
"pep508_rs",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
"uv-auth",
|
||||||
|
"uv-configuration",
|
||||||
|
"uv-fs",
|
||||||
|
"uv-normalize",
|
||||||
|
"uv-resolver",
|
||||||
|
"uv-toolchain",
|
||||||
|
"uv-warnings",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ uv-auth = { path = "crates/uv-auth" }
|
||||||
uv-build = { path = "crates/uv-build" }
|
uv-build = { path = "crates/uv-build" }
|
||||||
uv-cache = { path = "crates/uv-cache" }
|
uv-cache = { path = "crates/uv-cache" }
|
||||||
uv-client = { path = "crates/uv-client" }
|
uv-client = { path = "crates/uv-client" }
|
||||||
|
uv-configuration = { path = "crates/uv-configuration" }
|
||||||
uv-dev = { path = "crates/uv-dev" }
|
uv-dev = { path = "crates/uv-dev" }
|
||||||
uv-dispatch = { path = "crates/uv-dispatch" }
|
uv-dispatch = { path = "crates/uv-dispatch" }
|
||||||
uv-distribution = { path = "crates/uv-distribution" }
|
uv-distribution = { path = "crates/uv-distribution" }
|
||||||
|
|
@ -43,13 +44,13 @@ uv-interpreter = { path = "crates/uv-interpreter" }
|
||||||
uv-normalize = { path = "crates/uv-normalize" }
|
uv-normalize = { path = "crates/uv-normalize" }
|
||||||
uv-requirements = { path = "crates/uv-requirements" }
|
uv-requirements = { path = "crates/uv-requirements" }
|
||||||
uv-resolver = { path = "crates/uv-resolver" }
|
uv-resolver = { path = "crates/uv-resolver" }
|
||||||
uv-types = { path = "crates/uv-types" }
|
uv-toolchain = { path = "crates/uv-toolchain" }
|
||||||
uv-configuration = { path = "crates/uv-configuration" }
|
|
||||||
uv-trampoline = { path = "crates/uv-trampoline" }
|
uv-trampoline = { path = "crates/uv-trampoline" }
|
||||||
|
uv-types = { path = "crates/uv-types" }
|
||||||
uv-version = { path = "crates/uv-version" }
|
uv-version = { path = "crates/uv-version" }
|
||||||
uv-virtualenv = { path = "crates/uv-virtualenv" }
|
uv-virtualenv = { path = "crates/uv-virtualenv" }
|
||||||
uv-warnings = { path = "crates/uv-warnings" }
|
uv-warnings = { path = "crates/uv-warnings" }
|
||||||
uv-toolchain = { path = "crates/uv-toolchain" }
|
uv-workspace = { path = "crates/uv-workspace" }
|
||||||
|
|
||||||
anstream = { version = "0.6.13" }
|
anstream = { version = "0.6.13" }
|
||||||
anyhow = { version = "1.0.80" }
|
anyhow = { version = "1.0.80" }
|
||||||
|
|
@ -118,6 +119,7 @@ rmp-serde = { version = "1.1.2" }
|
||||||
rust-netrc = { version = "0.1.1" }
|
rust-netrc = { version = "0.1.1" }
|
||||||
rustc-hash = { version = "1.1.0" }
|
rustc-hash = { version = "1.1.0" }
|
||||||
same-file = { version = "1.0.6" }
|
same-file = { version = "1.0.6" }
|
||||||
|
schemars = { version = "0.8.16" }
|
||||||
seahash = { version = "4.1.0" }
|
seahash = { version = "4.1.0" }
|
||||||
serde = { version = "1.0.197" }
|
serde = { version = "1.0.197" }
|
||||||
serde_json = { version = "1.0.114" }
|
serde_json = { version = "1.0.114" }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use std::time::SystemTime;
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
use fs_err::{DirEntry, File};
|
use fs_err::{DirEntry, File};
|
||||||
use reflink_copy as reflink;
|
use reflink_copy as reflink;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tempfile::tempdir_in;
|
use tempfile::tempdir_in;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
|
@ -201,7 +202,7 @@ fn parse_scripts(
|
||||||
scripts_from_ini(extras, python_minor, ini)
|
scripts_from_ini(extras, python_minor, ini)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
pub enum LinkMode {
|
pub enum LinkMode {
|
||||||
/// Clone (i.e., copy-on-write) packages from the wheel into the site packages.
|
/// Clone (i.e., copy-on-write) packages from the wheel into the site packages.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ once_cell = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
reqwest-middleware = { workspace = true }
|
reqwest-middleware = { workspace = true }
|
||||||
rust-netrc = { workspace = true }
|
rust-netrc = { workspace = true }
|
||||||
|
serde = { workspace = true, optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use uv_auth::{self, KeyringProvider};
|
||||||
/// Keyring provider type to use for credential lookup.
|
/// Keyring provider type to use for credential lookup.
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||||
pub enum KeyringProviderType {
|
pub enum KeyringProviderType {
|
||||||
/// Do not use keyring for credential lookup.
|
/// Do not use keyring for credential lookup.
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ impl NoBuild {
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
|
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||||
pub enum IndexStrategy {
|
pub enum IndexStrategy {
|
||||||
/// Only use results from the first index that returns a match for a given package name.
|
/// Only use results from the first index that returns a match for a given package name.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use serde::ser::SerializeMap;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{btree_map::Entry, BTreeMap},
|
collections::{btree_map::Entry, BTreeMap},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
|
@ -35,12 +36,53 @@ enum ConfigSettingValue {
|
||||||
List(Vec<String>),
|
List(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl serde::Serialize for ConfigSettingValue {
|
||||||
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
match self {
|
||||||
|
ConfigSettingValue::String(value) => serializer.serialize_str(value),
|
||||||
|
ConfigSettingValue::List(values) => serializer.collect_seq(values.iter()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct Visitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = ConfigSettingValue;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a string or list of strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||||
|
Ok(ConfigSettingValue::String(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A: serde::de::SeqAccess<'de>>(
|
||||||
|
self,
|
||||||
|
mut seq: A,
|
||||||
|
) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
while let Some(value) = seq.next_element()? {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
Ok(ConfigSettingValue::List(values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
|
/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
|
||||||
/// list of strings.
|
/// list of strings.
|
||||||
///
|
///
|
||||||
/// See: <https://peps.python.org/pep-0517/#config-settings>
|
/// See: <https://peps.python.org/pep-0517/#config-settings>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
#[cfg_attr(not(feature = "serde"), allow(dead_code))]
|
|
||||||
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
|
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
|
||||||
|
|
||||||
impl FromIterator<ConfigSettingEntry> for ConfigSettings {
|
impl FromIterator<ConfigSettingEntry> for ConfigSettings {
|
||||||
|
|
@ -77,23 +119,42 @@ impl ConfigSettings {
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
impl serde::Serialize for ConfigSettings {
|
impl serde::Serialize for ConfigSettings {
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
use serde::ser::SerializeMap;
|
|
||||||
|
|
||||||
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||||
for (key, value) in &self.0 {
|
for (key, value) in &self.0 {
|
||||||
match value {
|
map.serialize_entry(key, value)?;
|
||||||
ConfigSettingValue::String(value) => {
|
|
||||||
map.serialize_entry(&key, &value)?;
|
|
||||||
}
|
|
||||||
ConfigSettingValue::List(values) => {
|
|
||||||
map.serialize_entry(&key, &values)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
map.end()
|
map.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for ConfigSettings {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct Visitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = ConfigSettings;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a map from string to string or list of strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<A: serde::de::MapAccess<'de>>(
|
||||||
|
self,
|
||||||
|
mut map: A,
|
||||||
|
) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut config = BTreeMap::default();
|
||||||
|
while let Some((key, value)) = map.next_entry()? {
|
||||||
|
config.insert(key, value);
|
||||||
|
}
|
||||||
|
Ok(ConfigSettings(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_map(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,44 @@ impl FromStr for PackageNameSpecifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for PackageNameSpecifier {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct Visitor;
|
||||||
|
|
||||||
|
impl<'de> serde::de::Visitor<'de> for Visitor {
|
||||||
|
type Value = PackageNameSpecifier;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a package name or `:all:` or `:none:`")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
// Accept the special values `:all:` and `:none:`.
|
||||||
|
match value {
|
||||||
|
":all:" => Ok(PackageNameSpecifier::All),
|
||||||
|
":none:" => Ok(PackageNameSpecifier::None),
|
||||||
|
_ => {
|
||||||
|
// Otherwise, parse the value as a package name.
|
||||||
|
match PackageName::from_str(value) {
|
||||||
|
Ok(name) => Ok(PackageNameSpecifier::Package(name)),
|
||||||
|
Err(err) => Err(E::custom(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Package name specification.
|
/// Package name specification.
|
||||||
///
|
///
|
||||||
/// Consumes both package names and selection directives for compatibility with pip flags
|
/// Consumes both package names and selection directives for compatibility with pip flags
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ petgraph = { workspace = true }
|
||||||
pubgrub = { workspace = true }
|
pubgrub = { workspace = true }
|
||||||
rkyv = { workspace = true }
|
rkyv = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
serde = { workspace = true, optional = true }
|
||||||
textwrap = { workspace = true }
|
textwrap = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["macros"] }
|
tokio = { workspace = true, features = ["macros"] }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use chrono::{DateTime, Days, NaiveDate, NaiveTime, Utc};
|
||||||
|
|
||||||
/// A timestamp that excludes files newer than it.
|
/// A timestamp that excludes files newer than it.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct ExcludeNewer(DateTime<Utc>);
|
pub struct ExcludeNewer(DateTime<Utc>);
|
||||||
|
|
||||||
impl ExcludeNewer {
|
impl ExcludeNewer {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::Manifest;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||||
pub enum PreReleaseMode {
|
pub enum PreReleaseMode {
|
||||||
/// Disallow all pre-release versions.
|
/// Disallow all pre-release versions.
|
||||||
Disallow,
|
Disallow,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ use crate::{Manifest, ResolveError};
|
||||||
/// package.
|
/// package.
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||||
pub enum AnnotationStyle {
|
pub enum AnnotationStyle {
|
||||||
/// Render the annotations on a single, comma-separated line.
|
/// Render the annotations on a single, comma-separated line.
|
||||||
Line,
|
Line,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use crate::Manifest;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
|
||||||
pub enum ResolutionMode {
|
pub enum ResolutionMode {
|
||||||
/// Resolve the highest compatible version of each package.
|
/// Resolve the highest compatible version of each package.
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
pep440_rs = { workspace = true }
|
||||||
|
pep508_rs = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
uv-extract = { workspace = true }
|
uv-extract = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
|
||||||
pep508_rs = { workspace = true }
|
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
|
|
@ -22,6 +22,7 @@ futures = { workspace = true }
|
||||||
once_cell = {workspace = true}
|
once_cell = {workspace = true}
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
reqwest-middleware = { workspace = true }
|
reqwest-middleware = { workspace = true }
|
||||||
|
serde = { workspace = true, optional = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ impl FromStr for PythonVersion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
PythonVersion::from_str(&s).map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for PythonVersion {
|
impl Display for PythonVersion {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
Display::fmt(&self.0, f)
|
Display::fmt(&self.0, f)
|
||||||
|
|
|
||||||
36
crates/uv-workspace/Cargo.toml
Normal file
36
crates/uv-workspace/Cargo.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
[package]
|
||||||
|
name = "uv-workspace"
|
||||||
|
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 }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
distribution-types = { workspace = true }
|
||||||
|
install-wheel-rs = { workspace = true }
|
||||||
|
pep508_rs = { workspace = true }
|
||||||
|
uv-auth = { workspace = true, features = ["serde"] }
|
||||||
|
uv-configuration = { workspace = true, features = ["serde"] }
|
||||||
|
uv-fs = { workspace = true }
|
||||||
|
uv-normalize = { workspace = true }
|
||||||
|
uv-resolver = { workspace = true, features = ["serde"] }
|
||||||
|
uv-toolchain = { workspace = true, features = ["serde"] }
|
||||||
|
uv-warnings = { workspace = true }
|
||||||
|
|
||||||
|
fs-err = { workspace = true }
|
||||||
|
schemars = { workspace = true, optional = true }
|
||||||
|
serde = { workspace = true, optional = true }
|
||||||
|
serde_json = { workspace = true, optional = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
serde = ["dep:serde", "dep:serde_json"]
|
||||||
5
crates/uv-workspace/src/lib.rs
Normal file
5
crates/uv-workspace/src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub use crate::settings::*;
|
||||||
|
pub use crate::workspace::*;
|
||||||
|
|
||||||
|
mod settings;
|
||||||
|
mod workspace;
|
||||||
88
crates/uv-workspace/src/settings.rs
Normal file
88
crates/uv-workspace/src/settings.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use distribution_types::{FlatIndexLocation, IndexUrl};
|
||||||
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier};
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
||||||
|
use uv_toolchain::PythonVersion;
|
||||||
|
|
||||||
|
/// A `pyproject.toml` with an (optional) `[tool.uv]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
pub(crate) struct PyProjectToml {
|
||||||
|
pub(crate) tool: Option<Tools>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `[tool]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
pub(crate) struct Tools {
|
||||||
|
pub(crate) uv: Option<Options>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `[tool.uv]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
pub struct Options {
|
||||||
|
pub quiet: Option<bool>,
|
||||||
|
pub verbose: Option<bool>,
|
||||||
|
pub native_tls: Option<bool>,
|
||||||
|
pub no_cache: bool,
|
||||||
|
pub cache_dir: Option<PathBuf>,
|
||||||
|
pub pip: Option<PipOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `[tool.uv.pip]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
pub struct PipOptions {
|
||||||
|
pub system: Option<bool>,
|
||||||
|
pub offline: Option<bool>,
|
||||||
|
pub index_url: Option<IndexUrl>,
|
||||||
|
pub extra_index_url: Option<IndexUrl>,
|
||||||
|
pub no_index: Option<bool>,
|
||||||
|
pub find_links: Option<Vec<FlatIndexLocation>>,
|
||||||
|
pub index_strategy: Option<IndexStrategy>,
|
||||||
|
pub keyring_provider: Option<KeyringProviderType>,
|
||||||
|
pub no_build: Option<bool>,
|
||||||
|
pub no_binary: Option<Vec<PackageNameSpecifier>>,
|
||||||
|
pub only_binary: Option<Vec<PackageNameSpecifier>>,
|
||||||
|
pub no_build_isolation: Option<bool>,
|
||||||
|
pub resolver: Option<ResolverOptions>,
|
||||||
|
pub installer: Option<InstallerOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `[tool.uv.pip.resolver]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
pub struct ResolverOptions {
|
||||||
|
pub resolution: Option<ResolutionMode>,
|
||||||
|
pub prerelease: Option<PreReleaseMode>,
|
||||||
|
pub no_strip_extras: Option<bool>,
|
||||||
|
pub no_annotate: Option<bool>,
|
||||||
|
pub no_header: Option<bool>,
|
||||||
|
pub generate_hashes: Option<bool>,
|
||||||
|
pub legacy_setup_py: Option<bool>,
|
||||||
|
pub config_setting: Option<ConfigSettings>,
|
||||||
|
pub python_version: Option<PythonVersion>,
|
||||||
|
pub exclude_newer: Option<ExcludeNewer>,
|
||||||
|
pub no_emit_package: Option<Vec<PackageName>>,
|
||||||
|
pub emit_index_url: Option<bool>,
|
||||||
|
pub emit_find_links: Option<bool>,
|
||||||
|
pub annotation_style: Option<AnnotationStyle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `[tool.uv.pip.installer]` section.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
pub struct InstallerOptions {
|
||||||
|
pub link_mode: Option<LinkMode>,
|
||||||
|
pub compile_bytecode: Option<bool>,
|
||||||
|
}
|
||||||
94
crates/uv-workspace/src/workspace.rs
Normal file
94
crates/uv-workspace/src/workspace.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
options: Options,
|
||||||
|
root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Workspace {
|
||||||
|
/// 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<Path>) -> Result<Option<Self>, WorkspaceError> {
|
||||||
|
for ancestor in path.as_ref().ancestors() {
|
||||||
|
match read_options(ancestor) {
|
||||||
|
Ok(Some(options)) => {
|
||||||
|
return Ok(Some(Self {
|
||||||
|
options,
|
||||||
|
root: ancestor.to_path_buf(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Continue traversing the directory tree.
|
||||||
|
}
|
||||||
|
Err(err @ WorkspaceError::PyprojectToml(..)) => {
|
||||||
|
// If we see an invalid `pyproject.toml`, warn but continue.
|
||||||
|
warn_user!("{err}");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// Otherwise, warn and stop.
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a `uv.toml` or `pyproject.toml` file in the given directory.
|
||||||
|
fn read_options(dir: &Path) -> Result<Option<Options>, 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))?;
|
||||||
|
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 = path.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 {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let Some(options) = tool.uv else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
return Ok(Some(options));
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ requirements-txt = { workspace = true, features = ["http"] }
|
||||||
uv-auth = { workspace = true }
|
uv-auth = { workspace = true }
|
||||||
uv-cache = { workspace = true, features = ["clap"] }
|
uv-cache = { workspace = true, features = ["clap"] }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
|
uv-configuration = { workspace = true, features = ["clap"] }
|
||||||
uv-dispatch = { workspace = true }
|
uv-dispatch = { workspace = true }
|
||||||
uv-distribution = { workspace = true }
|
uv-distribution = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
|
|
@ -31,11 +32,11 @@ uv-interpreter = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
uv-requirements = { workspace = true }
|
uv-requirements = { workspace = true }
|
||||||
uv-resolver = { workspace = true, features = ["clap"] }
|
uv-resolver = { workspace = true, features = ["clap"] }
|
||||||
uv-types = { workspace = true, features = ["clap"] }
|
|
||||||
uv-configuration = { workspace = true, features = ["clap"] }
|
|
||||||
uv-virtualenv = { workspace = true }
|
|
||||||
uv-toolchain = { workspace = true }
|
uv-toolchain = { workspace = true }
|
||||||
|
uv-types = { workspace = true, features = ["clap"] }
|
||||||
|
uv-virtualenv = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
uv-workspace = { workspace = true, features = ["serde", "schemars"] }
|
||||||
|
|
||||||
anstream = { workspace = true }
|
anstream = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,9 @@ async fn run() -> Result<ExitStatus> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load the workspace settings.
|
||||||
|
let _ = uv_workspace::Workspace::find(env::current_dir()?)?;
|
||||||
|
|
||||||
let globals = cli.global_args;
|
let globals = cli.global_args;
|
||||||
|
|
||||||
// Configure the `tracing` crate, which controls internal logging.
|
// Configure the `tracing` crate, which controls internal logging.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue