Add JSON Schema support (#3046)

## Summary

This PR adds JSON Schema support. The setup mirrors Ruff's own.
This commit is contained in:
Charlie Marsh 2024-04-17 13:24:41 -04:00 committed by GitHub
parent 7c5b13c412
commit 7fb2bf816f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 818 additions and 26 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
dev = "run --package uv-dev"

2
.gitattributes vendored
View file

@ -1 +1,3 @@
* text=auto eol=lf * text=auto eol=lf
uv.schema.json linguist-generated=true text=auto eol=lf

View file

@ -8,7 +8,7 @@ We have issues labeled as [Good First Issue](https://github.com/astral-sh/uv/iss
### Linux ### Linux
On Ubuntu and other Debian-based distributions, you can install the C compiler and CMake with On Ubuntu and other Debian-based distributions, you can install the C compiler and CMake with:
```shell ```shell
sudo apt install build-essential cmake sudo apt install build-essential cmake
@ -16,7 +16,7 @@ sudo apt install build-essential cmake
### macOS ### macOS
CMake may be installed with Homebrew: You can install CMake with Homebrew:
```shell ```shell
brew install cmake brew install cmake
@ -26,13 +26,14 @@ See the [Python](#python) section for instructions on installing the Python vers
### Windows ### Windows
You can install CMake from the [installers](https://cmake.org/download/) or with `pipx install cmake` You can install CMake from the [installers](https://cmake.org/download/) or with `pipx install cmake`.
(make sure that the pipx install path is in `PATH`, pipx complains if it isn't).
## Testing ## Testing
For running tests, we recommend [nextest](https://nexte.st/). For running tests, we recommend [nextest](https://nexte.st/).
If tests fail due to a mismatch in the JSON Schema, run: `cargo dev generate-json-schema`.
### Python ### Python
Testing uv requires multiple specific Python versions. You can install them into Testing uv requires multiple specific Python versions. You can install them into
@ -87,7 +88,7 @@ python -m scripts.bench \
./scripts/requirements/jupyter.in --benchmark resolve-cold --min-runs 20 ./scripts/requirements/jupyter.in --benchmark resolve-cold --min-runs 20
``` ```
### Analysing concurrency ### Analyzing concurrency
You can use [tracing-durations-export](https://github.com/konstin/tracing-durations-export) to visualize parallel requests and find any spots where uv is CPU-bound. Example usage, with `uv` and `uv-dev` respectively: You can use [tracing-durations-export](https://github.com/konstin/tracing-durations-export) to visualize parallel requests and find any spots where uv is CPU-bound. Example usage, with `uv` and `uv-dev` respectively:
@ -104,7 +105,7 @@ RUST_LOG=uv=info TRACING_DURATIONS_FILE=target/traces/jupyter.ndjson cargo run -
You can enable `trace` level logging using the `RUST_LOG` environment variable, i.e. You can enable `trace` level logging using the `RUST_LOG` environment variable, i.e.
```shell ```shell
RUST_LOG=trace uv RUST_LOG=trace uv
``` ```
## Releases ## Releases

35
Cargo.lock generated
View file

@ -983,6 +983,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "difflib" name = "difflib"
version = "0.4.0" version = "0.4.0"
@ -1051,6 +1057,7 @@ dependencies = [
"pypi-types", "pypi-types",
"rkyv", "rkyv",
"rustc-hash", "rustc-hash",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -1802,6 +1809,7 @@ dependencies = [
"reflink-copy", "reflink-copy",
"regex", "regex",
"rustc-hash", "rustc-hash",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -2652,6 +2660,16 @@ dependencies = [
"termtree", "termtree",
] ]
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]] [[package]]
name = "priority-queue" name = "priority-queue"
version = "2.0.2" version = "2.0.2"
@ -2714,7 +2732,7 @@ dependencies = [
"indoc", "indoc",
"libc", "libc",
"memoffset 0.9.1", "memoffset 0.9.1",
"parking_lot 0.11.2", "parking_lot 0.12.1",
"portable-atomic", "portable-atomic",
"pyo3-build-config", "pyo3-build-config",
"pyo3-ffi", "pyo3-ffi",
@ -4455,6 +4473,7 @@ dependencies = [
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
"rust-netrc", "rust-netrc",
"schemars",
"serde", "serde",
"tempfile", "tempfile",
"test-log", "test-log",
@ -4577,6 +4596,7 @@ dependencies = [
"itertools 0.12.1", "itertools 0.12.1",
"pep508_rs", "pep508_rs",
"rustc-hash", "rustc-hash",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"uv-auth", "uv-auth",
@ -4605,8 +4625,10 @@ dependencies = [
"pep508_rs", "pep508_rs",
"petgraph", "petgraph",
"poloto", "poloto",
"pretty_assertions",
"resvg", "resvg",
"rustc-hash", "rustc-hash",
"schemars",
"serde", "serde",
"serde_json", "serde_json",
"tagu", "tagu",
@ -4628,6 +4650,7 @@ dependencies = [
"uv-resolver", "uv-resolver",
"uv-toolchain", "uv-toolchain",
"uv-types", "uv-types",
"uv-workspace",
"walkdir", "walkdir",
] ]
@ -4831,6 +4854,7 @@ name = "uv-normalize"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"rkyv", "rkyv",
"schemars",
"serde", "serde",
] ]
@ -4898,6 +4922,7 @@ dependencies = [
"requirements-txt", "requirements-txt",
"rkyv", "rkyv",
"rustc-hash", "rustc-hash",
"schemars",
"serde", "serde",
"textwrap", "textwrap",
"thiserror", "thiserror",
@ -4927,6 +4952,7 @@ dependencies = [
"pep508_rs", "pep508_rs",
"reqwest", "reqwest",
"reqwest-middleware", "reqwest-middleware",
"schemars",
"serde", "serde",
"tempfile", "tempfile",
"thiserror", "thiserror",
@ -5006,7 +5032,6 @@ dependencies = [
"pep508_rs", "pep508_rs",
"schemars", "schemars",
"serde", "serde",
"serde_json",
"thiserror", "thiserror",
"toml", "toml",
"tracing", "tracing",
@ -5565,6 +5590,12 @@ version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.7.0" version = "1.7.0"

View file

@ -18,10 +18,10 @@ distribution-filename = { workspace = true, features = ["serde"] }
pep440_rs = { workspace = true } pep440_rs = { workspace = true }
pep508_rs = { workspace = true } pep508_rs = { workspace = true }
platform-tags = { workspace = true } platform-tags = { workspace = true }
pypi-types = { workspace = true }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-git = { workspace = true, features = ["vendored-openssl"] } uv-git = { workspace = true, features = ["vendored-openssl"] }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
pypi-types = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
@ -29,6 +29,7 @@ itertools = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
rkyv = { workspace = true } rkyv = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -27,6 +27,21 @@ pub enum IndexUrl {
Path(VerbatimUrl), Path(VerbatimUrl),
} }
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for IndexUrl {
fn schema_name() -> String {
"IndexUrl".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..schemars::schema::SchemaObject::default()
}
.into()
}
}
impl IndexUrl { impl IndexUrl {
/// Return the raw URL for the index. /// Return the raw URL for the index.
pub fn url(&self) -> &Url { pub fn url(&self) -> &Url {
@ -113,6 +128,21 @@ pub enum FlatIndexLocation {
Url(Url), Url(Url),
} }
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for FlatIndexLocation {
fn schema_name() -> String {
"FlatIndexLocation".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..schemars::schema::SchemaObject::default()
}
.into()
}
}
impl FromStr for FlatIndexLocation { impl FromStr for FlatIndexLocation {
type Err = url::ParseError; type Err = url::ParseError;

View file

@ -39,6 +39,7 @@ platform-info = { workspace = true }
reflink-copy = { workspace = true } reflink-copy = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }

View file

@ -205,6 +205,7 @@ fn parse_scripts(
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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.
Clone, Clone,

View file

@ -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 }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -22,6 +22,7 @@ anyhow = { workspace = true }
clap = { workspace = true, features = ["derive"], optional = true } clap = { workspace = true, features = ["derive"], optional = true }
itertools = { workspace = true } itertools = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true } serde_json = { workspace = true, optional = true }

View file

@ -8,6 +8,7 @@ use uv_auth::{self, KeyringProvider};
feature = "serde", feature = "serde",
serde(deny_unknown_fields, rename_all = "kebab-case") serde(deny_unknown_fields, rename_all = "kebab-case")
)] )]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum KeyringProviderType { pub enum KeyringProviderType {
/// Do not use keyring for credential lookup. /// Do not use keyring for credential lookup.
#[default] #[default]

View file

@ -203,6 +203,7 @@ impl NoBuild {
feature = "serde", feature = "serde",
serde(deny_unknown_fields, rename_all = "kebab-case") serde(deny_unknown_fields, rename_all = "kebab-case")
)] )]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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.
/// ///

View file

@ -28,6 +28,7 @@ impl FromStr for ConfigSettingEntry {
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
enum ConfigSettingValue { enum ConfigSettingValue {
/// The value consists of a single string. /// The value consists of a single string.
String(String), String(String),
@ -82,6 +83,7 @@ impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
/// ///
/// 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(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(not(feature = "serde"), allow(dead_code))] #[cfg_attr(not(feature = "serde"), allow(dead_code))]
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>); pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);

View file

@ -59,6 +59,29 @@ impl<'de> serde::Deserialize<'de> for PackageNameSpecifier {
} }
} }
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PackageNameSpecifier {
fn schema_name() -> String {
"PackageNameSpecifier".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
// See: https://packaging.python.org/en/latest/specifications/name-normalization/#name-format
pattern: Some(
r"^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$"
.to_string(),
),
..schemars::schema::StringValidation::default()
})),
..schemars::schema::SchemaObject::default()
}
.into()
}
}
/// 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

View file

@ -33,6 +33,7 @@ uv-normalize = { workspace = true }
uv-resolver = { workspace = true } uv-resolver = { workspace = true }
uv-toolchain = { workspace = true } uv-toolchain = { workspace = true }
uv-types = { 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 # 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. # dependencies, to ensure that we're forced to think twice before including them in other crates.
@ -47,8 +48,10 @@ itertools = { workspace = true }
owo-colors = { workspace = true } owo-colors = { workspace = true }
petgraph = { workspace = true } petgraph = { workspace = true }
poloto = { version = "19.1.2" } poloto = { version = "19.1.2" }
pretty_assertions = { version = "1.4.0" }
resvg = { version = "0.29.0" } resvg = { version = "0.29.0" }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tagu = { version = "0.1.6" } tagu = { version = "0.1.6" }

View file

@ -0,0 +1,99 @@
use std::path::PathBuf;
use anstream::println;
use anyhow::{bail, Result};
use pretty_assertions::StrComparison;
use schemars::schema_for;
use uv_workspace::Options;
use crate::ROOT_DIR;
#[derive(clap::Args)]
pub(crate) struct GenerateJsonSchemaArgs {
/// Write the generated table to stdout (rather than to `uv.schema.json`).
#[arg(long, default_value_t, value_enum)]
mode: Mode,
}
#[derive(Copy, Clone, PartialEq, Eq, clap::ValueEnum, Default)]
enum Mode {
/// Update the content in the `configuration.md`.
#[default]
Write,
/// Don't write to the file, check if the file is up-to-date and error if not.
Check,
/// Write the generated help to stdout.
DryRun,
}
pub(crate) fn main(args: &GenerateJsonSchemaArgs) -> Result<()> {
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
let filename = "uv.schema.json";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
match args.mode {
Mode::DryRun => {
println!("{schema_string}");
}
Mode::Check => match fs_err::read_to_string(schema_path) {
Ok(current) => {
if current == schema_string {
println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &schema_string);
bail!("{filename} changed, please run `cargo dev generate-json-schema`:\n{comparison}");
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("{filename} not found, please run `cargo dev generate-json-schema`");
}
Err(err) => {
bail!("{filename} changed, please run `cargo dev generate-json-schema`:\n{err}");
}
},
Mode::Write => match fs_err::read_to_string(&schema_path) {
Ok(current) => {
if current == schema_string {
println!("Up-to-date: {filename}");
} else {
println!("Updating: {filename}");
fs_err::write(schema_path, schema_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
println!("Updating: {filename}");
fs_err::write(schema_path, schema_string.as_bytes())?;
}
Err(err) => {
bail!("{filename} changed, please run `cargo dev generate-json-schema`:\n{err}");
}
},
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::env;
use anyhow::Result;
use crate::generate_json_schema::Mode;
use super::{main, GenerateJsonSchemaArgs};
#[test]
fn test_generate_json_schema() -> Result<()> {
let mode = if env::var("UV_UPDATE_SCHEMA").as_deref() == Ok("1") {
Mode::Write
} else {
Mode::Check
};
main(&GenerateJsonSchemaArgs { mode })
}
}

View file

@ -22,6 +22,7 @@ use crate::build::{build, BuildArgs};
use crate::clear_compile::ClearCompileArgs; use crate::clear_compile::ClearCompileArgs;
use crate::compile::CompileArgs; use crate::compile::CompileArgs;
use crate::fetch_python::FetchPythonArgs; use crate::fetch_python::FetchPythonArgs;
use crate::generate_json_schema::GenerateJsonSchemaArgs;
use crate::render_benchmarks::RenderBenchmarksArgs; use crate::render_benchmarks::RenderBenchmarksArgs;
use crate::resolve_cli::ResolveCliArgs; use crate::resolve_cli::ResolveCliArgs;
use crate::wheel_metadata::WheelMetadataArgs; use crate::wheel_metadata::WheelMetadataArgs;
@ -46,11 +47,14 @@ mod build;
mod clear_compile; mod clear_compile;
mod compile; mod compile;
mod fetch_python; mod fetch_python;
mod generate_json_schema;
mod render_benchmarks; mod render_benchmarks;
mod resolve_cli; mod resolve_cli;
mod resolve_many; mod resolve_many;
mod wheel_metadata; mod wheel_metadata;
const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");
#[derive(Parser)] #[derive(Parser)]
enum Cli { enum Cli {
/// Build a source distribution into a wheel /// Build a source distribution into a wheel
@ -76,6 +80,8 @@ enum Cli {
ClearCompile(ClearCompileArgs), ClearCompile(ClearCompileArgs),
/// Fetch Python versions for testing /// Fetch Python versions for testing
FetchPython(FetchPythonArgs), FetchPython(FetchPythonArgs),
/// Generate JSON schema for the TOML configuration file.
GenerateJSONSchema(GenerateJsonSchemaArgs),
} }
#[instrument] // Anchor span to check for overhead #[instrument] // Anchor span to check for overhead
@ -97,6 +103,7 @@ async fn run() -> Result<()> {
Cli::Compile(args) => compile::compile(args).await?, Cli::Compile(args) => compile::compile(args).await?,
Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?,
Cli::FetchPython(args) => fetch_python::fetch_python(args).await?, Cli::FetchPython(args) => fetch_python::fetch_python(args).await?,
Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?,
} }
Ok(()) Ok(())
} }

View file

@ -5,5 +5,6 @@ edition = "2021"
description = "Normalization for distribution, package and extra anmes" description = "Normalization for distribution, package and extra anmes"
[dependencies] [dependencies]
serde = { workspace = true, features = ["derive"], optional = true }
rkyv = { workspace = true, optional = true } rkyv = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }

View file

@ -14,8 +14,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam
/// See: /// See:
/// - <https://peps.python.org/pep-0685/#specification/> /// - <https://peps.python.org/pep-0685/#specification/>
/// - <https://packaging.python.org/en/latest/specifications/name-normalization/> /// - <https://packaging.python.org/en/latest/specifications/name-normalization/>
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ExtraName(String); pub struct ExtraName(String);
impl ExtraName { impl ExtraName {

View file

@ -12,14 +12,15 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam
/// down to a single `-`, e.g., `---`, `.`, and `__` all get converted to just `-`. /// down to a single `-`, e.g., `---`, `.`, and `__` all get converted to just `-`.
/// ///
/// See: <https://packaging.python.org/en/latest/specifications/name-normalization/> /// See: <https://packaging.python.org/en/latest/specifications/name-normalization/>
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize))] #[cfg_attr(feature = "serde", derive(Serialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr( #[cfg_attr(
feature = "rkyv", feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize), derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
archive(check_bytes), archive(check_bytes),
archive_attr(derive(Debug)) archive_attr(derive(Debug))
)] )]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PackageName(String); pub struct PackageName(String);
impl PackageName { impl PackageName {

View file

@ -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 }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
textwrap = { workspace = true } textwrap = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -52,3 +52,24 @@ impl std::fmt::Display for ExcludeNewer {
self.0.fmt(f) self.0.fmt(f)
} }
} }
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for ExcludeNewer {
fn schema_name() -> String {
"ExcludeNewer".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(
r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2}))?$".to_string(),
),
..schemars::schema::StringValidation::default()
})),
..Default::default()
}
.into()
}
}

View file

@ -12,6 +12,7 @@ use crate::Manifest;
feature = "serde", feature = "serde",
serde(deny_unknown_fields, rename_all = "kebab-case") serde(deny_unknown_fields, rename_all = "kebab-case")
)] )]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum PreReleaseMode { pub enum PreReleaseMode {
/// Disallow all pre-release versions. /// Disallow all pre-release versions.
Disallow, Disallow,

View file

@ -40,6 +40,7 @@ use crate::{Manifest, ResolveError};
feature = "serde", feature = "serde",
serde(deny_unknown_fields, rename_all = "kebab-case") serde(deny_unknown_fields, rename_all = "kebab-case")
)] )]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
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,

View file

@ -12,6 +12,7 @@ use crate::Manifest;
feature = "serde", feature = "serde",
serde(deny_unknown_fields, rename_all = "kebab-case") serde(deny_unknown_fields, rename_all = "kebab-case")
)] )]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ResolutionMode { pub enum ResolutionMode {
/// Resolve the highest compatible version of each package. /// Resolve the highest compatible version of each package.
#[default] #[default]

View file

@ -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 }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
tempfile = { workspace = true } tempfile = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -41,6 +41,17 @@ impl FromStr for PythonVersion {
} }
} }
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for PythonVersion {
fn schema_name() -> String {
String::from("PythonVersion")
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
<String>::json_schema(gen)
}
}
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PythonVersion { impl<'de> serde::Deserialize<'de> for PythonVersion {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {

View file

@ -13,25 +13,20 @@ license = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
distribution-types = { workspace = true } distribution-types = { workspace = true, features = ["schemars"] }
install-wheel-rs = { workspace = true } install-wheel-rs = { workspace = true, features = ["schemars"] }
pep508_rs = { workspace = true } pep508_rs = { workspace = true }
uv-auth = { workspace = true, features = ["serde"] } uv-auth = { workspace = true, features = ["schemars", "serde"] }
uv-configuration = { workspace = true, features = ["serde"] } uv-configuration = { workspace = true, features = ["schemars", "serde"] }
uv-fs = { workspace = true } uv-fs = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true, features = ["schemars"] }
uv-resolver = { workspace = true, features = ["serde"] } uv-resolver = { workspace = true, features = ["schemars", "serde"] }
uv-toolchain = { workspace = true, features = ["serde"] } uv-toolchain = { workspace = true, features = ["schemars", "serde"] }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true } serde = { workspace = true }
serde_json = { workspace = true, optional = true }
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
[features]
default = []
serde = ["dep:serde", "dep:serde_json"]

View file

@ -27,6 +27,7 @@ pub(crate) struct Tools {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options { pub struct Options {
pub native_tls: Option<bool>, pub native_tls: Option<bool>,
pub no_cache: Option<bool>, pub no_cache: Option<bool>,
@ -38,6 +39,7 @@ pub struct Options {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)] #[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PipOptions { pub struct PipOptions {
pub python: Option<String>, pub python: Option<String>,
pub system: Option<bool>, pub system: Option<bool>,

View file

@ -36,7 +36,7 @@ uv-toolchain = { workspace = true }
uv-types = { workspace = true, features = ["clap"] } uv-types = { workspace = true, features = ["clap"] }
uv-virtualenv = { workspace = true } uv-virtualenv = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
uv-workspace = { workspace = true, features = ["serde", "schemars"] } uv-workspace = { workspace = true, features = ["schemars"] }
anstream = { workspace = true } anstream = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }

548
uv.schema.json generated Normal file
View file

@ -0,0 +1,548 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Options",
"description": "A `[tool.uv]` section.",
"type": "object",
"properties": {
"cache-dir": {
"type": [
"string",
"null"
]
},
"native-tls": {
"type": [
"boolean",
"null"
]
},
"no-cache": {
"type": [
"boolean",
"null"
]
},
"pip": {
"anyOf": [
{
"$ref": "#/definitions/PipOptions"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"AnnotationStyle": {
"description": "Indicate the style of annotation comments, used to indicate the dependencies that requested each package.",
"oneOf": [
{
"description": "Render the annotations on a single, comma-separated line.",
"type": "string",
"enum": [
"line"
]
},
{
"description": "Render each annotation on its own line.",
"type": "string",
"enum": [
"split"
]
}
]
},
"ConfigSettingValue": {
"oneOf": [
{
"description": "The value consists of a single string.",
"type": "object",
"required": [
"String"
],
"properties": {
"String": {
"type": "string"
}
},
"additionalProperties": false
},
{
"description": "The value consists of a list of strings.",
"type": "object",
"required": [
"List"
],
"properties": {
"List": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
]
},
"ConfigSettings": {
"description": "Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or list of strings.\n\nSee: <https://peps.python.org/pep-0517/#config-settings>",
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/ConfigSettingValue"
}
},
"ExcludeNewer": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2}))?$"
},
"ExtraName": {
"description": "The normalized name of an extra dependency group.\n\nConverts the name to lowercase and collapses any run of the characters `-`, `_` and `.` down to a single `-`, e.g., `---`, `.`, and `__` all get converted to just `-`.\n\nSee: - <https://peps.python.org/pep-0685/#specification/> - <https://packaging.python.org/en/latest/specifications/name-normalization/>",
"type": "string"
},
"FlatIndexLocation": {
"type": "string"
},
"IndexStrategy": {
"oneOf": [
{
"description": "Only use results from the first index that returns a match for a given package name.\n\nWhile this differs from pip's behavior, it's the default index strategy as it's the most secure.",
"type": "string",
"enum": [
"first-match"
]
},
{
"description": "Search for every package name across all indexes, exhausting the versions from the first index before moving on to the next.\n\nIn this strategy, we look for every package across all indexes. When resolving, we attempt to use versions from the indexes in order, such that we exhaust all available versions from the first index before moving on to the next. Further, if a version is found to be incompatible in the first index, we do not reconsider that version in subsequent indexes, even if the secondary index might contain compatible versions (e.g., variants of the same versions with different ABI tags or Python version constraints).\n\nSee: https://peps.python.org/pep-0708/",
"type": "string",
"enum": [
"unsafe-any-match"
]
}
]
},
"IndexUrl": {
"type": "string"
},
"KeyringProviderType": {
"description": "Keyring provider type to use for credential lookup.",
"oneOf": [
{
"description": "Do not use keyring for credential lookup.",
"type": "string",
"enum": [
"disabled"
]
},
{
"description": "Use the `keyring` command for credential lookup.",
"type": "string",
"enum": [
"subprocess"
]
}
]
},
"LinkMode": {
"oneOf": [
{
"description": "Clone (i.e., copy-on-write) packages from the wheel into the site packages.",
"type": "string",
"enum": [
"clone"
]
},
{
"description": "Copy packages from the wheel into the site packages.",
"type": "string",
"enum": [
"copy"
]
},
{
"description": "Hard link packages from the wheel into the site packages.",
"type": "string",
"enum": [
"hardlink"
]
}
]
},
"PackageName": {
"description": "The normalized name of a package.\n\nConverts the name to lowercase and collapses any run of the characters `-`, `_` and `.` down to a single `-`, e.g., `---`, `.`, and `__` all get converted to just `-`.\n\nSee: <https://packaging.python.org/en/latest/specifications/name-normalization/>",
"type": "string"
},
"PackageNameSpecifier": {
"type": "string",
"pattern": "^(:none:|:all:|([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]))$"
},
"PipOptions": {
"description": "A `[tool.uv.pip]` section.",
"type": "object",
"properties": {
"all-extras": {
"type": [
"boolean",
"null"
]
},
"annotation-style": {
"anyOf": [
{
"$ref": "#/definitions/AnnotationStyle"
},
{
"type": "null"
}
]
},
"break-system-packages": {
"type": [
"boolean",
"null"
]
},
"compile-bytecode": {
"type": [
"boolean",
"null"
]
},
"config-settings": {
"anyOf": [
{
"$ref": "#/definitions/ConfigSettings"
},
{
"type": "null"
}
]
},
"custom-compile-command": {
"type": [
"string",
"null"
]
},
"emit-find-links": {
"type": [
"boolean",
"null"
]
},
"emit-index-annotation": {
"type": [
"boolean",
"null"
]
},
"emit-index-url": {
"type": [
"boolean",
"null"
]
},
"emit-marker-expression": {
"type": [
"boolean",
"null"
]
},
"exclude-newer": {
"anyOf": [
{
"$ref": "#/definitions/ExcludeNewer"
},
{
"type": "null"
}
]
},
"extra": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ExtraName"
}
},
"extra-index-url": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/IndexUrl"
}
},
"find-links": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/FlatIndexLocation"
}
},
"generate-hashes": {
"type": [
"boolean",
"null"
]
},
"index-strategy": {
"anyOf": [
{
"$ref": "#/definitions/IndexStrategy"
},
{
"type": "null"
}
]
},
"index-url": {
"anyOf": [
{
"$ref": "#/definitions/IndexUrl"
},
{
"type": "null"
}
]
},
"keyring-provider": {
"anyOf": [
{
"$ref": "#/definitions/KeyringProviderType"
},
{
"type": "null"
}
]
},
"legacy-setup-py": {
"type": [
"boolean",
"null"
]
},
"link-mode": {
"anyOf": [
{
"$ref": "#/definitions/LinkMode"
},
{
"type": "null"
}
]
},
"no-annotate": {
"type": [
"boolean",
"null"
]
},
"no-binary": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/PackageNameSpecifier"
}
},
"no-build": {
"type": [
"boolean",
"null"
]
},
"no-build-isolation": {
"type": [
"boolean",
"null"
]
},
"no-deps": {
"type": [
"boolean",
"null"
]
},
"no-emit-package": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/PackageName"
}
},
"no-header": {
"type": [
"boolean",
"null"
]
},
"no-index": {
"type": [
"boolean",
"null"
]
},
"no-strip-extras": {
"type": [
"boolean",
"null"
]
},
"offline": {
"type": [
"boolean",
"null"
]
},
"only-binary": {
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/PackageNameSpecifier"
}
},
"output-file": {
"type": [
"string",
"null"
]
},
"prerelease": {
"anyOf": [
{
"$ref": "#/definitions/PreReleaseMode"
},
{
"type": "null"
}
]
},
"python": {
"type": [
"string",
"null"
]
},
"python-version": {
"anyOf": [
{
"$ref": "#/definitions/PythonVersion"
},
{
"type": "null"
}
]
},
"require-hashes": {
"type": [
"boolean",
"null"
]
},
"resolution": {
"anyOf": [
{
"$ref": "#/definitions/ResolutionMode"
},
{
"type": "null"
}
]
},
"strict": {
"type": [
"boolean",
"null"
]
},
"system": {
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"PreReleaseMode": {
"oneOf": [
{
"description": "Disallow all pre-release versions.",
"type": "string",
"enum": [
"disallow"
]
},
{
"description": "Allow all pre-release versions.",
"type": "string",
"enum": [
"allow"
]
},
{
"description": "Allow pre-release versions if all versions of a package are pre-release.",
"type": "string",
"enum": [
"if-necessary"
]
},
{
"description": "Allow pre-release versions for first-party packages with explicit pre-release markers in their version requirements.",
"type": "string",
"enum": [
"explicit"
]
},
{
"description": "Allow pre-release versions if all versions of a package are pre-release, or if the package has an explicit pre-release marker in its version requirements.",
"type": "string",
"enum": [
"if-necessary-or-explicit"
]
}
]
},
"PythonVersion": {
"type": "string"
},
"ResolutionMode": {
"oneOf": [
{
"description": "Resolve the highest compatible version of each package.",
"type": "string",
"enum": [
"highest"
]
},
{
"description": "Resolve the lowest compatible version of each package.",
"type": "string",
"enum": [
"lowest"
]
},
{
"description": "Resolve the lowest compatible version of any direct dependencies, and the highest compatible version of any transitive dependencies.",
"type": "string",
"enum": [
"lowest-direct"
]
}
]
}
}
}