diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index f6e4b1b4a..7897aa245 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -28,8 +28,6 @@ jobs: pattern: wheels_uv-* path: wheels_uv merge-multiple: true - - name: Remove wheels unsupported by PyPI - run: rm wheels_uv/*riscv* - name: Publish to PyPI run: uv publish -v wheels_uv/* @@ -49,7 +47,5 @@ jobs: pattern: wheels_uv_build-* path: wheels_uv_build merge-multiple: true - - name: Remove wheels unsupported by PyPI - run: rm wheels_uv_build/*riscv* - name: Publish to PyPI run: uv publish -v wheels_uv_build/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a9e0af94..ab84b82c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ +## 0.8.3 + +### Python + +- Add CPython 3.14.0rc1 + +See the [`python-build-standalone` release notes](https://github.com/astral-sh/python-build-standalone/releases/tag/20250723) for more details. + +### Enhancements + +- Allow non-standard entrypoint names in `uv_build` ([#14867](https://github.com/astral-sh/uv/pull/14867)) +- Publish riscv64 wheels to PyPI ([#14852](https://github.com/astral-sh/uv/pull/14852)) + +### Bug fixes + +- Avoid writing redacted credentials to tool receipt ([#14855](https://github.com/astral-sh/uv/pull/14855)) +- Respect `--with` versions over base environment versions ([#14863](https://github.com/astral-sh/uv/pull/14863)) +- Respect credentials from all defined indexes ([#14858](https://github.com/astral-sh/uv/pull/14858)) +- Fix missed stabilization of removal of registry entry during Python uninstall ([#14859](https://github.com/astral-sh/uv/pull/14859)) +- Improve concurrency safety of Python downloads into cache ([#14846](https://github.com/astral-sh/uv/pull/14846)) + +### Documentation + +- Fix typos in `uv_build` reference documentation ([#14853](https://github.com/astral-sh/uv/pull/14853)) +- Move the "Cargo" install method further down in docs ([#14842](https://github.com/astral-sh/uv/pull/14842)) + ## 0.8.2 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 659430ac3..ee92de288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -1035,23 +1035,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] @@ -1591,11 +1591,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2994,13 +2994,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -3654,9 +3654,9 @@ checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" [[package]] name = "shellexpand" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" dependencies = [ "bstr", "dirs", @@ -4645,7 +4645,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.8.2" +version = "0.8.3" dependencies = [ "anstream", "anyhow", @@ -4656,7 +4656,7 @@ dependencies = [ "base64 0.22.1", "byteorder", "clap", - "console 0.15.11", + "console 0.16.0", "ctrlc", "dotenvy", "dunce", @@ -4811,7 +4811,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.8.2" +version = "0.8.3" dependencies = [ "anyhow", "uv-build-backend", @@ -5037,6 +5037,7 @@ name = "uv-configuration" version = "0.0.1" dependencies = [ "anyhow", + "bitflags 2.9.1", "clap", "either", "fs-err 3.1.1", @@ -5061,13 +5062,14 @@ dependencies = [ "uv-pep508", "uv-platform-tags", "uv-static", + "uv-warnings", ] [[package]] name = "uv-console" version = "0.0.1" dependencies = [ - "console 0.15.11", + "console 0.16.0", ] [[package]] @@ -5705,7 +5707,7 @@ version = "0.1.0" dependencies = [ "anyhow", "configparser", - "console 0.15.11", + "console 0.16.0", "fs-err 3.1.1", "futures", "rustc-hash", @@ -6006,13 +6008,13 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.8.2" +version = "0.8.3" [[package]] name = "uv-virtualenv" version = "0.0.4" dependencies = [ - "console 0.15.11", + "console 0.16.0", "fs-err 3.1.1", "itertools 0.14.0", "owo-colors", @@ -6340,7 +6342,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e89f786ea..d81535832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,7 +93,7 @@ cargo-util = { version = "0.2.14" } clap = { version = "4.5.17", features = ["derive", "env", "string", "wrap_help"] } clap_complete_command = { version = "0.6.1" } configparser = { version = "3.1.0" } -console = { version = "0.15.11", default-features = false } +console = { version = "0.16.0", default-features = false, features = ["std"] } csv = { version = "1.3.0" } ctrlc = { version = "3.4.5" } dashmap = { version = "6.1.0" } diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 1c2ecfeaf..3c2123cea 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -87,7 +87,7 @@ mod resolver { use uv_client::RegistryClient; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, IndexStrategy, - PackageConfigSettings, PreviewMode, SourceStrategy, + PackageConfigSettings, Preview, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; @@ -198,7 +198,7 @@ mod resolver { sources, workspace_cache, concurrency, - PreviewMode::Enabled, + Preview::default(), ); let markers = if universal { diff --git a/crates/uv-build-backend/src/metadata.rs b/crates/uv-build-backend/src/metadata.rs index d224fd788..9ccb3f6b2 100644 --- a/crates/uv-build-backend/src/metadata.rs +++ b/crates/uv-build-backend/src/metadata.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use itertools::Itertools; use serde::Deserialize; -use tracing::{debug, trace}; +use tracing::{debug, trace, warn}; use version_ranges::Ranges; use walkdir::WalkDir; @@ -54,10 +54,6 @@ pub enum ValidationError { "Entrypoint groups must consist of letters and numbers separated by dots, invalid group: `{0}`" )] InvalidGroup(String), - #[error( - "Entrypoint names must consist of letters, numbers, dots, underscores and dashes; invalid name: `{0}`" - )] - InvalidName(String), #[error("Use `project.scripts` instead of `project.entry-points.console_scripts`")] ReservedScripts, #[error("Use `project.gui-scripts` instead of `project.entry-points.gui_scripts`")] @@ -620,12 +616,14 @@ impl PyProjectToml { let _ = writeln!(writer, "[{group}]"); for (name, object_reference) in entries { - // More strict than the spec, we enforce the recommendation if !name .chars() .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_') { - return Err(ValidationError::InvalidName(name.to_string())); + warn!( + "Entrypoint names should consist of letters, numbers, dots, underscores and \ + dashes; non-compliant name: `{name}`" + ); } // TODO(konsti): Validate that the object references are valid Python identifiers. @@ -1403,16 +1401,6 @@ mod tests { assert_snapshot!(script_error(&contents), @"Entrypoint groups must consist of letters and numbers separated by dots, invalid group: `a@b`"); } - #[test] - fn invalid_entry_point_name() { - let contents = extend_project(indoc! {r#" - [project.scripts] - "a@b" = "bar" - "# - }); - assert_snapshot!(script_error(&contents), @"Entrypoint names must consist of letters, numbers, dots, underscores and dashes; invalid name: `a@b`"); - } - #[test] fn invalid_entry_point_conflict_scripts() { let contents = extend_project(indoc! {r#" diff --git a/crates/uv-build-backend/src/settings.rs b/crates/uv-build-backend/src/settings.rs index 9e9e44961..33e6d8c45 100644 --- a/crates/uv-build-backend/src/settings.rs +++ b/crates/uv-build-backend/src/settings.rs @@ -155,7 +155,7 @@ pub struct BuildBackendSettings { /// with this package as build requirement use the include directory to find additional header /// files. /// - `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended - /// to uses these two options. + /// to use these two options. // TODO(konsti): We should show a flat example instead. // ```toml // [tool.uv.build-backend.data] @@ -165,7 +165,7 @@ pub struct BuildBackendSettings { #[option( default = r#"{}"#, value_type = "dict[str, str]", - example = r#"data = { "headers": "include/headers", "scripts": "bin" }"# + example = r#"data = { headers = "include/headers", scripts = "bin" }"# )] pub data: WheelDataIncludes, } diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 7f112897b..092fcec5c 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -29,7 +29,7 @@ use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, warn}; use uv_cache_key::cache_digest; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_configuration::{BuildKind, BuildOutput, ConfigSettings, SourceStrategy}; use uv_distribution::BuildRequires; use uv_distribution_types::{IndexLocations, Requirement, Resolution}; @@ -289,7 +289,7 @@ impl SourceBuild { mut environment_variables: FxHashMap, level: BuildOutput, concurrent_builds: usize, - preview: PreviewMode, + preview: Preview, ) -> Result { let temp_dir = build_context.cache().venv_dir()?; diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 063939748..3e80beb1f 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.8.2" +version = "0.8.3" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index fcf06ed05..970950ebc 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.8.2" +version = "0.8.3" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index b6a41f3e7..1b41050b2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -11,8 +11,8 @@ use clap::{Args, Parser, Subcommand}; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ConfigSettingPackageEntry, ExportFormat, IndexStrategy, - KeyringProviderType, PackageNameSpecifier, ProjectBuildBackend, TargetTriple, TrustedHost, - TrustedPublishing, VersionControlSystem, + KeyringProviderType, PackageNameSpecifier, PreviewFeatures, ProjectBuildBackend, TargetTriple, + TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex}; use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName}; @@ -273,7 +273,7 @@ pub struct GlobalArgs { )] pub allow_insecure_host: Option>>, - /// Whether to enable experimental, preview features. + /// Whether to enable all experimental preview features. /// /// Preview features may change without warning. #[arg(global = true, long, hide = true, env = EnvVars::UV_PREVIEW, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))] @@ -282,6 +282,25 @@ pub struct GlobalArgs { #[arg(global = true, long, overrides_with("preview"), hide = true)] pub no_preview: bool, + /// Enable experimental preview features. + /// + /// Preview features may change without warning. + /// + /// Use comma-separated values or pass multiple times to enable multiple features. + /// + /// The following features are available: `python-install-default`, `python-upgrade`, + /// `json-output`, `pylock`, `add-bounds`. + #[arg( + global = true, + long = "preview-features", + env = EnvVars::UV_PREVIEW_FEATURES, + value_delimiter = ',', + hide = true, + alias = "preview-feature", + value_enum, + )] + pub preview_features: Vec, + /// Avoid discovering a `pyproject.toml` or `uv.toml` file. /// /// Normally, configuration files are discovered in the current directory, diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 1c195ab3d..807340a01 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -27,7 +27,9 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true, features = ["schemars"] } uv-platform-tags = { workspace = true } uv-static = { workspace = true } +uv-warnings = { workspace = true } +bitflags = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } either = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-configuration/src/preview.rs b/crates/uv-configuration/src/preview.rs index 38572589b..fab7dd34e 100644 --- a/crates/uv-configuration/src/preview.rs +++ b/crates/uv-configuration/src/preview.rs @@ -1,37 +1,250 @@ -use std::fmt::{Display, Formatter}; +use std::{ + fmt::{Display, Formatter}, + str::FromStr, +}; + +use thiserror::Error; +use uv_warnings::warn_user_once; + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct PreviewFeatures: u32 { + const PYTHON_INSTALL_DEFAULT = 1 << 0; + const PYTHON_UPGRADE = 1 << 1; + const JSON_OUTPUT = 1 << 2; + const PYLOCK = 1 << 3; + const ADD_BOUNDS = 1 << 4; + const EXTRA_BUILD_DEPENDENCIES = 1 << 5; + } +} + +impl PreviewFeatures { + /// Returns the string representation of a single preview feature flag. + /// + /// Panics if given a combination of flags. + fn flag_as_str(self) -> &'static str { + match self { + Self::PYTHON_INSTALL_DEFAULT => "python-install-default", + Self::PYTHON_UPGRADE => "python-upgrade", + Self::JSON_OUTPUT => "json-output", + Self::PYLOCK => "pylock", + Self::ADD_BOUNDS => "add-bounds", + Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies", + _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), + } + } +} + +impl Display for PreviewFeatures { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.is_empty() { + write!(f, "none") + } else { + let features: Vec<&str> = self.iter().map(PreviewFeatures::flag_as_str).collect(); + write!(f, "{}", features.join(",")) + } + } +} + +#[derive(Debug, Error, Clone)] +pub enum PreviewFeaturesParseError { + #[error("Empty string in preview features: {0}")] + Empty(String), +} + +impl FromStr for PreviewFeatures { + type Err = PreviewFeaturesParseError; + + fn from_str(s: &str) -> Result { + let mut flags = PreviewFeatures::empty(); + + for part in s.split(',') { + let part = part.trim(); + if part.is_empty() { + return Err(PreviewFeaturesParseError::Empty( + "Empty string in preview features".to_string(), + )); + } + + let flag = match part { + "python-install-default" => Self::PYTHON_INSTALL_DEFAULT, + "python-upgrade" => Self::PYTHON_UPGRADE, + "json-output" => Self::JSON_OUTPUT, + "pylock" => Self::PYLOCK, + "add-bounds" => Self::ADD_BOUNDS, + "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES, + _ => { + warn_user_once!("Unknown preview feature: `{part}`"); + continue; + } + }; + + flags |= flag; + } + + Ok(flags) + } +} #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum PreviewMode { - #[default] - Disabled, - Enabled, +pub struct Preview { + flags: PreviewFeatures, } -impl PreviewMode { - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Enabled) +impl Preview { + pub fn new(flags: PreviewFeatures) -> Self { + Self { flags } } - pub fn is_disabled(&self) -> bool { - matches!(self, Self::Disabled) + pub fn all() -> Self { + Self::new(PreviewFeatures::all()) } -} -impl From for PreviewMode { - fn from(version: bool) -> Self { - if version { - PreviewMode::Enabled - } else { - PreviewMode::Disabled + pub fn from_args( + preview: bool, + no_preview: bool, + preview_features: &[PreviewFeatures], + ) -> Self { + if no_preview { + return Self::default(); } + + if preview { + return Self::all(); + } + + let mut flags = PreviewFeatures::empty(); + + for features in preview_features { + flags |= *features; + } + + Self { flags } + } + + pub fn is_enabled(&self, flag: PreviewFeatures) -> bool { + self.flags.contains(flag) } } -impl Display for PreviewMode { +impl Display for Preview { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::Disabled => write!(f, "disabled"), - Self::Enabled => write!(f, "enabled"), + if self.flags.is_empty() { + write!(f, "disabled") + } else if self.flags == PreviewFeatures::all() { + write!(f, "enabled") + } else { + write!(f, "{}", self.flags) } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_preview_features_from_str() { + // Test single feature + let features = PreviewFeatures::from_str("python-install-default").unwrap(); + assert_eq!(features, PreviewFeatures::PYTHON_INSTALL_DEFAULT); + + // Test multiple features + let features = PreviewFeatures::from_str("python-upgrade,json-output").unwrap(); + assert!(features.contains(PreviewFeatures::PYTHON_UPGRADE)); + assert!(features.contains(PreviewFeatures::JSON_OUTPUT)); + assert!(!features.contains(PreviewFeatures::PYLOCK)); + + // Test with whitespace + let features = PreviewFeatures::from_str("pylock , add-bounds").unwrap(); + assert!(features.contains(PreviewFeatures::PYLOCK)); + assert!(features.contains(PreviewFeatures::ADD_BOUNDS)); + + // Test empty string error + assert!(PreviewFeatures::from_str("").is_err()); + assert!(PreviewFeatures::from_str("pylock,").is_err()); + assert!(PreviewFeatures::from_str(",pylock").is_err()); + + // Test unknown feature (should be ignored with warning) + let features = PreviewFeatures::from_str("unknown-feature,pylock").unwrap(); + assert!(features.contains(PreviewFeatures::PYLOCK)); + assert_eq!(features.bits().count_ones(), 1); + } + + #[test] + fn test_preview_features_display() { + // Test empty + let features = PreviewFeatures::empty(); + assert_eq!(features.to_string(), "none"); + + // Test single feature + let features = PreviewFeatures::PYTHON_INSTALL_DEFAULT; + assert_eq!(features.to_string(), "python-install-default"); + + // Test multiple features + let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT; + assert_eq!(features.to_string(), "python-upgrade,json-output"); + } + + #[test] + fn test_preview_display() { + // Test disabled + let preview = Preview::default(); + assert_eq!(preview.to_string(), "disabled"); + + // Test enabled (all features) + let preview = Preview::all(); + assert_eq!(preview.to_string(), "enabled"); + + // Test specific features + let preview = Preview::new(PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::PYLOCK); + assert_eq!(preview.to_string(), "python-upgrade,pylock"); + } + + #[test] + fn test_preview_from_args() { + // Test no_preview + let preview = Preview::from_args(true, true, &[]); + assert_eq!(preview.to_string(), "disabled"); + + // Test preview (all features) + let preview = Preview::from_args(true, false, &[]); + assert_eq!(preview.to_string(), "enabled"); + + // Test specific features + let features = vec![ + PreviewFeatures::PYTHON_UPGRADE, + PreviewFeatures::JSON_OUTPUT, + ]; + let preview = Preview::from_args(false, false, &features); + assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE)); + assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT)); + assert!(!preview.is_enabled(PreviewFeatures::PYLOCK)); + } + + #[test] + fn test_as_str_single_flags() { + assert_eq!( + PreviewFeatures::PYTHON_INSTALL_DEFAULT.flag_as_str(), + "python-install-default" + ); + assert_eq!( + PreviewFeatures::PYTHON_UPGRADE.flag_as_str(), + "python-upgrade" + ); + assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output"); + assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock"); + assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds"); + assert_eq!( + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(), + "extra-build-dependencies" + ); + } + + #[test] + #[should_panic(expected = "`flag_as_str` can only be used for exactly one feature flag")] + fn test_as_str_multiple_flags_panics() { + let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT; + let _ = features.flag_as_str(); + } +} diff --git a/crates/uv-dev/src/compile.rs b/crates/uv-dev/src/compile.rs index d2b685b23..0e1392f63 100644 --- a/crates/uv-dev/src/compile.rs +++ b/crates/uv-dev/src/compile.rs @@ -4,7 +4,7 @@ use clap::Parser; use tracing::info; use uv_cache::{Cache, CacheArgs}; -use uv_configuration::{Concurrency, PreviewMode}; +use uv_configuration::{Concurrency, Preview}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; #[derive(Parser)] @@ -26,7 +26,7 @@ pub(crate) async fn compile(args: CompileArgs) -> anyhow::Result<()> { &PythonRequest::default(), EnvironmentPreference::OnlyVirtual, &cache, - PreviewMode::Disabled, + Preview::default(), )? .into_interpreter(); interpreter.sys_executable().to_path_buf() diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 0af691b6e..83084633b 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -18,7 +18,7 @@ use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ BuildKind, BuildOptions, ConfigSettings, Constraints, IndexStrategy, PackageConfigSettings, - PreviewMode, Reinstall, SourceStrategy, + Preview, Reinstall, SourceStrategy, }; use uv_configuration::{BuildOutput, Concurrency}; use uv_distribution::DistributionDatabase; @@ -101,7 +101,7 @@ pub struct BuildDispatch<'a> { sources: SourceStrategy, workspace_cache: WorkspaceCache, concurrency: Concurrency, - preview: PreviewMode, + preview: Preview, } impl<'a> BuildDispatch<'a> { @@ -126,7 +126,7 @@ impl<'a> BuildDispatch<'a> { sources: SourceStrategy, workspace_cache: WorkspaceCache, concurrency: Concurrency, - preview: PreviewMode, + preview: Preview, ) -> Self { Self { client, diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index 6baca1c1f..a75f1ea1b 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -203,7 +203,11 @@ impl serde::ser::Serialize for IndexUrl { where S: serde::ser::Serializer, { - self.to_string().serialize(serializer) + match self { + Self::Pypi(url) => url.without_credentials().serialize(serializer), + Self::Url(url) => url.without_credentials().serialize(serializer), + Self::Path(url) => url.without_credentials().serialize(serializer), + } } } @@ -396,14 +400,17 @@ impl<'a> IndexLocations { /// /// This includes explicit indexes, implicit indexes, flat indexes, and the default index. /// - /// The indexes will be returned in the order in which they were defined, such that the - /// last-defined index is the last item in the vector. + /// The indexes will be returned in the reverse of the order in which they were defined, such + /// that the last-defined index is the first item in the vector. pub fn allowed_indexes(&'a self) -> Vec<&'a Index> { if self.no_index { self.flat_index.iter().rev().collect() } else { let mut indexes = vec![]; + // TODO(charlie): By only yielding the first default URL, we'll drop credentials if, + // e.g., an authenticated default URL is provided in a configuration file, but an + // unauthenticated default URL is present in the receipt. let mut seen = FxHashSet::default(); let mut default = false; for index in { @@ -429,9 +436,29 @@ impl<'a> IndexLocations { } } + /// Return a vector containing all known [`Index`] entries. + /// + /// This includes explicit indexes, implicit indexes, flat indexes, and default indexes; + /// in short, it includes all defined indexes, even if they're overridden by some other index + /// definition. + /// + /// The indexes will be returned in the reverse of the order in which they were defined, such + /// that the last-defined index is the first item in the vector. + pub fn known_indexes(&'a self) -> impl Iterator { + if self.no_index { + Either::Left(self.flat_index.iter().rev()) + } else { + Either::Right( + std::iter::once(&*DEFAULT_INDEX) + .chain(self.flat_index.iter().rev()) + .chain(self.indexes.iter().rev()), + ) + } + } + /// Add all authenticated sources to the cache. pub fn cache_index_credentials(&self) { - for index in self.allowed_indexes() { + for index in self.known_indexes() { if let Some(credentials) = index.credentials() { let credentials = Arc::new(credentials); uv_auth::store_credentials(index.raw_url(), credentials.clone()); diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index 52ac2fbd1..5046eea21 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -91,13 +91,21 @@ impl CompatibleDist<'_> { } } + // For installable distributions, return the prioritized distribution it was derived from. + pub fn prioritized(&self) -> Option<&PrioritizedDist> { + match self { + CompatibleDist::InstalledDist(_) => None, + CompatibleDist::SourceDist { prioritized, .. } + | CompatibleDist::CompatibleWheel { prioritized, .. } + | CompatibleDist::IncompatibleWheel { prioritized, .. } => Some(prioritized), + } + } + /// Return the set of supported platform the distribution, in terms of their markers. pub fn implied_markers(&self) -> MarkerTree { - match self { - CompatibleDist::InstalledDist(_) => MarkerTree::TRUE, - CompatibleDist::SourceDist { prioritized, .. } => prioritized.0.markers, - CompatibleDist::CompatibleWheel { prioritized, .. } => prioritized.0.markers, - CompatibleDist::IncompatibleWheel { prioritized, .. } => prioritized.0.markers, + match self.prioritized() { + Some(prioritized) => prioritized.0.markers, + None => MarkerTree::TRUE, } } } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index f10b480e2..466ea4b0f 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -8,7 +8,7 @@ use std::{env, io, iter}; use std::{path::Path, path::PathBuf, str::FromStr}; use thiserror::Error; use tracing::{debug, instrument, trace}; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use which::{which, which_all}; use uv_cache::Cache; @@ -335,7 +335,7 @@ fn python_executables_from_installed<'a>( implementation: Option<&'a ImplementationName>, platform: PlatformRequest, preference: PythonPreference, - preview: PreviewMode, + preview: Preview, ) -> Box> + 'a> { let from_managed_installations = iter::once_with(move || { ManagedPythonInstallations::from_settings(None) @@ -485,7 +485,7 @@ fn python_executables<'a>( platform: PlatformRequest, environments: EnvironmentPreference, preference: PythonPreference, - preview: PreviewMode, + preview: Preview, ) -> Box> + 'a> { // Always read from `UV_INTERNAL__PARENT_INTERPRETER` — it could be a system interpreter let from_parent_interpreter = iter::once_with(|| { @@ -705,7 +705,7 @@ fn python_interpreters<'a>( environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, - preview: PreviewMode, + preview: Preview, ) -> impl Iterator> + 'a { python_interpreters_from_executables( // Perform filtering on the discovered executables based on their source. This avoids @@ -1053,7 +1053,7 @@ pub fn find_python_installations<'a>( environments: EnvironmentPreference, preference: PythonPreference, cache: &'a Cache, - preview: PreviewMode, + preview: Preview, ) -> Box> + 'a> { let sources = DiscoveryPreferences { python_preference: preference, @@ -1254,7 +1254,7 @@ pub(crate) fn find_python_installation( environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> Result { let installations = find_python_installations(request, environments, preference, cache, preview); @@ -1353,7 +1353,7 @@ pub(crate) fn find_best_python_installation( environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> Result { debug!("Starting Python discovery for {}", request); diff --git a/crates/uv-python/src/environment.rs b/crates/uv-python/src/environment.rs index 10cec16ad..f82319074 100644 --- a/crates/uv-python/src/environment.rs +++ b/crates/uv-python/src/environment.rs @@ -7,7 +7,7 @@ use owo_colors::OwoColorize; use tracing::debug; use uv_cache::Cache; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_fs::{LockedFile, Simplified}; use uv_pep440::Version; @@ -153,7 +153,7 @@ impl PythonEnvironment { request: &PythonRequest, preference: EnvironmentPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> Result { let installation = match find_python_installation( request, diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index a5dbb55f2..3f5b506a6 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -8,7 +8,7 @@ use tracing::{debug, info}; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_pep440::{Prerelease, Version}; use crate::discovery::{ @@ -58,7 +58,7 @@ impl PythonInstallation { environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> Result { let installation = find_python_installation(request, environments, preference, cache, preview)??; @@ -72,7 +72,7 @@ impl PythonInstallation { environments: EnvironmentPreference, preference: PythonPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> Result { Ok(find_best_python_installation( request, @@ -97,7 +97,7 @@ impl PythonInstallation { python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, python_downloads_json_url: Option<&str>, - preview: PreviewMode, + preview: Preview, ) -> Result { let request = request.unwrap_or(&PythonRequest::Default); @@ -220,7 +220,7 @@ impl PythonInstallation { reporter: Option<&dyn Reporter>, python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, - preview: PreviewMode, + preview: Preview, ) -> Result { let installations = ManagedPythonInstallations::from_settings(None)?.init()?; let installations_dir = installations.root(); diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 2461f9006..8b8e9c129 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -135,7 +135,7 @@ mod tests { use indoc::{formatdoc, indoc}; use temp_env::with_vars; use test_log::test; - use uv_configuration::PreviewMode; + use uv_configuration::Preview; use uv_static::EnvVars; use uv_cache::Cache; @@ -468,7 +468,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }); assert!( @@ -483,7 +483,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }); assert!( @@ -508,7 +508,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }); assert!( @@ -530,7 +530,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert!( @@ -592,7 +592,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert!( @@ -624,7 +624,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }); assert!( @@ -661,7 +661,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::default(), &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert!( @@ -693,7 +693,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -715,7 +715,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -741,7 +741,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -767,7 +767,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -790,7 +790,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -824,7 +824,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -858,7 +858,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -880,7 +880,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -902,7 +902,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -936,7 +936,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -973,7 +973,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert!( @@ -1004,7 +1004,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert!( @@ -1039,7 +1039,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1065,7 +1065,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1092,7 +1092,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1117,7 +1117,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )?; @@ -1139,7 +1139,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1162,7 +1162,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1195,7 +1195,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1216,7 +1216,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1243,7 +1243,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -1261,7 +1261,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -1290,7 +1290,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1328,7 +1328,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1356,7 +1356,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1381,7 +1381,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1406,7 +1406,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1431,7 +1431,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1469,7 +1469,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1497,7 +1497,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1514,7 +1514,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1531,7 +1531,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1553,7 +1553,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1570,7 +1570,7 @@ mod tests { EnvironmentPreference::OnlySystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )?; @@ -1592,7 +1592,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1607,7 +1607,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1621,7 +1621,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1650,7 +1650,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1666,7 +1666,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1696,7 +1696,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1712,7 +1712,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1728,7 +1728,7 @@ mod tests { EnvironmentPreference::OnlyVirtual, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1744,7 +1744,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1768,7 +1768,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1783,7 +1783,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1807,7 +1807,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1827,7 +1827,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }, )??; @@ -1856,7 +1856,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1878,7 +1878,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1908,7 +1908,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -1924,7 +1924,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1951,7 +1951,7 @@ mod tests { EnvironmentPreference::ExplicitSystem, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -1976,7 +1976,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -1993,7 +1993,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2008,7 +2008,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2034,7 +2034,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2049,7 +2049,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2075,7 +2075,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2102,7 +2102,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2129,7 +2129,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2156,7 +2156,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2183,7 +2183,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2211,7 +2211,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })?; assert!( @@ -2233,7 +2233,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2248,7 +2248,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2274,7 +2274,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2289,7 +2289,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; assert_eq!( @@ -2327,7 +2327,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2345,7 +2345,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2387,7 +2387,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2405,7 +2405,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2442,7 +2442,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2465,7 +2465,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2488,7 +2488,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) }) .unwrap() @@ -2527,7 +2527,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; @@ -2580,7 +2580,7 @@ mod tests { EnvironmentPreference::Any, PythonPreference::OnlySystem, &context.cache, - PreviewMode::Disabled, + Preview::default(), ) })??; diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 9ee72adda..d9b96e5ed 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -12,7 +12,7 @@ use itertools::Itertools; use same_file::is_same_file; use thiserror::Error; use tracing::{debug, warn}; -use uv_configuration::PreviewMode; +use uv_configuration::{Preview, PreviewFeatures}; #[cfg(windows)] use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; @@ -519,7 +519,7 @@ impl ManagedPythonInstallation { /// Ensure the environment contains the symlink directory (or junction on Windows) /// pointing to the patch directory for this minor version. - pub fn ensure_minor_version_link(&self, preview: PreviewMode) -> Result<(), Error> { + pub fn ensure_minor_version_link(&self, preview: Preview) -> Result<(), Error> { if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { minor_version_link.create_directory()?; } @@ -531,7 +531,7 @@ impl ManagedPythonInstallation { /// /// Unlike [`ensure_minor_version_link`], will not create a new symlink directory /// if one doesn't already exist, - pub fn update_minor_version_link(&self, preview: PreviewMode) -> Result<(), Error> { + pub fn update_minor_version_link(&self, preview: Preview) -> Result<(), Error> { if let Some(minor_version_link) = PythonMinorVersionLink::from_installation(self, preview) { if !minor_version_link.exists() { return Ok(()); @@ -702,7 +702,7 @@ impl PythonMinorVersionLink { pub fn from_executable( executable: &Path, key: &PythonInstallationKey, - preview: PreviewMode, + preview: Preview, ) -> Option { let implementation = key.implementation(); if !matches!( @@ -755,7 +755,7 @@ impl PythonMinorVersionLink { // If preview mode is disabled, still return a `MinorVersionSymlink` for // existing symlinks, allowing continued operations without the `--preview` // flag after initial symlink directory installation. - if preview.is_disabled() && !minor_version_link.exists() { + if !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) && !minor_version_link.exists() { return None; } Some(minor_version_link) @@ -763,7 +763,7 @@ impl PythonMinorVersionLink { pub fn from_installation( installation: &ManagedPythonInstallation, - preview: PreviewMode, + preview: Preview, ) -> Option { PythonMinorVersionLink::from_executable( installation.executable(false).as_path(), diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs index 5c9a8e278..a0534c46d 100644 --- a/crates/uv-redacted/src/lib.rs +++ b/crates/uv-redacted/src/lib.rs @@ -1,5 +1,6 @@ use ref_cast::RefCast; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; use std::str::FromStr; @@ -98,6 +99,24 @@ impl DisplaySafeUrl { let _ = self.0.set_password(None); } + /// Returns the URL with any credentials removed. + pub fn without_credentials(&self) -> Cow<'_, Url> { + if self.0.password().is_none() && self.0.username() == "" { + return Cow::Borrowed(&self.0); + } + + // For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the + // username. + if is_ssh_git_username(&self.0) { + return Cow::Borrowed(&self.0); + } + + let mut url = self.0.clone(); + let _ = url.set_username(""); + let _ = url.set_password(None); + Cow::Owned(url) + } + /// Returns [`Display`] implementation that doesn't mask credentials. #[inline] pub fn displayable_with_credentials(&self) -> impl Display { diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index e03302966..57776bda4 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -266,7 +266,6 @@ impl CandidateSelector { return Some(Candidate { name: package_name, version, - prioritized: None, dist: CandidateDist::Compatible(CompatibleDist::InstalledDist( dist, )), @@ -368,7 +367,6 @@ impl CandidateSelector { return Some(Candidate { name: package_name, version, - prioritized: None, dist: CandidateDist::Compatible(CompatibleDist::InstalledDist(dist)), choice_kind: VersionChoiceKind::Installed, }); @@ -546,10 +544,14 @@ impl CandidateSelector { // exclude-newer in our error messages. if matches!( candidate.dist(), - CandidateDist::Incompatible( - IncompatibleDist::Source(IncompatibleSource::ExcludeNewer(_)) - | IncompatibleDist::Wheel(IncompatibleWheel::ExcludeNewer(_)) - ) + CandidateDist::Incompatible { + incompatible_dist: IncompatibleDist::Source(IncompatibleSource::ExcludeNewer( + _ + )) | IncompatibleDist::Wheel( + IncompatibleWheel::ExcludeNewer(_) + ), + .. + } ) { continue; } @@ -572,7 +574,7 @@ impl CandidateSelector { // even though there are compatible wheels on PyPI. Thus, we need to ensure that we // return the first _compatible_ candidate across all indexes, if such a candidate // exists. - if matches!(candidate.dist(), CandidateDist::Incompatible(_)) { + if matches!(candidate.dist(), CandidateDist::Incompatible { .. }) { if incompatible.is_none() { incompatible = Some(candidate); } @@ -602,7 +604,25 @@ impl CandidateSelector { #[derive(Debug, Clone)] pub(crate) enum CandidateDist<'a> { Compatible(CompatibleDist<'a>), - Incompatible(IncompatibleDist), + Incompatible { + /// The reason the prioritized distribution is incompatible. + incompatible_dist: IncompatibleDist, + /// The prioritized distribution that had no compatible wheelr or sdist. + prioritized_dist: &'a PrioritizedDist, + }, +} + +impl CandidateDist<'_> { + /// For an installable dist, return the prioritized distribution. + fn prioritized(&self) -> Option<&PrioritizedDist> { + match self { + CandidateDist::Compatible(dist) => dist.prioritized(), + CandidateDist::Incompatible { + incompatible_dist: _, + prioritized_dist: prioritized, + } => Some(prioritized), + } + } } impl<'a> From<&'a PrioritizedDist> for CandidateDist<'a> { @@ -621,7 +641,10 @@ impl<'a> From<&'a PrioritizedDist> for CandidateDist<'a> { } else { IncompatibleDist::Unavailable }; - CandidateDist::Incompatible(dist) + CandidateDist::Incompatible { + incompatible_dist: dist, + prioritized_dist: value, + } } } } @@ -654,8 +677,6 @@ pub(crate) struct Candidate<'a> { name: &'a PackageName, /// The version of the package. version: &'a Version, - /// The prioritized distribution for the package. - prioritized: Option<&'a PrioritizedDist>, /// The distributions to use for resolving and installing the package. dist: CandidateDist<'a>, /// Whether this candidate was selected from a preference. @@ -672,7 +693,6 @@ impl<'a> Candidate<'a> { Self { name, version, - prioritized: Some(dist), dist: CandidateDist::from(dist), choice_kind, } @@ -709,7 +729,7 @@ impl<'a> Candidate<'a> { /// Return the prioritized distribution for the candidate. pub(crate) fn prioritized(&self) -> Option<&PrioritizedDist> { - self.prioritized + self.dist.prioritized() } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 49cb851b3..2e3ee56d4 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1431,7 +1431,7 @@ impl Lock { } // Collect the set of available indexes (both `--index-url` and `--find-links` entries). - let remotes = indexes.map(|locations| { + let mut remotes = indexes.map(|locations| { locations .allowed_indexes() .into_iter() @@ -1444,7 +1444,7 @@ impl Lock { .collect::>() }); - let locals = indexes.map(|locations| { + let mut locals = indexes.map(|locations| { locations .allowed_indexes() .into_iter() @@ -1717,6 +1717,38 @@ impl Lock { return Ok(SatisfiesResult::MissingVersion(&package.id.name)); } + // Add any explicit indexes to the list of known locals or remotes. These indexes may + // not be available as top-level configuration (i.e., if they're defined within a + // workspace member), but we already validated that the dependencies are up-to-date, so + // we can consider them "available". + for requirement in &package.metadata.requires_dist { + if let RequirementSource::Registry { + index: Some(index), .. + } = &requirement.source + { + match &index.url { + IndexUrl::Pypi(_) | IndexUrl::Url(_) => { + if let Some(remotes) = remotes.as_mut() { + remotes.insert(UrlString::from( + index.url().without_credentials().as_ref(), + )); + } + } + IndexUrl::Path(url) => { + if let Some(locals) = locals.as_mut() { + if let Some(path) = url.to_file_path().ok().and_then(|path| { + relative_to(&path, root) + .or_else(|_| std::path::absolute(path)) + .ok() + }) { + locals.insert(path.into_boxed_path()); + } + } + } + } + } + } + // Recurse. for dep in &package.dependencies { if seen.insert(&dep.package_id) { diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index fb4092099..6af21702a 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1271,7 +1271,10 @@ impl ResolverState dist, - CandidateDist::Incompatible(incompatibility) => { + CandidateDist::Incompatible { + incompatible_dist: incompatibility, + prioritized_dist: _, + } => { // If the version is incompatible because no distributions are compatible, exit early. return Ok(Some(ResolverVersion::Unavailable( candidate.version().clone(), @@ -3779,9 +3782,7 @@ impl Fork { if self.env.included_by_group(conflicting_item) { return true; } - if let Some(conflicting_item) = dep.package.conflicting_item() { - self.conflicts.remove(&conflicting_item); - } + self.conflicts.remove(&conflicting_item); false }); Some(self) diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index a18bc11a8..b8adf225f 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -225,6 +225,9 @@ impl EnvVars { /// Equivalent to the `--preview` argument. Enables preview mode. pub const UV_PREVIEW: &'static str = "UV_PREVIEW"; + /// Equivalent to the `--preview-features` argument. Enables specific preview features. + pub const UV_PREVIEW_FEATURES: &'static str = "UV_PREVIEW_FEATURES"; + /// Equivalent to the `--token` argument for self update. A GitHub token for authentication. pub const UV_GITHUB_TOKEN: &'static str = "UV_GITHUB_TOKEN"; diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 4afc83bcb..902dbf2d0 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -1,7 +1,7 @@ use core::fmt; use fs_err as fs; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_dirs::user_executable_directory; use uv_pep440::Version; use uv_pep508::{InvalidNameError, PackageName}; @@ -258,7 +258,7 @@ impl InstalledTools { &self, name: &PackageName, interpreter: Interpreter, - preview: PreviewMode, + preview: Preview, ) -> Result { let environment_path = self.tool_dir(name); diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 3afc69a98..9fa7125e1 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.8.2" +version = "0.8.3" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index bcf1e9f97..7c682627b 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -3,7 +3,7 @@ use std::path::Path; use thiserror::Error; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_python::{Interpreter, PythonEnvironment}; pub use virtualenv::{OnExisting, remove_virtualenv}; @@ -56,7 +56,7 @@ pub fn create_venv( relocatable: bool, seed: bool, upgradeable: bool, - preview: PreviewMode, + preview: Preview, ) -> Result { // Create the virtualenv at the given location. let virtualenv = virtualenv::create( diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 5fa77034e..7c65ec1bf 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -12,7 +12,7 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, trace}; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_fs::{CWD, Simplified, cachedir}; use uv_pypi_types::Scheme; use uv_python::managed::{PythonMinorVersionLink, create_link_to_executable}; @@ -59,7 +59,7 @@ pub(crate) fn create( relocatable: bool, seed: bool, upgradeable: bool, - preview: PreviewMode, + preview: Preview, ) -> Result { // Determine the base Python executable; that is, the Python executable that should be // considered the "base" for the virtual environment. diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 272b5ae87..ec8cf18bd 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.8.2" +version = "0.8.3" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index bd39bad18..d5d5df635 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -16,7 +16,7 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildKind, BuildOptions, BuildOutput, Concurrency, ConfigSettings, Constraints, DependencyGroupsWithDefaults, HashCheckingMode, IndexStrategy, KeyringProviderType, - PackageConfigSettings, PreviewMode, SourceStrategy, + PackageConfigSettings, Preview, SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_filename::{ @@ -118,7 +118,7 @@ pub(crate) async fn build_frontend( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let build_result = build_impl( project_dir, @@ -186,7 +186,7 @@ async fn build_impl( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Extract the resolver settings. let ResolverSettings { @@ -441,7 +441,7 @@ async fn build_package( link_mode: LinkMode, config_setting: &ConfigSettings, config_settings_package: &PackageConfigSettings, - preview: PreviewMode, + preview: Preview, ) -> Result, Error> { let output_dir = if let Some(output_dir) = output_dir { Cow::Owned(std::path::absolute(output_dir)?) diff --git a/crates/uv/src/commands/pip/check.rs b/crates/uv/src/commands/pip/check.rs index bfbb20ee6..cf5ab350b 100644 --- a/crates/uv/src/commands/pip/check.rs +++ b/crates/uv/src/commands/pip/check.rs @@ -5,7 +5,7 @@ use anyhow::Result; use owo_colors::OwoColorize; use uv_cache::Cache; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_distribution_types::{Diagnostic, InstalledDist}; use uv_installer::{SitePackages, SitePackagesDiagnostic}; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; @@ -20,7 +20,7 @@ pub(crate) fn pip_check( system: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let start = Instant::now(); diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 7dbaaf428..9f8b3f7fa 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -14,7 +14,7 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, ExportFormat, ExtrasSpecification, - IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, Reinstall, + IndexStrategy, NoBinary, NoBuild, PackageConfigSettings, Preview, PreviewFeatures, Reinstall, SourceStrategy, Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; @@ -112,11 +112,14 @@ pub(crate) async fn pip_compile( quiet: bool, cache: Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { - if preview.is_disabled() && !extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index 8c8491d45..6f663e03b 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use owo_colors::OwoColorize; use uv_cache::Cache; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_distribution_types::{Diagnostic, InstalledDist, Name}; use uv_installer::SitePackages; use uv_python::{EnvironmentPreference, PythonEnvironment, PythonRequest}; @@ -24,7 +24,7 @@ pub(crate) fn pip_freeze( paths: Option>, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Detect the current Python interpreter. let environment = PythonEnvironment::find( diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index f202f3ab0..58f172351 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -10,8 +10,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification, - HashCheckingMode, IndexStrategy, PackageConfigSettings, PreviewMode, Reinstall, SourceStrategy, - Upgrade, + HashCheckingMode, IndexStrategy, PackageConfigSettings, Preview, PreviewFeatures, Reinstall, + SourceStrategy, Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -97,13 +97,16 @@ pub(crate) async fn pip_install( cache: Cache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> anyhow::Result { let start = std::time::Instant::now(); - if preview.is_disabled() && !extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } @@ -141,9 +144,10 @@ pub(crate) async fn pip_install( .await?; if pylock.is_some() { - if preview.is_disabled() { + if !preview.is_enabled(PreviewFeatures::PYLOCK) { warn_user!( - "The `--pylock` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `--pylock` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::PYLOCK ); } } diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 40e8c770d..9205268ba 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -15,7 +15,7 @@ use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_cli::ListFormat; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; -use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, PreviewMode}; +use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, Preview}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name, RequiresPython, @@ -54,7 +54,7 @@ pub(crate) async fn pip_list( system: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Disallow `--outdated` with `--format freeze`. if outdated && matches!(format, ListFormat::Freeze) { diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs index 4d2b3c3a7..f7a46ea7a 100644 --- a/crates/uv/src/commands/pip/show.rs +++ b/crates/uv/src/commands/pip/show.rs @@ -7,7 +7,7 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use uv_cache::Cache; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_distribution_types::{Diagnostic, Name}; use uv_fs::Simplified; use uv_install_wheel::read_record_file; @@ -28,7 +28,7 @@ pub(crate) fn pip_show( files: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { if packages.is_empty() { #[allow(clippy::print_stderr)] diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index be1161c8e..f179c583d 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -9,8 +9,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DryRun, ExtrasSpecification, - HashCheckingMode, IndexStrategy, PackageConfigSettings, PreviewMode, Reinstall, SourceStrategy, - Upgrade, + HashCheckingMode, IndexStrategy, PackageConfigSettings, Preview, PreviewFeatures, Reinstall, + SourceStrategy, Upgrade, }; use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_dispatch::{BuildDispatch, SharedState}; @@ -85,11 +85,14 @@ pub(crate) async fn pip_sync( cache: Cache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { - if preview.is_disabled() && !extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } @@ -134,9 +137,10 @@ pub(crate) async fn pip_sync( .await?; if pylock.is_some() { - if preview.is_disabled() { + if !preview.is_enabled(PreviewFeatures::PYLOCK) { warn_user!( - "The `--pylock` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `--pylock` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::PYLOCK ); } } diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index 81a566b8e..8b0aa0c3a 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -13,7 +13,7 @@ use tokio::sync::Semaphore; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; -use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, PreviewMode}; +use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType, Preview}; use uv_distribution_types::{Diagnostic, IndexCapabilities, IndexLocations, Name, RequiresPython}; use uv_installer::SitePackages; use uv_normalize::PackageName; @@ -52,7 +52,7 @@ pub(crate) async fn pip_tree( system: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Detect the current Python interpreter. let environment = PythonEnvironment::find( diff --git a/crates/uv/src/commands/pip/uninstall.rs b/crates/uv/src/commands/pip/uninstall.rs index f617a0203..af7dfc8f3 100644 --- a/crates/uv/src/commands/pip/uninstall.rs +++ b/crates/uv/src/commands/pip/uninstall.rs @@ -7,7 +7,7 @@ use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::{DryRun, KeyringProviderType, PreviewMode}; +use uv_configuration::{DryRun, KeyringProviderType, Preview}; use uv_distribution_types::Requirement; use uv_distribution_types::{InstalledMetadata, Name, UnresolvedRequirement}; use uv_fs::Simplified; @@ -37,7 +37,7 @@ pub(crate) async fn pip_uninstall( network_settings: &NetworkSettings, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let start = std::time::Instant::now(); diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 73b6a81ab..e201935db 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -18,8 +18,8 @@ use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DevMode, DryRun, - EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, InstallOptions, - PreviewMode, SourceStrategy, + EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, InstallOptions, Preview, + PreviewFeatures, SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -95,15 +95,21 @@ pub(crate) async fn add( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { - if bounds.is_some() && preview.is_disabled() { - warn_user_once!("The bounds option is in preview and may change in any future release."); + if bounds.is_some() && !preview.is_enabled(PreviewFeatures::ADD_BOUNDS) { + warn_user_once!( + "The `bounds` option is in preview and may change in any future release. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::ADD_BOUNDS + ); } - if preview.is_disabled() && !settings.resolver.extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !settings.resolver.extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } @@ -963,7 +969,7 @@ async fn lock_and_sync( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result<(), ProjectError> { let mut lock = project::lock::LockOperation::new( if locked { diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index af3b3b351..675b1a960 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -12,7 +12,7 @@ use crate::settings::{NetworkSettings, ResolverInstallerSettings}; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::{cache_digest, hash_digest}; -use uv_configuration::{Concurrency, Constraints, PreviewMode}; +use uv_configuration::{Concurrency, Constraints, Preview}; use uv_distribution_types::{Name, Resolution}; use uv_fs::PythonExt; use uv_python::{Interpreter, PythonEnvironment, canonicalize_executable}; @@ -119,7 +119,7 @@ impl CachedEnvironment { concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let interpreter = Self::base_interpreter(interpreter, cache)?; diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index b78fc8ce8..df839be08 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -9,7 +9,7 @@ use owo_colors::OwoColorize; use uv_cache::Cache; use uv_configuration::{ Concurrency, DependencyGroups, EditableMode, ExportFormat, ExtrasSpecification, InstallOptions, - PreviewMode, + Preview, }; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_python::{PythonDownloads, PythonPreference, PythonRequest}; @@ -80,7 +80,7 @@ pub(crate) async fn export( quiet: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Identify the target. let workspace_cache = WorkspaceCache::default(); diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 9ba2a434d..6e04524fb 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -12,7 +12,7 @@ use uv_cache::Cache; use uv_cli::AuthorFrom; use uv_client::BaseClientBuilder; use uv_configuration::{ - DependencyGroupsWithDefaults, PreviewMode, ProjectBuildBackend, VersionControlError, + DependencyGroupsWithDefaults, Preview, ProjectBuildBackend, VersionControlError, VersionControlSystem, }; use uv_fs::{CWD, Simplified}; @@ -62,7 +62,7 @@ pub(crate) async fn init( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { match init_kind { InitKind::Script => { @@ -201,7 +201,7 @@ async fn init_script( pin_python: bool, package: bool, no_config: bool, - preview: PreviewMode, + preview: Preview, ) -> Result<()> { if no_workspace { warn_user_once!("`--no-workspace` is a no-op for Python scripts, which are standalone"); @@ -296,7 +296,7 @@ async fn init_project( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result<()> { // Discover the current workspace, if it exists. let workspace_cache = WorkspaceCache::default(); diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index ff6686691..6607459b2 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -12,8 +12,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, - PreviewMode, Reinstall, Upgrade, + Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview, + PreviewFeatures, Reinstall, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -93,7 +93,7 @@ pub(crate) async fn lock( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> anyhow::Result { // If necessary, initialize the PEP 723 script. let script = match script { @@ -271,7 +271,7 @@ pub(super) struct LockOperation<'env> { cache: &'env Cache, workspace_cache: &'env WorkspaceCache, printer: Printer, - preview: PreviewMode, + preview: Preview, } impl<'env> LockOperation<'env> { @@ -286,7 +286,7 @@ impl<'env> LockOperation<'env> { cache: &'env Cache, workspace_cache: &'env WorkspaceCache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Self { Self { mode, @@ -418,7 +418,7 @@ async fn do_lock( cache: &Cache, workspace_cache: &WorkspaceCache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let start = std::time::Instant::now(); @@ -443,9 +443,12 @@ async fn do_lock( sources, } = settings; - if preview.is_disabled() && !extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 96860b911..50a38115c 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -12,8 +12,8 @@ use uv_cache::{Cache, CacheBucket}; use uv_cache_key::cache_digest; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, - PreviewMode, Reinstall, SourceStrategy, Upgrade, + Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, Preview, + PreviewFeatures, Reinstall, SourceStrategy, Upgrade, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::{DistributionDatabase, LoweredRequirement}; @@ -648,7 +648,7 @@ impl ScriptInterpreter { active: Option, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // For now, we assume that scripts are never evaluated in the context of a workspace. let workspace = None; @@ -888,7 +888,7 @@ impl ProjectInterpreter { active: Option, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Resolve the Python request and requirement for the workspace. let WorkspacePython { @@ -1270,7 +1270,7 @@ impl ProjectEnvironment { cache: &Cache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Lock the project environment to avoid synchronization issues. let _lock = ProjectInterpreter::lock(workspace) @@ -1280,7 +1280,7 @@ impl ProjectEnvironment { }) .ok(); - let upgradeable = preview.is_enabled() + let upgradeable = preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) && python .as_ref() .is_none_or(|request| !request.includes_patch()); @@ -1502,7 +1502,7 @@ impl ScriptEnvironment { cache: &Cache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Lock the script environment to avoid synchronization issues. let _lock = ScriptInterpreter::lock(script) @@ -1659,7 +1659,7 @@ pub(crate) async fn resolve_names( cache: &Cache, workspace_cache: &WorkspaceCache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result, uv_requirements::Error> { // Partition the requirements into named and unnamed requirements. let (mut requirements, unnamed): (Vec<_>, Vec<_>) = @@ -1834,7 +1834,7 @@ pub(crate) async fn resolve_environment( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { warn_on_requirements_txt_setting(&spec.requirements, settings); @@ -2026,7 +2026,7 @@ pub(crate) async fn sync_environment( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let InstallerSettingsRef { index_locations, @@ -2198,7 +2198,7 @@ pub(crate) async fn update_environment( workspace_cache: WorkspaceCache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { warn_on_requirements_txt_setting(&spec, &settings.resolver); @@ -2441,7 +2441,7 @@ pub(crate) async fn init_script_python_requirement( client_builder: &BaseClientBuilder<'_>, cache: &Cache, reporter: &PythonDownloadReporter, - preview: PreviewMode, + preview: Preview, ) -> anyhow::Result { let python_request = if let Some(request) = python { // (1) Explicit request from user diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index cfda7327b..50c498833 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -10,7 +10,7 @@ use tracing::{debug, warn}; use uv_cache::Cache; use uv_configuration::{ Concurrency, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, InstallOptions, - PreviewMode, + Preview, }; use uv_fs::Simplified; use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, DefaultGroups}; @@ -60,7 +60,7 @@ pub(crate) async fn remove( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let target = if let Some(script) = script { // If we found a PEP 723 script and the user provided a project-only setting, warn. diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 8b863eb10..c50788ad8 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -19,7 +19,7 @@ use uv_cli::ExternalCommand; use uv_client::BaseClientBuilder; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, ExtrasSpecification, - InstallOptions, PreviewMode, + InstallOptions, Preview, }; use uv_distribution_types::Requirement; use uv_fs::which::is_executable; @@ -94,7 +94,7 @@ pub(crate) async fn run( printer: Printer, env_file: Vec, no_env_file: bool, - preview: PreviewMode, + preview: Preview, max_recursion_depth: u32, ) -> anyhow::Result { // Check if max recursion depth was exceeded. This most commonly happens @@ -1071,8 +1071,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl ephemeral_env.set_overlay(format!( "import site; site.addsitedir(\"{}\"); site.addsitedir(\"{}\");", - base_site_packages.escape_for_python(), requirements_site_packages.escape_for_python(), + base_site_packages.escape_for_python(), ))?; // N.B. The order here matters — earlier interpreters take precedence over the diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index c93e1dc39..032d69962 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -14,7 +14,7 @@ use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, ExtrasSpecification, ExtrasSpecificationWithDefaults, HashCheckingMode, InstallOptions, - PreviewMode, TargetTriple, Upgrade, + Preview, PreviewFeatures, TargetTriple, Upgrade, }; use uv_dispatch::BuildDispatch; use uv_distribution_types::{ @@ -80,12 +80,14 @@ pub(crate) async fn sync( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, output_format: SyncFormat, ) -> Result { - if preview.is_enabled() && matches!(output_format, SyncFormat::Json) { + if preview.is_enabled(PreviewFeatures::JSON_OUTPUT) && matches!(output_format, SyncFormat::Json) + { warn_user!( - "The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview` to disable this warning." + "The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::JSON_OUTPUT ); } @@ -579,7 +581,7 @@ pub(super) async fn do_sync( workspace_cache: WorkspaceCache, dry_run: DryRun, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result<(), ProjectError> { // Extract the project settings. let InstallerSettingsRef { @@ -600,9 +602,12 @@ pub(super) async fn do_sync( sources, } = settings; - if preview.is_disabled() && !extra_build_dependencies.is_empty() { + if !preview.is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + && !extra_build_dependencies.is_empty() + { warn_user_once!( - "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index 15d0c990b..b54d8040a 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -7,7 +7,7 @@ use tokio::sync::Semaphore; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::RegistryClientBuilder; -use uv_configuration::{Concurrency, DependencyGroups, PreviewMode, TargetTriple}; +use uv_configuration::{Concurrency, DependencyGroups, Preview, TargetTriple}; use uv_distribution_types::IndexCapabilities; use uv_normalize::DefaultGroups; use uv_pep508::PackageName; @@ -57,7 +57,7 @@ pub(crate) async fn tree( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Find the project requirements. let workspace_cache = WorkspaceCache::default(); diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index c4b32485d..566573594 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -11,7 +11,7 @@ use uv_cli::version::VersionInfo; use uv_cli::{VersionBump, VersionFormat}; use uv_configuration::{ Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, EditableMode, - ExtrasSpecification, InstallOptions, PreviewMode, + ExtrasSpecification, InstallOptions, Preview, }; use uv_fs::Simplified; use uv_normalize::DefaultExtras; @@ -76,7 +76,7 @@ pub(crate) async fn project_version( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Read the metadata let project = find_target(project_dir, package.as_ref(), explicit_project).await?; @@ -414,7 +414,7 @@ async fn print_frozen_version( short: bool, output_format: VersionFormat, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // Discover the interpreter (this is the same interpreter --no-sync uses). let interpreter = ProjectInterpreter::discover( @@ -509,7 +509,7 @@ async fn lock_and_sync( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { // If frozen, don't touch the lock or sync at all if frozen { diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index e188e9d20..806e670a8 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use std::path::Path; use uv_cache::Cache; -use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode}; +use uv_configuration::{DependencyGroupsWithDefaults, Preview}; use uv_fs::Simplified; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, @@ -32,7 +32,7 @@ pub(crate) async fn find( python_preference: PythonPreference, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let environment_preference = if system { EnvironmentPreference::OnlySystem @@ -123,7 +123,7 @@ pub(crate) async fn find_script( no_config: bool, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let interpreter = match ScriptInterpreter::discover( script, diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index e54c44424..f2082ce8a 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -14,7 +14,7 @@ use owo_colors::OwoColorize; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, trace}; -use uv_configuration::PreviewMode; +use uv_configuration::{Preview, PreviewFeatures}; use uv_fs::Simplified; use uv_python::downloads::{ self, ArchRequest, DownloadResult, ManagedPythonDownload, PythonDownloadRequest, @@ -161,7 +161,7 @@ pub(crate) async fn install( default: bool, python_downloads: PythonDownloads, no_config: bool, - preview: PreviewMode, + preview: Preview, printer: Printer, ) -> Result { let start = std::time::Instant::now(); @@ -170,15 +170,17 @@ pub(crate) async fn install( // `--default` is used. It's not clear how this overlaps with a global Python pin, but I'd be // surprised if `uv python find` returned the "newest" Python version rather than the one I just // installed with the `--default` flag. - if default && !preview.is_enabled() { + if default && !preview.is_enabled(PreviewFeatures::PYTHON_INSTALL_DEFAULT) { warn_user!( - "The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning" + "The `--default` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning", + PreviewFeatures::PYTHON_INSTALL_DEFAULT ); } - if upgrade && preview.is_disabled() { + if upgrade && !preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) { warn_user!( - "`uv python upgrade` is experimental and may change without warning. Pass `--preview` to disable this warning" + "`uv python upgrade` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning", + PreviewFeatures::PYTHON_UPGRADE ); } @@ -737,12 +739,13 @@ fn create_bin_links( installations: &[&ManagedPythonInstallation], changelog: &mut Changelog, errors: &mut Vec<(InstallErrorKind, PythonInstallationKey, Error)>, - preview: PreviewMode, + preview: Preview, ) { // TODO(zanieb): We want more feedback on the `is_default_install` behavior before stabilizing // it. In particular, it may be confusing because it does not apply when versions are loaded // from a `.python-version` file. - let targets = if (default || (is_default_install && preview.is_enabled())) + let targets = if (default + || (is_default_install && preview.is_enabled(PreviewFeatures::PYTHON_INSTALL_DEFAULT))) && first_request.matches_installation(installation) { vec![ diff --git a/crates/uv/src/commands/python/list.rs b/crates/uv/src/commands/python/list.rs index 17528a11e..c30eecf83 100644 --- a/crates/uv/src/commands/python/list.rs +++ b/crates/uv/src/commands/python/list.rs @@ -2,7 +2,7 @@ use serde::Serialize; use std::collections::BTreeSet; use std::fmt::Write; use uv_cli::PythonListFormat; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_pep440::Version; use anyhow::Result; @@ -65,7 +65,7 @@ pub(crate) async fn list( python_downloads: PythonDownloads, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let request = request.as_deref().map(PythonRequest::parse); let base_download_request = if python_preference == PythonPreference::OnlySystem { diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index 0e78e6b5c..064cc780c 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -8,7 +8,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::{DependencyGroupsWithDefaults, PreviewMode}; +use uv_configuration::{DependencyGroupsWithDefaults, Preview}; use uv_fs::Simplified; use uv_python::{ EnvironmentPreference, PYTHON_VERSION_FILENAME, PythonDownloads, PythonInstallation, @@ -39,7 +39,7 @@ pub(crate) async fn pin( network_settings: NetworkSettings, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let workspace_cache = WorkspaceCache::default(); let virtual_project = if no_project { @@ -270,7 +270,7 @@ fn warn_if_existing_pin_incompatible_with_project( virtual_project: &VirtualProject, python_preference: PythonPreference, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) { // Check if the pinned version is compatible with the project. if let Some(pin_version) = pep440_version_from_request(pin) { diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index c2e2e6877..b58b5831a 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -11,7 +11,7 @@ use owo_colors::OwoColorize; use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, warn}; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_fs::Simplified; use uv_python::downloads::PythonDownloadRequest; use uv_python::managed::{ @@ -30,7 +30,7 @@ pub(crate) async fn uninstall( targets: Vec, all: bool, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?; @@ -66,7 +66,7 @@ async fn do_uninstall( targets: Vec, all: bool, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let start = std::time::Instant::now(); @@ -112,13 +112,11 @@ async fn do_uninstall( } if !found { // Clear any remnants in the registry - if preview.is_enabled() { - #[cfg(windows)] - { - uv_python::windows_registry::remove_orphan_registry_entries( - &installed_installations, - ); - } + #[cfg(windows)] + { + uv_python::windows_registry::remove_orphan_registry_entries( + &installed_installations, + ); } if matches!(requests.as_slice(), [PythonRequest::Default]) { diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index b24a64e25..5647afa32 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -7,7 +7,7 @@ use std::{collections::BTreeSet, ffi::OsString}; use tracing::{debug, warn}; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_distribution_types::Requirement; use uv_distribution_types::{InstalledDist, Name}; use uv_fs::Simplified; @@ -81,7 +81,7 @@ pub(crate) async fn refine_interpreter( python_preference: PythonPreference, python_downloads: PythonDownloads, cache: &Cache, - preview: PreviewMode, + preview: Preview, ) -> anyhow::Result, ProjectError> { let pip::operations::Error::Resolve(uv_resolver::ResolveError::NoSolution(no_solution_err)) = err diff --git a/crates/uv/src/commands/tool/dir.rs b/crates/uv/src/commands/tool/dir.rs index b246e701d..7f937e35e 100644 --- a/crates/uv/src/commands/tool/dir.rs +++ b/crates/uv/src/commands/tool/dir.rs @@ -2,12 +2,12 @@ use anstream::println; use anyhow::Context; use owo_colors::OwoColorize; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_fs::Simplified; use uv_tool::{InstalledTools, tool_executable_dir}; /// Show the tool directory. -pub(crate) fn dir(bin: bool, _preview: PreviewMode) -> anyhow::Result<()> { +pub(crate) fn dir(bin: bool, _preview: Preview) -> anyhow::Result<()> { if bin { let executable_directory = tool_executable_dir()?; println!("{}", executable_directory.simplified_display().cyan()); diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index fa4ae243a..6528f61d2 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -8,7 +8,7 @@ use tracing::{debug, trace}; use uv_cache::{Cache, Refresh}; use uv_cache_info::Timestamp; use uv_client::BaseClientBuilder; -use uv_configuration::{Concurrency, Constraints, DryRun, PreviewMode, Reinstall, Upgrade}; +use uv_configuration::{Concurrency, Constraints, DryRun, Preview, Reinstall, Upgrade}; use uv_distribution_types::{ NameRequirementSpecification, Requirement, RequirementSource, UnresolvedRequirementSpecification, @@ -62,7 +62,7 @@ pub(crate) async fn install( concurrency: Concurrency, cache: Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let client_builder = BaseClientBuilder::new() .retries_from_env()? diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 7c91b9fe9..1c86feb3a 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -17,7 +17,7 @@ use uv_cache_info::Timestamp; use uv_cli::ExternalCommand; use uv_client::BaseClientBuilder; use uv_configuration::Constraints; -use uv_configuration::{Concurrency, PreviewMode}; +use uv_configuration::{Concurrency, Preview}; use uv_distribution_types::InstalledDist; use uv_distribution_types::{ IndexUrl, Name, NameRequirementSpecification, Requirement, RequirementSource, @@ -101,7 +101,7 @@ pub(crate) async fn run( printer: Printer, env_file: Vec, no_env_file: bool, - preview: PreviewMode, + preview: Preview, ) -> anyhow::Result { /// Whether or not a path looks like a Python script based on the file extension. fn has_python_script_ext(path: &Path) -> bool { @@ -686,7 +686,7 @@ async fn get_or_create_environment( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result<(ToolRequirement, PythonEnvironment), ProjectError> { let client_builder = BaseClientBuilder::new() .retries_from_env()? diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 1c511524c..2b4819265 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -7,7 +7,7 @@ use tracing::debug; use uv_cache::Cache; use uv_client::BaseClientBuilder; -use uv_configuration::{Concurrency, Constraints, DryRun, PreviewMode}; +use uv_configuration::{Concurrency, Constraints, DryRun, Preview}; use uv_distribution_types::Requirement; use uv_fs::CWD; use uv_normalize::PackageName; @@ -47,7 +47,7 @@ pub(crate) async fn upgrade( concurrency: Concurrency, cache: &Cache, printer: Printer, - preview: PreviewMode, + preview: Preview, ) -> Result { let installed_tools = InstalledTools::from_settings()?.init()?; let _lock = installed_tools.lock().await?; @@ -221,7 +221,7 @@ async fn upgrade_tool( filesystem: &ResolverInstallerOptions, installer_metadata: bool, concurrency: Concurrency, - preview: PreviewMode, + preview: Preview, ) -> Result { // Ensure the tool is installed. let existing_tool_receipt = match installed_tools.get_tool_receipt(name) { diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index c2395fc4f..39f47c7aa 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -1,7 +1,6 @@ use std::fmt::Write; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::Arc; use std::vec; use anyhow::Result; @@ -12,7 +11,8 @@ use uv_cache::Cache; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, Constraints, DependencyGroups, IndexStrategy, - KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, SourceStrategy, + KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, Preview, PreviewFeatures, + SourceStrategy, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution_types::Requirement; @@ -83,7 +83,7 @@ pub(crate) async fn venv( cache: &Cache, printer: Printer, relocatable: bool, - preview: PreviewMode, + preview: Preview, ) -> Result { let workspace_cache = WorkspaceCache::default(); let project = if no_project { @@ -200,7 +200,7 @@ pub(crate) async fn venv( path.user_display().cyan() )?; - let upgradeable = preview.is_enabled() + let upgradeable = preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE) && python_request .as_ref() .is_none_or(|request| !request.includes_patch()); @@ -225,15 +225,7 @@ pub(crate) async fn venv( let interpreter = venv.interpreter(); // Add all authenticated sources to the cache. - for index in index_locations.allowed_indexes() { - if let Some(credentials) = index.credentials() { - let credentials = Arc::new(credentials); - uv_auth::store_credentials(index.raw_url(), credentials.clone()); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - } - } + index_locations.cache_index_credentials(); // Instantiate a client. let client = RegistryClientBuilder::try_from(client_builder)? diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 2ec7aa626..57a8a0320 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -28,7 +28,7 @@ use uv_cli::{ ProjectCommand, PythonCommand, PythonNamespace, SelfCommand, SelfNamespace, ToolCommand, ToolNamespace, TopLevelArgs, compat::CompatArgs, }; -use uv_configuration::min_stack_size; +use uv_configuration::{PreviewFeatures, min_stack_size}; use uv_fs::{CWD, Simplified}; #[cfg(feature = "self-update")] use uv_pep440::release_specifiers_to_ranges; @@ -443,9 +443,14 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCompileSettings::resolve(args, filesystem); show_settings!(args); - if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + if !args.settings.extra_build_dependencies.is_empty() + && !globals + .preview + .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + { warn_user_once!( - "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } @@ -549,9 +554,14 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipSyncSettings::resolve(args, filesystem); show_settings!(args); - if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + if !args.settings.extra_build_dependencies.is_empty() + && !globals + .preview + .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + { warn_user_once!( - "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } @@ -633,9 +643,14 @@ async fn run(mut cli: Cli) -> Result { // Resolve the settings from the command-line arguments and workspace configuration. let mut args = PipInstallSettings::resolve(args, filesystem); show_settings!(args); - if !args.settings.extra_build_dependencies.is_empty() && globals.preview.is_disabled() { + if !args.settings.extra_build_dependencies.is_empty() + && !globals + .preview + .is_enabled(PreviewFeatures::EXTRA_BUILD_DEPENDENCIES) + { warn_user_once!( - "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview` to disable this warning." + "The `extra-build-dependencies` setting is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::EXTRA_BUILD_DEPENDENCIES ); } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index c30935218..35e94ceea 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -23,9 +23,9 @@ use uv_client::Connectivity; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DependencyGroups, DryRun, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, - KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, PreviewMode, - ProjectBuildBackend, Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, - TrustedPublishing, Upgrade, VersionControlSystem, + KeyringProviderType, NoBinary, NoBuild, PackageConfigSettings, Preview, ProjectBuildBackend, + Reinstall, RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, + Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl, Requirement}; use uv_install_wheel::LinkMode; @@ -63,7 +63,7 @@ pub(crate) struct GlobalSettings { pub(crate) network_settings: NetworkSettings, pub(crate) concurrency: Concurrency, pub(crate) show_settings: bool, - pub(crate) preview: PreviewMode, + pub(crate) preview: Preview, pub(crate) python_preference: PythonPreference, pub(crate) python_downloads: PythonDownloads, pub(crate) no_progress: bool, @@ -117,10 +117,12 @@ impl GlobalSettings { .unwrap_or_else(Concurrency::threads), }, show_settings: args.show_settings, - preview: PreviewMode::from( + preview: Preview::from_args( flag(args.preview, args.no_preview, "preview") .combine(workspace.and_then(|workspace| workspace.globals.preview)) .unwrap_or(false), + args.no_preview, + &args.preview_features, ), python_preference, python_downloads: flag( diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4c686cb77..d7fbdaa6f 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -21,7 +21,7 @@ use regex::Regex; use tokio::io::AsyncWriteExt; use uv_cache::Cache; -use uv_configuration::PreviewMode; +use uv_configuration::Preview; use uv_fs::Simplified; use uv_python::managed::ManagedPythonInstallations; use uv_python::{ @@ -706,6 +706,11 @@ impl TestContext { ), r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(), )); + // Filter script environment hashes + filters.push(( + r"environments-v(\d+)[\\/](\w+)-[a-z0-9]+".to_string(), + "environments-v$1/$2-[HASH]".to_string(), + )); Self { root: ChildPath::new(root.path()), @@ -1500,7 +1505,7 @@ pub fn python_installations_for_versions( EnvironmentPreference::OnlySystem, PythonPreference::Managed, &cache, - PreviewMode::Disabled, + Preview::default(), ) { python.into_interpreter().sys_executable().to_owned() } else { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 05527b139..7fa46f0c3 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -13021,7 +13021,7 @@ fn add_bounds() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The bounds option is in preview and may change in any future release. + warning: The `bounds` option is in preview and may change in any future release. Pass `--preview-features add-bounds` to disable this warning. Resolved 2 packages in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] @@ -13061,7 +13061,7 @@ fn add_bounds() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The bounds option is in preview and may change in any future release. + warning: The `bounds` option is in preview and may change in any future release. Pass `--preview-features add-bounds` to disable this warning. Resolved 4 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 8a006ffb6..9f38a168a 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -29411,3 +29411,490 @@ fn test_tilde_equals_python_version() -> Result<()> { Ok(()) } + +/// Test that lockfile validation includes explicit indexes from path dependencies. +/// +#[test] +fn lock_path_dependency_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the path dependency with explicit index + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv.sources] + iniconfig = { index = "inner-index" } + + [[tool.uv.index]] + name = "inner-index" + url = "https://pypi-proxy.fly.dev/simple" + explicit = true + "#, + )?; + + // Create a project that depends on pkg_a + let pkg_b = context.temp_dir.child("pkg_b"); + fs_err::create_dir_all(&pkg_b)?; + + let pyproject_toml = pkg_b.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-b" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-a"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/", editable = true } + black = { index = "outer-index" } + + [[tool.uv.index]] + name = "outer-index" + url = "https://outer-index.com/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 3 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check").current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 3 packages in [TIME] + "); + + Ok(()) +} + +/// Test that lockfile validation includes explicit indexes from path dependencies +/// defined in a non-root workspace member. +#[test] +fn lock_path_dependency_explicit_index_workspace_member() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the path dependency with explicit index + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv.sources] + iniconfig = { index = "inner-index" } + + [[tool.uv.index]] + name = "inner-index" + url = "https://pypi-proxy.fly.dev/simple" + explicit = true + "#, + )?; + + // Create a project that depends on pkg_a + let member = context.temp_dir.child("member"); + fs_err::create_dir_all(&member)?; + + let pyproject_toml = member.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "member" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-a"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/", editable = true } + black = { index = "middle-index" } + + [[tool.uv.index]] + name = "middle-index" + url = "https://middle-index.com/simple" + explicit = true + "#, + )?; + + // Create a root with workspace member + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "root-project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["member"] + + [tool.uv.workspace] + members = ["member"] + + [tool.uv.sources] + member = { workspace = true } + anyio = { index = "outer-index" } + + [[tool.uv.index]] + name = "outer-index" + url = "https://outer-index.com/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + Ok(()) +} + +/// Test that lockfile validation works correctly when path dependency has +/// both explicit and non-explicit indexes. +#[test] +fn lock_path_dependency_mixed_indexes() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the path dependency with both explicit and non-explicit indexes. + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig", "anyio"] + + [tool.uv.sources] + iniconfig = { index = "explicit-index" } + anyio = { index = "non-explicit-index" } + + [[tool.uv.index]] + name = "non-explicit-index" + url = "https://pypi-proxy.fly.dev/simple" + + [[tool.uv.index]] + name = "explicit-index" + url = "https://pypi.org/simple" + explicit = true + "#, + )?; + + // Create a project that depends on pkg_a. + let pkg_b = context.temp_dir.child("pkg_b"); + fs_err::create_dir_all(&pkg_b)?; + + let pyproject_toml = pkg_b.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-b" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-a"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/", editable = true } + black = { index = "outer-index" } + + [[tool.uv.index]] + name = "outer-index" + url = "https://outer-index.com/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 6 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check").current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 6 packages in [TIME] + "); + + Ok(()) +} + +/// Test that path dependencies without an index don't affect validation. +#[test] +fn lock_path_dependency_no_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the path dependency without explicit indexes. + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["requests"] + "#, + )?; + + // Create a project that depends on pkg_a. + let pkg_b = context.temp_dir.child("pkg_b"); + fs_err::create_dir_all(&pkg_b)?; + + let pyproject_toml = pkg_b.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-b" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-a"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/", editable = true } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 7 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check").current_dir(&pkg_b), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 7 packages in [TIME] + "); + + Ok(()) +} + +/// Test that a nested path dependency with an explicit index validates correctly. +#[test] +fn lock_nested_path_dependency_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create the inner dependency with explicit index. + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [tool.uv.sources] + iniconfig = { index = "inner-index" } + + [[tool.uv.index]] + name = "inner-index" + url = "https://pypi-proxy.fly.dev/simple" + explicit = true + "#, + )?; + + // Create intermediate dependency that depends on pkg_a. + let pkg_b = context.temp_dir.child("pkg_b"); + fs_err::create_dir_all(&pkg_b)?; + + let pyproject_toml = pkg_b.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-b" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-a"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/", editable = true } + "#, + )?; + + // Create a project that depends on intermediate dependency. + let pkg_c = context.temp_dir.child("pkg_c"); + fs_err::create_dir_all(&pkg_c)?; + + let pyproject_toml = pkg_c.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-c" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["pkg-b"] + + [tool.uv.sources] + pkg-b = { path = "../pkg_b/", editable = true } + black = { index = "outer-index" } + + [[tool.uv.index]] + name = "outer-index" + url = "https://outer-index.com/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().current_dir(&pkg_c), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 4 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check").current_dir(&pkg_c), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 4 packages in [TIME] + "); + + Ok(()) +} + +/// Test that validating circular path dependency indexes doesn't cause an infinite loop. +#[test] +fn lock_circular_path_dependency_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create pkg_a (with explicit index) that depends on pkg_b. + let pkg_a = context.temp_dir.child("pkg_a"); + fs_err::create_dir_all(&pkg_a)?; + + let pyproject_toml = pkg_a.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-a" + version = "0.1.0" + requires-python = ">=3.10" + dependencies = ["pkg-b", "iniconfig"] + + [tool.uv.sources] + pkg-b = { path = "../pkg_b/" } + iniconfig = { index = "index-a" } + + [[tool.uv.index]] + name = "index-a" + url = "https://pypi-proxy.fly.dev/simple" + explicit = true + "#, + )?; + + // Create pkg_b that depends on pkg_a. This is a circular dependency. + let pkg_b = context.temp_dir.child("pkg_b"); + fs_err::create_dir_all(&pkg_b)?; + + let pyproject_toml = pkg_b.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "pkg-b" + version = "0.1.0" + requires-python = ">=3.10" + dependencies = ["pkg-a", "anyio"] + + [tool.uv.sources] + pkg-a = { path = "../pkg_a/" } + anyio = { index = "index-b" } + + [[tool.uv.index]] + name = "index-b" + url = "https://pypi.org/simple" + explicit = true + default = true + "#, + )?; + + // This should not hang or crash due to the circular dependency. + uv_snapshot!(context.filters(), context.lock().current_dir(&pkg_a), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 8 packages in [TIME] + "); + + uv_snapshot!(context.filters(), context.lock().arg("--check").current_dir(&pkg_a), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Resolved 8 packages in [TIME] + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/lock_conflict.rs b/crates/uv/tests/it/lock_conflict.rs index d67736c88..2025dbd8b 100644 --- a/crates/uv/tests/it/lock_conflict.rs +++ b/crates/uv/tests/it/lock_conflict.rs @@ -1057,6 +1057,7 @@ fn extra_unconditional() -> Result<()> { ----- stderr ----- Resolved 6 packages in [TIME] "###); + // This should error since we're enabling two conflicting extras. uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" success: false @@ -1652,6 +1653,249 @@ fn extra_nested_across_workspace() -> Result<()> { Ok(()) } +/// The project declares conflicting extras, but one of the extras directly depends on the other. +#[test] +fn extra_depends_on_conflicting_extra() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "example" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + foo = ["sortedcontainers==2.3.0", "example[bar]"] + bar = ["sortedcontainers==2.4.0"] + + [tool.uv] + conflicts = [ + [ + { extra = "foo" }, + { extra = "bar" }, + ], + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.setuptools.packages.find] + include = ["example"] + "#, + )?; + + // This should fail to resolve, because the extras are always required together and + // `example[foo]` is unusable. + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example[foo] depends on sortedcontainers==2.3.0 and sortedcontainers==2.4.0, we can conclude that example[foo]'s requirements are unsatisfiable. + And because your project requires example[foo], we can conclude that your project's requirements are unsatisfiable. + "); + + Ok(()) +} + +/// Like [`extra_depends_on_conflicting_extra`], but the conflict between the extras is mediated by +/// another package. +#[test] +fn extra_depends_on_conflicting_extra_transitive() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "example" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + foo = ["sortedcontainers==2.3.0", "indirection"] + bar = ["sortedcontainers==2.4.0"] + + [tool.uv] + conflicts = [ + [ + { extra = "foo" }, + { extra = "bar" }, + ], + ] + + [tool.uv.sources] + indirection = { workspace = true } + + [tool.uv.workspace] + members = ["indirection"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.setuptools.packages.find] + include = ["example"] + "#, + )?; + + // Create the indirection subproject + let subproject_dir = context.temp_dir.child("indirection"); + subproject_dir.create_dir_all()?; + + let sub_pyproject_toml = subproject_dir.child("pyproject.toml"); + sub_pyproject_toml.write_str( + r#" + [project] + name = "indirection" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["example[bar]"] + + [tool.uv.sources] + example = { workspace = true } + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // This succeeds, but probably shouldn't. There's an unconditional conflict in `example[foo] + // -> indirection[bar] -> example[bar]`, which means `example[foo]` can never be used. + uv_snapshot!(context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r#" + version = 1 + revision = 2 + requires-python = ">=3.12" + conflicts = [[ + { package = "example", extra = "bar" }, + { package = "example", extra = "foo" }, + ]] + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + members = [ + "example", + "indirection", + ] + + [[package]] + name = "example" + version = "0.1.0" + source = { editable = "." } + + [package.optional-dependencies] + bar = [ + { name = "sortedcontainers", version = "2.4.0", source = { registry = "https://pypi.org/simple" } }, + ] + foo = [ + { name = "indirection" }, + { name = "sortedcontainers", version = "2.3.0", source = { registry = "https://pypi.org/simple" } }, + ] + + [package.metadata] + requires-dist = [ + { name = "indirection", marker = "extra == 'foo'", editable = "indirection" }, + { name = "sortedcontainers", marker = "extra == 'bar'", specifier = "==2.4.0" }, + { name = "sortedcontainers", marker = "extra == 'foo'", specifier = "==2.3.0" }, + ] + provides-extras = ["foo", "bar"] + + [[package]] + name = "indirection" + version = "0.1.0" + source = { editable = "indirection" } + dependencies = [ + { name = "example" }, + { name = "example", extra = ["bar"], marker = "extra == 'extra-7-example-bar'" }, + ] + + [package.metadata] + requires-dist = [{ name = "example", extras = ["bar"], editable = "." }] + + [[package]] + name = "sortedcontainers" + version = "2.3.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/14/10/6a9481890bae97da9edd6e737c9c3dec6aea3fc2fa53b0934037b35c89ea/sortedcontainers-2.3.0.tar.gz", hash = "sha256:59cc937650cf60d677c16775597c89a960658a09cf7c1a668f86e1e4464b10a1", size = 30509, upload-time = "2020-11-09T00:03:52.258Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/20/4d/a7046ae1a1a4cc4e9bbed194c387086f06b25038be596543d026946330c9/sortedcontainers-2.3.0-py2.py3-none-any.whl", hash = "sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f", size = 29479, upload-time = "2020-11-09T00:03:50.723Z" }, + ] + + [[package]] + name = "sortedcontainers" + version = "2.4.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } + wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, + ] + "# + ); + }); + + // Install from the lockfile + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + example==0.1.0 (from file://[TEMP_DIR]/) + "); + + // Install with `foo` + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--extra").arg("foo"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Found conflicting extras `example[bar]` and `example[foo]` enabled simultaneously + "); + + // Install the child package + uv_snapshot!(context.filters(), context.sync().arg("--frozen").arg("--package").arg("indirection"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + indirection==0.1.0 (from file://[TEMP_DIR]/indirection) + + sortedcontainers==2.4.0 + "); + + Ok(()) +} + /// This tests a "basic" case for specifying conflicting groups. #[test] fn group_basic() -> Result<()> { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 577fe79f4..698f201fe 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2479,8 +2479,7 @@ fn install_git_private_https_pat_not_authorized() { ├─▶ failed to clone into: [CACHE_DIR]/git-v0/db/8401f5508e3e612d ╰─▶ process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128) --- stderr - remote: Support for password authentication was removed on August 13, 2021. - remote: Please see https://docs.github.com/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication. + remote: Invalid username or token. Password authentication is not supported for Git operations. fatal: Authentication failed for 'https://github.com/astral-test/uv-private-pypackage/' "); } diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index 41eceeb92..d2ae51e38 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -837,16 +837,7 @@ fn python_find_script() { .with_filtered_python_names() .with_filtered_exe_suffix(); - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/[\w-]+", - "environments-v2/[HASHEDNAME]", - )]) - .collect::>(); - - uv_snapshot!(filters, context.init().arg("--script").arg("foo.py"), @r###" + uv_snapshot!(context.filters(), context.init().arg("--script").arg("foo.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -855,22 +846,22 @@ fn python_find_script() { Initialized script at `foo.py` "###); - uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("foo.py"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME] + Creating script environment at: [CACHE_DIR]/environments-v2/foo-[HASH] Resolved in [TIME] Audited in [TIME] "); - uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r" + uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r" success: true exit_code: 0 ----- stdout ----- - [CACHE_DIR]/environments-v2/[HASHEDNAME]/[BIN]/[PYTHON] + [CACHE_DIR]/environments-v2/foo-[HASH]/[BIN]/[PYTHON] ----- stderr ----- "); @@ -936,15 +927,6 @@ fn python_find_script_no_such_version() { .with_filtered_python_names() .with_filtered_exe_suffix() .with_filtered_python_sources(); - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/[\w-]+", - "environments-v2/[HASHEDNAME]", - )]) - .collect::>(); - let script = context.temp_dir.child("foo.py"); script .write_str(indoc! {r#" @@ -955,13 +937,13 @@ fn python_find_script_no_such_version() { "#}) .unwrap(); - uv_snapshot!(filters, context.sync().arg("--script").arg("foo.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("foo.py"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Creating script environment at: [CACHE_DIR]/environments-v2/[HASHEDNAME] + Creating script environment at: [CACHE_DIR]/environments-v2/foo-[HASH] Resolved in [TIME] Audited in [TIME] "); @@ -975,7 +957,7 @@ fn python_find_script_no_such_version() { "#}) .unwrap(); - uv_snapshot!(filters, context.python_find().arg("--script").arg("foo.py"), @r" + uv_snapshot!(context.filters(), context.python_find().arg("--script").arg("foo.py"), @r" success: false exit_code: 1 ----- stdout ----- diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 41b046026..c5f98af0d 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -630,14 +630,14 @@ fn python_install_preview() { "###); // Should be a no-op when already installed - uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###" + uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Python is already installed. Use `uv python install ` to install another version. - "###); + "); // You can opt-in to a reinstall uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--reinstall"), @r" @@ -1260,7 +1260,7 @@ fn python_install_default() { ----- stdout ----- ----- stderr ----- - warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning + warning: The `--default` option is experimental and may change without warning. Pass `--preview-features python-install-default` to disable this warning Installed Python 3.13.5 in [TIME] + cpython-3.13.5-[PLATFORM] (python, python3) "); @@ -1294,7 +1294,7 @@ fn python_install_default() { ----- stdout ----- ----- stderr ----- - warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning + warning: The `--default` option is experimental and may change without warning. Pass `--preview-features python-install-default` to disable this warning Installed Python 3.13.5 in [TIME] + cpython-3.13.5-[PLATFORM] (python, python3, python3.13) "); @@ -1379,7 +1379,7 @@ fn python_install_default() { ----- stdout ----- ----- stderr ----- - warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning + warning: The `--default` option is experimental and may change without warning. Pass `--preview-features python-install-default` to disable this warning error: The `--default` flag cannot be used with multiple targets "); @@ -1390,7 +1390,7 @@ fn python_install_default() { ----- stdout ----- ----- stderr ----- - warning: The `--default` option is experimental and may change without warning. Pass `--preview` to disable this warning + warning: The `--default` option is experimental and may change without warning. Pass `--preview-features python-install-default` to disable this warning Installed Python 3.12.11 in [TIME] + cpython-3.12.11-[PLATFORM] (python, python3, python3.12) "); diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 2e9762a60..300a87f06 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1167,14 +1167,17 @@ fn run_with() -> Result<()> { let test_script = context.temp_dir.child("main.py"); test_script.write_str(indoc! { r" import sniffio + + print(sniffio.__version__) " })?; // Requesting an unsatisfied requirement should install it. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("iniconfig").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- + 1.3.0 ----- stderr ----- Resolved 2 packages in [TIME] @@ -1186,24 +1189,26 @@ fn run_with() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + iniconfig==2.0.0 - "###); + "); // Requesting a satisfied requirement should use the base environment. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- + 1.3.0 ----- stderr ----- Resolved 2 packages in [TIME] Audited 2 packages in [TIME] - "###); + "); // Unless the user requests a different version. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio<1.3.0").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("sniffio<1.3.0").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- + 1.2.0 ----- stderr ----- Resolved 2 packages in [TIME] @@ -1212,15 +1217,16 @@ fn run_with() -> Result<()> { Prepared 1 package in [TIME] Installed 1 package in [TIME] + sniffio==1.2.0 - "###); + "); // If we request a dependency that isn't in the base environment, we should still respect any // other dependencies. In this case, `sniffio==1.3.0` is not the latest-compatible version, but // we should use it anyway. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- + 1.3.0 ----- stderr ----- Resolved 2 packages in [TIME] @@ -1231,13 +1237,14 @@ fn run_with() -> Result<()> { + anyio==4.3.0 + idna==3.6 + sniffio==1.3.0 - "###); + "); // Even if we run with` --no-sync`. - uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio==4.2.0").arg("--no-sync").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--with").arg("anyio==4.2.0").arg("--no-sync").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- + 1.3.0 ----- stderr ----- Resolved 3 packages in [TIME] @@ -1246,7 +1253,7 @@ fn run_with() -> Result<()> { + anyio==4.2.0 + idna==3.6 + sniffio==1.3.0 - "###); + "); // If the dependencies can't be resolved, we should reference `--with`. uv_snapshot!(context.filters(), context.run().arg("--with").arg("add").arg("main.py"), @r###" @@ -4024,17 +4031,8 @@ fn run_active_script_environment() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v1/main-\w+", - "environments-v1/main-[HASH]", - )]) - .collect::>(); - // Running `uv run --script` with `VIRTUAL_ENV` should _not_ warn. - uv_snapshot!(&filters, context.run() + uv_snapshot!(context.filters(), context.run() .arg("--script") .arg("main.py") .env(EnvVars::VIRTUAL_ENV, "foo"), @r###" @@ -4051,7 +4049,7 @@ fn run_active_script_environment() -> Result<()> { "###); // Using `--no-active` should also _not_ warn. - uv_snapshot!(&filters, context.run() + uv_snapshot!(context.filters(), context.run() .arg("--no-active") .arg("--script") .arg("main.py") @@ -4070,7 +4068,7 @@ fn run_active_script_environment() -> Result<()> { .assert(predicate::path::missing()); // Using `--active` should create the environment - uv_snapshot!(&filters, context.run() + uv_snapshot!(context.filters(), context.run() .arg("--active") .arg("--script") .arg("main.py") @@ -4092,7 +4090,7 @@ fn run_active_script_environment() -> Result<()> { .assert(predicate::path::is_dir()); // Requesting a different Python version should invalidate the environment - uv_snapshot!(&filters, context.run() + uv_snapshot!(context.filters(), context.run() .arg("--active") .arg("-p").arg("3.12") .arg("--script") diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index b30f7d6b2..4c417e420 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -71,7 +71,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -257,7 +261,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -444,7 +452,11 @@ fn resolve_uv_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -663,7 +675,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -851,7 +867,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -1015,7 +1035,11 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -1228,7 +1252,11 @@ fn resolve_index_url() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -1449,7 +1477,11 @@ fn resolve_index_url() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -1728,7 +1760,11 @@ fn resolve_find_links() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -1938,7 +1974,11 @@ fn resolve_top_level() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -2107,7 +2147,11 @@ fn resolve_top_level() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -2326,7 +2370,11 @@ fn resolve_top_level() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -2568,7 +2616,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -2727,7 +2779,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -2886,7 +2942,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -3047,7 +3107,11 @@ fn resolve_user_configuration() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -3227,7 +3291,11 @@ fn resolve_tool() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -3398,7 +3466,11 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -3591,7 +3663,11 @@ fn resolve_both() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -3822,7 +3898,11 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -4132,7 +4212,11 @@ fn resolve_config_file() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -4418,7 +4502,11 @@ fn resolve_skip_empty() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -4580,7 +4668,11 @@ fn resolve_skip_empty() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -4761,7 +4853,11 @@ fn allow_insecure_host() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -4934,7 +5030,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -5155,7 +5255,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -5382,7 +5486,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -5604,7 +5712,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -5833,7 +5945,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6055,7 +6171,11 @@ fn index_priority() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6290,7 +6410,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6442,7 +6566,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6592,7 +6720,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6744,7 +6876,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -6894,7 +7030,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -7045,7 +7185,11 @@ fn verify_hashes() -> anyhow::Result<()> { installs: 8, }, show_settings: true, - preview: Disabled, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, python_preference: Managed, python_downloads: Automatic, no_progress: false, @@ -7175,3 +7319,639 @@ fn verify_hashes() -> anyhow::Result<()> { Ok(()) } + +/// Test preview feature flagging. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn preview_features() { + let context = TestContext::new("3.12"); + + let cmd = || { + let mut cmd = context.version(); + cmd.arg("--show-settings"); + add_shared_args(cmd, context.temp_dir.path()) + }; + + uv_snapshot!(context.filters(), cmd().arg("--preview"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + uv_snapshot!(context.filters(), cmd().arg("--preview").arg("--no-preview"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + uv_snapshot!(context.filters(), cmd().arg("--preview").arg("--preview-features").arg("python-install-default"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | EXTRA_BUILD_DEPENDENCIES, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + uv_snapshot!(context.filters(), cmd().arg("--preview-features").arg("python-install-default,python-upgrade"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + uv_snapshot!(context.filters(), cmd().arg("--preview-features").arg("python-install-default").arg("--preview-feature").arg("python-upgrade"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); + + uv_snapshot!(context.filters(), cmd() + .arg("--preview-features").arg("python-install-default").arg("--preview-feature").arg("python-upgrade") + .arg("--no-preview"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + required_version: None, + quiet: 0, + verbose: 0, + color: Auto, + network_settings: NetworkSettings { + connectivity: Online, + native_tls: false, + allow_insecure_host: [], + }, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + show_settings: true, + preview: Preview { + flags: PreviewFeatures( + 0x0, + ), + }, + python_preference: Managed, + python_downloads: Automatic, + no_progress: false, + installer_metadata: true, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + VersionSettings { + value: None, + bump: [], + short: false, + output_format: Text, + dry_run: false, + locked: false, + frozen: false, + active: None, + no_sync: false, + package: None, + python: None, + install_mirrors: PythonInstallMirrors { + python_install_mirror: None, + pypy_install_mirror: None, + python_downloads_json_url: None, + }, + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: ResolverInstallerSettings { + resolver: ResolverSettings { + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + config_setting: ConfigSettings( + {}, + ), + config_settings_package: PackageConfigSettings( + {}, + ), + dependency_metadata: DependencyMetadata( + {}, + ), + exclude_newer: None, + fork_strategy: RequiresPython, + index_locations: IndexLocations { + indexes: [], + flat_index: [], + no_index: false, + }, + index_strategy: FirstIndex, + keyring_provider: Disabled, + link_mode: Clone, + no_build_isolation: false, + no_build_isolation_package: [], + extra_build_dependencies: {}, + prerelease: IfNecessaryOrExplicit, + resolution: Highest, + sources: Enabled, + upgrade: None, + }, + compile_bytecode: false, + reinstall: None, + }, + } + + ----- stderr ----- + "# + ); +} diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 6c612fceb..597a9db87 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1655,7 +1655,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] @@ -1685,7 +1685,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error @@ -1754,7 +1754,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `bad-child @ file://[TEMP_DIR]/bad_child` ├─▶ The build backend returned an error @@ -1790,7 +1790,7 @@ fn sync_extra_build_dependencies() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] @@ -1866,7 +1866,7 @@ fn sync_extra_build_dependencies_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] @@ -1946,7 +1946,7 @@ fn sync_extra_build_dependencies_sources_from_child() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview` to disable this warning. + warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning. Resolved [N] packages in [TIME] × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error @@ -5487,17 +5487,8 @@ fn sync_active_script_environment() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-[a-z0-9]+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); - // Running `uv sync --script` with `VIRTUAL_ENV` should warn - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5519,7 +5510,7 @@ fn sync_active_script_environment() -> Result<()> { .assert(predicate::path::missing()); // Using `--active` should create the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5539,7 +5530,7 @@ fn sync_active_script_environment() -> Result<()> { .assert(predicate::path::is_dir()); // A subsequent sync will re-use the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5551,7 +5542,7 @@ fn sync_active_script_environment() -> Result<()> { "); // Requesting another Python version will invalidate the environment - uv_snapshot!(&filters, context.sync() + uv_snapshot!(context.filters(), context.sync() .arg("--script") .arg("script.py") .env(EnvVars::VIRTUAL_ENV, "foo") @@ -5593,17 +5584,8 @@ fn sync_active_script_environment_json() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-[a-z0-9]+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); - // Running `uv sync --script` with `VIRTUAL_ENV` should warn - uv_snapshot!(&filters, context.sync() + uv_snapshot!(context.filters(), context.sync() .arg("--script").arg("script.py") .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo"), @r#" @@ -5649,7 +5631,7 @@ fn sync_active_script_environment_json() -> Result<()> { .assert(predicate::path::missing()); // Using `--active` should create the environment - uv_snapshot!(&filters, context.sync() + uv_snapshot!(context.filters(), context.sync() .arg("--script").arg("script.py") .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r#" @@ -5693,7 +5675,7 @@ fn sync_active_script_environment_json() -> Result<()> { .assert(predicate::path::is_dir()); // A subsequent sync will re-use the environment - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").env(EnvVars::VIRTUAL_ENV, "foo").arg("--active"), @r" success: true exit_code: 0 ----- stdout ----- @@ -5705,7 +5687,7 @@ fn sync_active_script_environment_json() -> Result<()> { "); // Requesting another Python version will invalidate the environment - uv_snapshot!(&filters, context.sync() + uv_snapshot!(context.filters(), context.sync() .arg("--script").arg("script.py") .arg("--output-format").arg("json") .env(EnvVars::VIRTUAL_ENV, "foo") @@ -9591,16 +9573,7 @@ fn sync_script() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-\w+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); - - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9632,7 +9605,7 @@ fn sync_script() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9658,7 +9631,7 @@ fn sync_script() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9683,7 +9656,7 @@ fn sync_script() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9701,7 +9674,7 @@ fn sync_script() -> Result<()> { "); // `--locked` and `--frozen` should fail with helpful error messages. - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").arg("--locked"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9711,7 +9684,7 @@ fn sync_script() -> Result<()> { error: `uv sync --locked` requires a script lockfile; run `uv lock --script script.py` to lock the script "); - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--frozen"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").arg("--frozen"), @r" success: false exit_code: 2 ----- stdout ----- @@ -9741,17 +9714,8 @@ fn sync_locked_script() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-\w+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); - // Lock the script. - uv_snapshot!(&filters, context.lock().arg("--script").arg("script.py"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--script").arg("script.py"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -9811,7 +9775,7 @@ fn sync_locked_script() -> Result<()> { ); }); - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9841,7 +9805,7 @@ fn sync_locked_script() -> Result<()> { })?; // Re-run with `--locked`. - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").arg("--locked"), @r" success: false exit_code: 1 ----- stdout ----- @@ -9852,7 +9816,7 @@ fn sync_locked_script() -> Result<()> { The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. "); - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -9943,7 +9907,7 @@ fn sync_locked_script() -> Result<()> { })?; // Re-run with `--locked`. - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py").arg("--locked"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py").arg("--locked"), @r" success: false exit_code: 1 ----- stdout ----- @@ -9955,7 +9919,7 @@ fn sync_locked_script() -> Result<()> { The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`. "); - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -10000,16 +9964,7 @@ fn sync_script_with_compatible_build_constraints() -> Result<()> { "# })?; - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-\w+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); - - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -10035,14 +9990,6 @@ fn sync_script_with_incompatible_build_constraints() -> Result<()> { let context = TestContext::new("3.9"); let test_script = context.temp_dir.child("script.py"); - let filters = context - .filters() - .into_iter() - .chain(vec![( - r"environments-v2/script-\w+", - "environments-v2/script-[HASH]", - )]) - .collect::>(); // Incompatible build constraints. test_script.write_str(indoc! { r#" @@ -10061,7 +10008,7 @@ fn sync_script_with_incompatible_build_constraints() -> Result<()> { "# })?; - uv_snapshot!(&filters, context.sync().arg("--script").arg("script.py"), @r" + uv_snapshot!(context.filters(), context.sync().arg("--script").arg("script.py"), @r" success: false exit_code: 1 ----- stdout ----- diff --git a/crates/uv/tests/it/tool_install.rs b/crates/uv/tests/it/tool_install.rs index 6a2d38db8..0af2510fb 100644 --- a/crates/uv/tests/it/tool_install.rs +++ b/crates/uv/tests/it/tool_install.rs @@ -3629,3 +3629,206 @@ fn tool_install_mismatched_name() { error: Package name (`black`) provided with `--from` does not match install request (`flask`) "###); } + +/// When installing from an authenticated index, the credentials should be omitted from the receipt. +#[test] +fn tool_install_credentials() { + let context = TestContext::new("3.12") + .with_exclude_newer("2025-01-18T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `executable-application` + uv_snapshot!(context.filters(), context.tool_install() + .arg("executable-application") + .arg("--index") + .arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + executable-application==0.3.0 + Installed 1 executable: app + "###); + + tool_dir + .child("executable-application") + .assert(predicate::path::is_dir()); + tool_dir + .child("executable-application") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + let executable = bin_dir.child(format!("app{}", std::env::consts::EXE_SUFFIX)); + assert!(executable.exists()); + + // On Windows, we can't snapshot an executable file. + #[cfg(not(windows))] + insta::with_settings!({ + filters => context.filters(), + }, { + // Should run black in the virtual environment + assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###" + #![TEMP_DIR]/tools/executable-application/bin/python + # -*- coding: utf-8 -*- + import sys + from executable_application import main + if __name__ == "__main__": + if sys.argv[0].endswith("-script.pyw"): + sys.argv[0] = sys.argv[0][:-11] + elif sys.argv[0].endswith(".exe"): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) + "###); + + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#" + [tool] + requirements = [{ name = "executable-application" }] + entrypoints = [ + { name = "app", install-path = "[TEMP_DIR]/bin/app" }, + ] + + [tool.options] + index = [{ url = "https://pypi-proxy.fly.dev/basic-auth/simple", explicit = false, default = false, format = "simple", authenticate = "auto" }] + exclude-newer = "2025-01-18T00:00:00Z" + "#); + }); +} + +/// When installing from an authenticated index, the credentials should be omitted from the receipt. +#[test] +fn tool_install_default_credentials() -> Result<()> { + let context = TestContext::new("3.12") + .with_exclude_newer("2025-01-18T00:00:00Z") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Write a `uv.toml` with a default index that has credentials. + let uv_toml = context.temp_dir.child("uv.toml"); + uv_toml.write_str(indoc::indoc! {r#" + [[index]] + url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/simple" + default = true + authenticate = "always" + "#})?; + + // Install `executable-application` + uv_snapshot!(context.filters(), context.tool_install() + .arg("executable-application") + .arg("--config-file") + .arg(uv_toml.as_os_str()) + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + executable-application==0.3.0 + Installed 1 executable: app + "###); + + tool_dir + .child("executable-application") + .assert(predicate::path::is_dir()); + tool_dir + .child("executable-application") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + let executable = bin_dir.child(format!("app{}", std::env::consts::EXE_SUFFIX)); + assert!(executable.exists()); + + // On Windows, we can't snapshot an executable file. + #[cfg(not(windows))] + insta::with_settings!({ + filters => context.filters(), + }, { + // Should run black in the virtual environment + assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###" + #![TEMP_DIR]/tools/executable-application/bin/python + # -*- coding: utf-8 -*- + import sys + from executable_application import main + if __name__ == "__main__": + if sys.argv[0].endswith("-script.pyw"): + sys.argv[0] = sys.argv[0][:-11] + elif sys.argv[0].endswith(".exe"): + sys.argv[0] = sys.argv[0][:-4] + sys.exit(main()) + "###); + + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#" + [tool] + requirements = [{ name = "executable-application" }] + entrypoints = [ + { name = "app", install-path = "[TEMP_DIR]/bin/app" }, + ] + + [tool.options] + index = [{ url = "https://pypi-proxy.fly.dev/basic-auth/simple", explicit = false, default = true, format = "simple", authenticate = "always" }] + exclude-newer = "2025-01-18T00:00:00Z" + "#); + }); + + // Attempt to upgrade without providing the credentials (from the config file). + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("executable-application") + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: Failed to upgrade executable-application + Caused by: Failed to fetch: `https://pypi-proxy.fly.dev/basic-auth/simple/executable-application/` + Caused by: Missing credentials for https://pypi-proxy.fly.dev/basic-auth/simple/executable-application/ + "); + + // Attempt to upgrade. + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("executable-application") + .arg("--config-file") + .arg(uv_toml.as_os_str()) + .env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str()) + .env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()) + .env(EnvVars::PATH, bin_dir.as_os_str()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Nothing to upgrade + "); + + Ok(()) +} diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index ce1af212d..e7288492f 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -31,7 +31,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the ```toml title="pyproject.toml" [build-system] -requires = ["uv_build>=0.8.2,<0.9.0"] +requires = ["uv_build>=0.8.3,<0.9.0"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 47791cec2..4b815b9bf 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.8.2/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.8.3/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.2/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.8.3/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 4046b009e..9077cb96e 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.8.2 AS uv +FROM ghcr.io/astral-sh/uv:0.8.3 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.8.2 AS uv +FROM ghcr.io/astral-sh/uv:0.8.3 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index b17ee0f1e..4ebd34ca1 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.8.2` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.8.3` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch version) @@ -75,7 +75,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.8.2-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.8.3-alpine`. In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin` to allow `uv tool install` to work as expected with the default user. @@ -116,7 +116,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.8.2 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.8.3 /uv /uvx /bin/ ``` !!! tip @@ -134,7 +134,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.8.2 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.8.2/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.8.3/install.sh /uv-installer.sh ``` ### Installing a project @@ -560,5 +560,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.8.2`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.8.3`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index 932c47033..582ac344d 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@v6 with: # Install a specific version of uv. - version: "0.8.2" + version: "0.8.3" ``` ## Setting up Python diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 2e83b4822..7bf393871 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.2 + rev: 0.8.3 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.2 + rev: 0.8.3 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.2 + rev: 0.8.3 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.2 + rev: 0.8.3 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.8.2 + rev: 0.8.3 hooks: # Compile requirements - id: pip-compile diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 6b93dbe7e..1958b8780 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -306,6 +306,10 @@ Equivalent to the `--prerelease` command-line argument. For example, if set to Equivalent to the `--preview` argument. Enables preview mode. +### `UV_PREVIEW_FEATURES` + +Equivalent to the `--preview-features` argument. Enables specific preview features. + ### `UV_PROJECT` Equivalent to the `--project` command-line argument. diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 7159eef1b..3cca74d95 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -447,7 +447,7 @@ data files are included by placing them in the Python module instead of using da with this package as build requirement use the include directory to find additional header files. - `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended - to uses these two options. + to use these two options. **Default value**: `{}` @@ -457,7 +457,7 @@ data files are included by placing them in the Python module instead of using da ```toml title="pyproject.toml" [tool.uv.build-backend] -data = { "headers": "include/headers", "scripts": "bin" } +data = { headers = "include/headers", scripts = "bin" } ``` --- diff --git a/pyproject.toml b/pyproject.toml index d18374587..547c4dd28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.8.2" +version = "0.8.3" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/uv.schema.json b/uv.schema.json index abda0af5e..eaf8d4104 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -670,7 +670,7 @@ "type": "object", "properties": { "data": { - "description": "Data includes for wheels.\n\nEach entry is a directory, whose contents are copied to the matching directory in the wheel\nin `-.data/(purelib|platlib|headers|scripts|data)`. Upon installation, this\ndata is moved to its target location, as defined by\n. Usually, small\ndata files are included by placing them in the Python module instead of using data includes.\n\n- `scripts`: Installed to the directory for executables, `/bin` on Unix or\n `\\Scripts` on Windows. This directory is added to `PATH` when the virtual\n environment is activated or when using `uv run`, so this data type can be used to install\n additional binaries. Consider using `project.scripts` instead for Python entrypoints.\n- `data`: Installed over the virtualenv environment root.\n\n Warning: This may override existing files!\n\n- `headers`: Installed to the include directory. Compilers building Python packages\n with this package as build requirement use the include directory to find additional header\n files.\n- `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended\n to uses these two options.", + "description": "Data includes for wheels.\n\nEach entry is a directory, whose contents are copied to the matching directory in the wheel\nin `-.data/(purelib|platlib|headers|scripts|data)`. Upon installation, this\ndata is moved to its target location, as defined by\n. Usually, small\ndata files are included by placing them in the Python module instead of using data includes.\n\n- `scripts`: Installed to the directory for executables, `/bin` on Unix or\n `\\Scripts` on Windows. This directory is added to `PATH` when the virtual\n environment is activated or when using `uv run`, so this data type can be used to install\n additional binaries. Consider using `project.scripts` instead for Python entrypoints.\n- `data`: Installed over the virtualenv environment root.\n\n Warning: This may override existing files!\n\n- `headers`: Installed to the include directory. Compilers building Python packages\n with this package as build requirement use the include directory to find additional header\n files.\n- `purelib` and `platlib`: Installed to the `site-packages` directory. It is not recommended\n to use these two options.", "allOf": [ { "$ref": "#/definitions/WheelDataIncludes"