From c77aa5820b64ec6855daa83e369330573f6bae53 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 31 Dec 2024 10:37:46 -0500 Subject: [PATCH] Add a required version setting to uv (#10248) ## Summary This follows Ruff's design exactly: you can provide a version specifier (like `>=0.5`), and we'll enforce it at runtime. Closes https://github.com/astral-sh/uv/issues/8605. --- Cargo.lock | 1 + crates/uv-configuration/Cargo.toml | 1 + crates/uv-configuration/src/lib.rs | 2 + .../uv-configuration/src/required_version.rs | 61 +++++++++++++++++++ crates/uv-settings/src/combine.rs | 8 ++- crates/uv-settings/src/settings.rs | 21 ++++++- crates/uv/src/lib.rs | 11 ++++ crates/uv/src/settings.rs | 7 ++- crates/uv/tests/it/pip_install.rs | 6 +- crates/uv/tests/it/show_settings.rs | 37 ++++++++++- docs/reference/settings.md | 29 +++++++++ uv.schema.json | 15 +++++ 12 files changed, 188 insertions(+), 11 deletions(-) create mode 100644 crates/uv-configuration/src/required_version.rs diff --git a/Cargo.lock b/Cargo.lock index c4f8c7857..8e45d60e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4772,6 +4772,7 @@ dependencies = [ "uv-cache-info", "uv-cache-key", "uv-normalize", + "uv-pep440", "uv-pep508", "uv-platform-tags", "uv-pypi-types", diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 83de06f5c..cc8b00757 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -21,6 +21,7 @@ uv-cache = { workspace = true } uv-cache-info = { workspace = true } uv-cache-key = { workspace = true } uv-normalize = { workspace = true } +uv-pep440 = { workspace = true } uv-pep508 = { workspace = true, features = ["schemars"] } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index 6eb47a89a..62dfcb4a1 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -16,6 +16,7 @@ pub use package_options::*; pub use preview::*; pub use project_build_backend::*; pub use rayon::*; +pub use required_version::*; pub use sources::*; pub use target_triple::*; pub use trusted_host::*; @@ -40,6 +41,7 @@ mod package_options; mod preview; mod project_build_backend; mod rayon; +mod required_version; mod sources; mod target_triple; mod trusted_host; diff --git a/crates/uv-configuration/src/required_version.rs b/crates/uv-configuration/src/required_version.rs new file mode 100644 index 000000000..339abb1cf --- /dev/null +++ b/crates/uv-configuration/src/required_version.rs @@ -0,0 +1,61 @@ +use std::str::FromStr; + +use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers, VersionSpecifiersParseError}; + +/// A required version of uv, represented as a version specifier (e.g. `>=0.5.0`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RequiredVersion(VersionSpecifiers); + +impl RequiredVersion { + /// Return `true` if the given version is required. + pub fn contains(&self, version: &Version) -> bool { + self.0.contains(version) + } +} + +impl FromStr for RequiredVersion { + type Err = VersionSpecifiersParseError; + + fn from_str(s: &str) -> Result { + // Treat `0.5.0` as `==0.5.0`, for backwards compatibility. + if let Ok(version) = Version::from_str(s) { + Ok(Self(VersionSpecifiers::from( + VersionSpecifier::equals_version(version), + ))) + } else { + Ok(Self(VersionSpecifiers::from_str(s)?)) + } + } +} + +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for RequiredVersion { + fn schema_name() -> String { + String::from("RequiredVersion") + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some("A version specifier, e.g. `>=0.5.0` or `==0.5.0`.".to_string()), + ..schemars::schema::Metadata::default() + })), + ..schemars::schema::SchemaObject::default() + } + .into() + } +} + +impl<'de> serde::Deserialize<'de> for RequiredVersion { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } +} + +impl std::fmt::Display for RequiredVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index d33314474..14c37db8d 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -4,7 +4,8 @@ use std::path::PathBuf; use url::Url; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, TargetTriple, TrustedPublishing, + ConfigSettings, IndexStrategy, KeyringProviderType, RequiredVersion, TargetTriple, + TrustedPublishing, }; use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex}; use uv_install_wheel::linker::LinkMode; @@ -73,12 +74,12 @@ macro_rules! impl_combine_or { impl_combine_or!(AnnotationStyle); impl_combine_or!(ExcludeNewer); +impl_combine_or!(ForkStrategy); impl_combine_or!(Index); impl_combine_or!(IndexStrategy); impl_combine_or!(IndexUrl); impl_combine_or!(KeyringProviderType); impl_combine_or!(LinkMode); -impl_combine_or!(ForkStrategy); impl_combine_or!(NonZeroUsize); impl_combine_or!(PathBuf); impl_combine_or!(PipExtraIndex); @@ -88,10 +89,11 @@ impl_combine_or!(PrereleaseMode); impl_combine_or!(PythonDownloads); impl_combine_or!(PythonPreference); impl_combine_or!(PythonVersion); +impl_combine_or!(RequiredVersion); impl_combine_or!(ResolutionMode); +impl_combine_or!(SchemaConflicts); impl_combine_or!(String); impl_combine_or!(SupportedEnvironments); -impl_combine_or!(SchemaConflicts); impl_combine_or!(TargetTriple); impl_combine_or!(TrustedPublishing); impl_combine_or!(Url); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 1285f0d6d..bf9815e7f 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -3,8 +3,8 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; use url::Url; use uv_cache_info::CacheKey; use uv_configuration::{ - ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple, - TrustedHost, TrustedPublishing, + ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier, RequiredVersion, + TargetTriple, TrustedHost, TrustedPublishing, }; use uv_distribution_types::{ Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata, @@ -149,6 +149,20 @@ impl Options { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct GlobalOptions { + /// Enforce a requirement on the version of uv. + /// + /// If the version of uv does not meet the requirement at runtime, uv will exit + /// with an error. + /// + /// Accepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`. + #[option( + default = "null", + value_type = "str", + example = r#" + required-version = ">=0.5.0" + "# + )] + pub required_version: Option, /// Whether to load TLS certificates from the platform's native certificate store. /// /// By default, uv loads certificates from the bundled `webpki-roots` crate. The @@ -1623,6 +1637,7 @@ impl From for ResolverInstallerOptions { pub struct OptionsWire { // #[serde(flatten)] // globals: GlobalOptions + required_version: Option, native_tls: Option, offline: Option, no_cache: Option, @@ -1704,6 +1719,7 @@ pub struct OptionsWire { impl From for Options { fn from(value: OptionsWire) -> Self { let OptionsWire { + required_version, native_tls, offline, no_cache, @@ -1764,6 +1780,7 @@ impl From for Options { Self { globals: GlobalOptions { + required_version, native_tls, offline, no_cache, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e30baf2df..9e7be8ce3 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -4,6 +4,7 @@ use std::fmt::Write; use std::io::stdout; use std::path::Path; use std::process::ExitCode; +use std::str::FromStr; use std::sync::atomic::Ordering; use anstream::eprintln; @@ -208,6 +209,16 @@ async fn run(mut cli: Cli) -> Result { // Resolve the cache settings. let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref()); + // Enforce the required version. + if let Some(required_version) = globals.required_version.as_ref() { + let package_version = uv_pep440::Version::from_str(uv_version::version())?; + if !required_version.contains(&package_version) { + return Err(anyhow::anyhow!( + "Required version `{required_version}` does not match the running version `{package_version}`", + )); + } + } + // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] let (duration_layer, _duration_guard) = logging::setup_duration()?; diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 3acbc107a..fff0dabb6 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -23,8 +23,8 @@ use uv_client::Connectivity; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DevGroupsSpecification, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, - NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, - TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, RequiredVersion, + SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl}; use uv_install_wheel::linker::LinkMode; @@ -53,6 +53,7 @@ const PYPI_PUBLISH_URL: &str = "https://upload.pypi.org/legacy/"; #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct GlobalSettings { + pub(crate) required_version: Option, pub(crate) quiet: bool, pub(crate) verbose: u8, pub(crate) color: ColorChoice, @@ -72,6 +73,8 @@ impl GlobalSettings { /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration. pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { Self { + required_version: workspace + .and_then(|workspace| workspace.globals.required_version.clone()), quiet: args.quiet, verbose: args.verbose, color: if let Some(color_choice) = args.color { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index c51e3819f..4742a85bf 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -218,8 +218,8 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { let mut filters = context.filters(); filters.push(( - "expected one of `native-tls`, `offline`, .*", - "expected one of `native-tls`, `offline`, [...]", + "expected one of `required-version`, `native-tls`, .*", + "expected one of `required-version`, `native-tls`, [...]", )); uv_snapshot!(filters, context.pip_install() @@ -235,7 +235,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, [...] + unknown field `unknown`, expected one of `required-version`, `native-tls`, [...] Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index fb8ea039f..3cf5d5ddc 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -63,6 +63,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -215,6 +216,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -368,6 +370,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -553,6 +556,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -707,6 +711,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -840,6 +845,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1017,6 +1023,7 @@ fn resolve_index_url() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1200,6 +1207,7 @@ fn resolve_index_url() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1437,6 +1445,7 @@ fn resolve_find_links() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1613,6 +1622,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1752,6 +1762,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -1933,6 +1944,7 @@ fn resolve_top_level() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2138,6 +2150,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2267,6 +2280,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2396,6 +2410,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2527,6 +2542,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2677,6 +2693,7 @@ fn resolve_tool() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2835,6 +2852,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -2992,6 +3010,7 @@ fn resolve_both() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3267,6 +3286,7 @@ fn resolve_config_file() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3438,7 +3458,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` "### ); @@ -3520,6 +3540,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3652,6 +3673,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3792,6 +3814,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -3946,6 +3969,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4129,6 +4153,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4318,6 +4343,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4502,6 +4528,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4693,6 +4720,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -4877,6 +4905,7 @@ fn index_priority() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5074,6 +5103,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5197,6 +5227,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5318,6 +5349,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5441,6 +5473,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5562,6 +5595,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, @@ -5684,6 +5718,7 @@ fn verify_hashes() -> anyhow::Result<()> { exit_code: 0 ----- stdout ----- GlobalSettings { + required_version: None, quiet: false, verbose: 0, color: Auto, diff --git a/docs/reference/settings.md b/docs/reference/settings.md index e0e2fc738..e8a142900 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1522,6 +1522,35 @@ Reinstall a specific package, regardless of whether it's already installed. Impl --- +### [`required-version`](#required-version) {: #required-version } + +Enforce a requirement on the version of uv. + +If the version of uv does not meet the requirement at runtime, uv will exit +with an error. + +Accepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`. + +**Default value**: `null` + +**Type**: `str` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + required-version = ">=0.5.0" + ``` +=== "uv.toml" + + ```toml + required-version = ">=0.5.0" + ``` + +--- + ### [`resolution`](#resolution) {: #resolution } The strategy to use when selecting between the different compatible versions for a given diff --git a/uv.schema.json b/uv.schema.json index de5b6eaae..fcba78967 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -443,6 +443,17 @@ "$ref": "#/definitions/PackageName" } }, + "required-version": { + "description": "Enforce a requirement on the version of uv.\n\nIf the version of uv does not meet the requirement at runtime, uv will exit with an error.\n\nAccepts a [PEP 440](https://peps.python.org/pep-0440/) specifier, like `==0.5.0` or `>=0.5.0`.", + "anyOf": [ + { + "$ref": "#/definitions/RequiredVersion" + }, + { + "type": "null" + } + ] + }, "resolution": { "description": "The strategy to use when selecting between the different compatible versions for a given package requirement.\n\nBy default, uv will use the latest compatible version of each package (`highest`).", "anyOf": [ @@ -1409,6 +1420,10 @@ "type": "string", "pattern": "^3\\.\\d+(\\.\\d+)?$" }, + "RequiredVersion": { + "description": "A version specifier, e.g. `>=0.5.0` or `==0.5.0`.", + "type": "string" + }, "Requirement": { "description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`", "type": "string"