From 7fb2bf816fc3b4aa92170aba0ef1ebb028de4b84 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 17 Apr 2024 13:24:41 -0400 Subject: [PATCH] Add JSON Schema support (#3046) ## Summary This PR adds JSON Schema support. The setup mirrors Ruff's own. --- .cargo/config.toml | 2 + .gitattributes | 2 + CONTRIBUTING.md | 13 +- Cargo.lock | 35 +- crates/distribution-types/Cargo.toml | 3 +- crates/distribution-types/src/index_url.rs | 30 + crates/install-wheel-rs/Cargo.toml | 1 + crates/install-wheel-rs/src/linker.rs | 1 + crates/uv-auth/Cargo.toml | 1 + crates/uv-configuration/Cargo.toml | 1 + crates/uv-configuration/src/authentication.rs | 1 + crates/uv-configuration/src/build_options.rs | 1 + .../uv-configuration/src/config_settings.rs | 2 + .../uv-configuration/src/name_specifiers.rs | 23 + crates/uv-dev/Cargo.toml | 3 + crates/uv-dev/src/generate_json_schema.rs | 99 ++++ crates/uv-dev/src/main.rs | 7 + crates/uv-normalize/Cargo.toml | 3 +- crates/uv-normalize/src/extra_name.rs | 3 +- crates/uv-normalize/src/package_name.rs | 3 +- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/exclude_newer.rs | 21 + crates/uv-resolver/src/prerelease_mode.rs | 1 + crates/uv-resolver/src/resolution.rs | 1 + crates/uv-resolver/src/resolution_mode.rs | 1 + crates/uv-toolchain/Cargo.toml | 1 + crates/uv-toolchain/src/python_version.rs | 11 + crates/uv-workspace/Cargo.toml | 21 +- crates/uv-workspace/src/settings.rs | 2 + crates/uv/Cargo.toml | 2 +- uv.schema.json | 548 ++++++++++++++++++ 31 files changed, 818 insertions(+), 26 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 crates/uv-dev/src/generate_json_schema.rs create mode 100644 uv.schema.json diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..689580aad --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +dev = "run --package uv-dev" diff --git a/.gitattributes b/.gitattributes index 6313b56c5..923031e0b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ * text=auto eol=lf + +uv.schema.json linguist-generated=true text=auto eol=lf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9341a3972..1a20bf5ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We have issues labeled as [Good First Issue](https://github.com/astral-sh/uv/iss ### 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 sudo apt install build-essential cmake @@ -16,7 +16,7 @@ sudo apt install build-essential cmake ### macOS -CMake may be installed with Homebrew: +You can install CMake with Homebrew: ```shell brew install cmake @@ -26,13 +26,14 @@ See the [Python](#python) section for instructions on installing the Python vers ### Windows -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). +You can install CMake from the [installers](https://cmake.org/download/) or with `pipx install cmake`. ## Testing 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 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 ``` -### 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: @@ -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. ```shell -RUST_LOG=trace uv … +RUST_LOG=trace uv ``` ## Releases diff --git a/Cargo.lock b/Cargo.lock index 708fbc95f..2cc929509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -983,6 +983,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -1051,6 +1057,7 @@ dependencies = [ "pypi-types", "rkyv", "rustc-hash", + "schemars", "serde", "serde_json", "thiserror", @@ -1802,6 +1809,7 @@ dependencies = [ "reflink-copy", "regex", "rustc-hash", + "schemars", "serde", "serde_json", "sha2", @@ -2652,6 +2660,16 @@ dependencies = [ "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]] name = "priority-queue" version = "2.0.2" @@ -2714,7 +2732,7 @@ dependencies = [ "indoc", "libc", "memoffset 0.9.1", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "portable-atomic", "pyo3-build-config", "pyo3-ffi", @@ -4455,6 +4473,7 @@ dependencies = [ "reqwest", "reqwest-middleware", "rust-netrc", + "schemars", "serde", "tempfile", "test-log", @@ -4577,6 +4596,7 @@ dependencies = [ "itertools 0.12.1", "pep508_rs", "rustc-hash", + "schemars", "serde", "serde_json", "uv-auth", @@ -4605,8 +4625,10 @@ dependencies = [ "pep508_rs", "petgraph", "poloto", + "pretty_assertions", "resvg", "rustc-hash", + "schemars", "serde", "serde_json", "tagu", @@ -4628,6 +4650,7 @@ dependencies = [ "uv-resolver", "uv-toolchain", "uv-types", + "uv-workspace", "walkdir", ] @@ -4831,6 +4854,7 @@ name = "uv-normalize" version = "0.0.1" dependencies = [ "rkyv", + "schemars", "serde", ] @@ -4898,6 +4922,7 @@ dependencies = [ "requirements-txt", "rkyv", "rustc-hash", + "schemars", "serde", "textwrap", "thiserror", @@ -4927,6 +4952,7 @@ dependencies = [ "pep508_rs", "reqwest", "reqwest-middleware", + "schemars", "serde", "tempfile", "thiserror", @@ -5006,7 +5032,6 @@ dependencies = [ "pep508_rs", "schemars", "serde", - "serde_json", "thiserror", "toml", "tracing", @@ -5565,6 +5590,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zeroize" version = "1.7.0" diff --git a/crates/distribution-types/Cargo.toml b/crates/distribution-types/Cargo.toml index c43028750..66602434f 100644 --- a/crates/distribution-types/Cargo.toml +++ b/crates/distribution-types/Cargo.toml @@ -18,10 +18,10 @@ distribution-filename = { workspace = true, features = ["serde"] } pep440_rs = { workspace = true } pep508_rs = { workspace = true } platform-tags = { workspace = true } +pypi-types = { workspace = true } uv-fs = { workspace = true } uv-git = { workspace = true, features = ["vendored-openssl"] } uv-normalize = { workspace = true } -pypi-types = { workspace = true } anyhow = { workspace = true } fs-err = { workspace = true } @@ -29,6 +29,7 @@ itertools = { workspace = true } once_cell = { workspace = true } rkyv = { workspace = true } rustc-hash = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 3b0893fc2..1dc4cc332 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -27,6 +27,21 @@ pub enum IndexUrl { 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 { /// Return the raw URL for the index. pub fn url(&self) -> &Url { @@ -113,6 +128,21 @@ pub enum FlatIndexLocation { 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 { type Err = url::ParseError; diff --git a/crates/install-wheel-rs/Cargo.toml b/crates/install-wheel-rs/Cargo.toml index 132ad4fa5..273f2534d 100644 --- a/crates/install-wheel-rs/Cargo.toml +++ b/crates/install-wheel-rs/Cargo.toml @@ -39,6 +39,7 @@ platform-info = { workspace = true } reflink-copy = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/crates/install-wheel-rs/src/linker.rs b/crates/install-wheel-rs/src/linker.rs index 0c456d489..cf7121914 100644 --- a/crates/install-wheel-rs/src/linker.rs +++ b/crates/install-wheel-rs/src/linker.rs @@ -205,6 +205,7 @@ fn parse_scripts( #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum LinkMode { /// Clone (i.e., copy-on-write) packages from the wheel into the site packages. Clone, diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index d6ec43701..e23fe4610 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -11,6 +11,7 @@ once_cell = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } rust-netrc = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index bc4029869..848ff94e2 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -22,6 +22,7 @@ anyhow = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } itertools = { workspace = true } rustc-hash = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } diff --git a/crates/uv-configuration/src/authentication.rs b/crates/uv-configuration/src/authentication.rs index cf907abc7..a5cf0e029 100644 --- a/crates/uv-configuration/src/authentication.rs +++ b/crates/uv-configuration/src/authentication.rs @@ -8,6 +8,7 @@ use uv_auth::{self, KeyringProvider}; feature = "serde", serde(deny_unknown_fields, rename_all = "kebab-case") )] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum KeyringProviderType { /// Do not use keyring for credential lookup. #[default] diff --git a/crates/uv-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index 371123ff5..d3cf8dcaf 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -203,6 +203,7 @@ impl NoBuild { feature = "serde", serde(deny_unknown_fields, rename_all = "kebab-case") )] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum IndexStrategy { /// Only use results from the first index that returns a match for a given package name. /// diff --git a/crates/uv-configuration/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs index 50ebfb982..a43232943 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -28,6 +28,7 @@ impl FromStr for ConfigSettingEntry { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] enum ConfigSettingValue { /// The value consists of a single string. String(String), @@ -82,6 +83,7 @@ impl<'de> serde::Deserialize<'de> for ConfigSettingValue { /// /// See: #[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr(not(feature = "serde"), allow(dead_code))] pub struct ConfigSettings(BTreeMap); diff --git a/crates/uv-configuration/src/name_specifiers.rs b/crates/uv-configuration/src/name_specifiers.rs index 8496b9831..37942433b 100644 --- a/crates/uv-configuration/src/name_specifiers.rs +++ b/crates/uv-configuration/src/name_specifiers.rs @@ -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. /// /// Consumes both package names and selection directives for compatibility with pip flags diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 6bab89234..ef98d505e 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -33,6 +33,7 @@ uv-normalize = { workspace = true } uv-resolver = { workspace = true } 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. @@ -47,8 +48,10 @@ itertools = { workspace = true } owo-colors = { workspace = true } petgraph = { workspace = true } poloto = { version = "19.1.2" } +pretty_assertions = { version = "1.4.0" } resvg = { version = "0.29.0" } rustc-hash = { workspace = true } +schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tagu = { version = "0.1.6" } diff --git a/crates/uv-dev/src/generate_json_schema.rs b/crates/uv-dev/src/generate_json_schema.rs new file mode 100644 index 000000000..b30bea1fa --- /dev/null +++ b/crates/uv-dev/src/generate_json_schema.rs @@ -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(¤t, &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 }) + } +} diff --git a/crates/uv-dev/src/main.rs b/crates/uv-dev/src/main.rs index ce83a8167..02a3e5376 100644 --- a/crates/uv-dev/src/main.rs +++ b/crates/uv-dev/src/main.rs @@ -22,6 +22,7 @@ use crate::build::{build, BuildArgs}; use crate::clear_compile::ClearCompileArgs; use crate::compile::CompileArgs; use crate::fetch_python::FetchPythonArgs; +use crate::generate_json_schema::GenerateJsonSchemaArgs; use crate::render_benchmarks::RenderBenchmarksArgs; use crate::resolve_cli::ResolveCliArgs; use crate::wheel_metadata::WheelMetadataArgs; @@ -46,11 +47,14 @@ mod build; mod clear_compile; mod compile; mod fetch_python; +mod generate_json_schema; mod render_benchmarks; mod resolve_cli; mod resolve_many; mod wheel_metadata; +const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../"); + #[derive(Parser)] enum Cli { /// Build a source distribution into a wheel @@ -76,6 +80,8 @@ enum Cli { ClearCompile(ClearCompileArgs), /// Fetch Python versions for testing FetchPython(FetchPythonArgs), + /// Generate JSON schema for the TOML configuration file. + GenerateJSONSchema(GenerateJsonSchemaArgs), } #[instrument] // Anchor span to check for overhead @@ -97,6 +103,7 @@ async fn run() -> Result<()> { Cli::Compile(args) => compile::compile(args).await?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, Cli::FetchPython(args) => fetch_python::fetch_python(args).await?, + Cli::GenerateJSONSchema(args) => generate_json_schema::main(&args)?, } Ok(()) } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 658925fce..4b64edfc1 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -5,5 +5,6 @@ edition = "2021" description = "Normalization for distribution, package and extra anmes" [dependencies] -serde = { workspace = true, features = ["derive"], optional = true } rkyv = { workspace = true, optional = true } +schemars = { workspace = true, optional = true } +serde = { workspace = true, features = ["derive"], optional = true } diff --git a/crates/uv-normalize/src/extra_name.rs b/crates/uv-normalize/src/extra_name.rs index 8c1c94e7b..c4de74f34 100644 --- a/crates/uv-normalize/src/extra_name.rs +++ b/crates/uv-normalize/src/extra_name.rs @@ -14,8 +14,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: /// - /// - -#[cfg_attr(feature = "serde", derive(Serialize))] #[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); impl ExtraName { diff --git a/crates/uv-normalize/src/package_name.rs b/crates/uv-normalize/src/package_name.rs index fbc9d4b30..4f990e07a 100644 --- a/crates/uv-normalize/src/package_name.rs +++ b/crates/uv-normalize/src/package_name.rs @@ -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 `-`. /// /// See: +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[cfg_attr( feature = "rkyv", derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize), archive(check_bytes), archive_attr(derive(Debug)) )] -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct PackageName(String); impl PackageName { diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 08a853693..acdc3395b 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -48,6 +48,7 @@ petgraph = { workspace = true } pubgrub = { workspace = true } rkyv = { workspace = true } rustc-hash = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } textwrap = { workspace = true } thiserror = { workspace = true } diff --git a/crates/uv-resolver/src/exclude_newer.rs b/crates/uv-resolver/src/exclude_newer.rs index 68b8d8f74..95e16d7d2 100644 --- a/crates/uv-resolver/src/exclude_newer.rs +++ b/crates/uv-resolver/src/exclude_newer.rs @@ -52,3 +52,24 @@ impl std::fmt::Display for ExcludeNewer { 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() + } +} diff --git a/crates/uv-resolver/src/prerelease_mode.rs b/crates/uv-resolver/src/prerelease_mode.rs index 9d887a37a..7b0aa6b41 100644 --- a/crates/uv-resolver/src/prerelease_mode.rs +++ b/crates/uv-resolver/src/prerelease_mode.rs @@ -12,6 +12,7 @@ use crate::Manifest; feature = "serde", serde(deny_unknown_fields, rename_all = "kebab-case") )] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum PreReleaseMode { /// Disallow all pre-release versions. Disallow, diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 86980cf67..c6fe8cfe9 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -40,6 +40,7 @@ use crate::{Manifest, ResolveError}; feature = "serde", serde(deny_unknown_fields, rename_all = "kebab-case") )] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum AnnotationStyle { /// Render the annotations on a single, comma-separated line. Line, diff --git a/crates/uv-resolver/src/resolution_mode.rs b/crates/uv-resolver/src/resolution_mode.rs index f18331b00..d15d389cc 100644 --- a/crates/uv-resolver/src/resolution_mode.rs +++ b/crates/uv-resolver/src/resolution_mode.rs @@ -12,6 +12,7 @@ use crate::Manifest; feature = "serde", serde(deny_unknown_fields, rename_all = "kebab-case") )] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum ResolutionMode { /// Resolve the highest compatible version of each package. #[default] diff --git a/crates/uv-toolchain/Cargo.toml b/crates/uv-toolchain/Cargo.toml index c9361c394..4e78f95ea 100644 --- a/crates/uv-toolchain/Cargo.toml +++ b/crates/uv-toolchain/Cargo.toml @@ -22,6 +22,7 @@ futures = { workspace = true } once_cell = {workspace = true} reqwest = { workspace = true } reqwest-middleware = { workspace = true } +schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } tempfile = { workspace = true } thiserror = { workspace = true } diff --git a/crates/uv-toolchain/src/python_version.rs b/crates/uv-toolchain/src/python_version.rs index d15b35abe..73ea5874b 100644 --- a/crates/uv-toolchain/src/python_version.rs +++ b/crates/uv-toolchain/src/python_version.rs @@ -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 { + ::json_schema(gen) + } +} + #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for PythonVersion { fn deserialize>(deserializer: D) -> Result { diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index df41bc281..cf57d33eb 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -13,25 +13,20 @@ license = { workspace = true } workspace = true [dependencies] -distribution-types = { workspace = true } -install-wheel-rs = { workspace = true } +distribution-types = { workspace = true, features = ["schemars"] } +install-wheel-rs = { workspace = true, features = ["schemars"] } pep508_rs = { workspace = true } -uv-auth = { workspace = true, features = ["serde"] } -uv-configuration = { workspace = true, features = ["serde"] } +uv-auth = { workspace = true, features = ["schemars", "serde"] } +uv-configuration = { workspace = true, features = ["schemars", "serde"] } uv-fs = { workspace = true } -uv-normalize = { workspace = true } -uv-resolver = { workspace = true, features = ["serde"] } -uv-toolchain = { workspace = true, features = ["serde"] } +uv-normalize = { workspace = true, features = ["schemars"] } +uv-resolver = { workspace = true, features = ["schemars", "serde"] } +uv-toolchain = { workspace = true, features = ["schemars", "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 } +serde = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } - -[features] -default = [] -serde = ["dep:serde", "dep:serde_json"] diff --git a/crates/uv-workspace/src/settings.rs b/crates/uv-workspace/src/settings.rs index 1e7e34960..77ab5bc74 100644 --- a/crates/uv-workspace/src/settings.rs +++ b/crates/uv-workspace/src/settings.rs @@ -27,6 +27,7 @@ pub(crate) struct Tools { #[allow(dead_code)] #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Options { pub native_tls: Option, pub no_cache: Option, @@ -38,6 +39,7 @@ pub struct Options { #[allow(dead_code)] #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct PipOptions { pub python: Option, pub system: Option, diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index f82c78049..d36d0b884 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -36,7 +36,7 @@ uv-toolchain = { workspace = true } uv-types = { workspace = true, features = ["clap"] } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } -uv-workspace = { workspace = true, features = ["serde", "schemars"] } +uv-workspace = { workspace = true, features = ["schemars"] } anstream = { workspace = true } anyhow = { workspace = true } diff --git a/uv.schema.json b/uv.schema.json new file mode 100644 index 000000000..9bf6dff5f --- /dev/null +++ b/uv.schema.json @@ -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: ", + "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: - - ", + "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: ", + "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" + ] + } + ] + } + } +} \ No newline at end of file