From 14eccf10e8c40ad717d07f81835c4e06ec7412d6 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Wed, 24 Jun 2026 16:06:53 +0100 Subject: [PATCH] Use locked ty versions in uv check (#19884) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uv check` currently resolves its standalone `ty` executable from uv’s built-in compatible range, even when the project’s locked development dependencies select a different version or source. When `uv.lock` contains an applicable `ty` package, `uv check` now runs that exact locked package. It first looks in the project’s `dev` group, then falls back to a direct runtime dependency. With `--no-sync` (or when `ty` is excluded by the selected dependency groups), uv materializes only the locked `ty` subgraph into a cached environment without modifying the project environment. The precedence order for ty version selection is now: - the `TY` executable override - an explicit `--ty-version` - the applicable `ty` package from `uv.lock` (if any) - uv’s default constrained range when the lock contains no applicable `ty` development dependency This work was split into several focused pull requests, which are now dependencies of this PR: - [[#19907](https://github.com/astral-sh/uv/pull/19907)](https://github.com/astral-sh/uv/pull/19907) adds the locked-subgraph materialization and cached-installation primitives this PR uses. This PR is stacked on it. - [[#19909](https://github.com/astral-sh/uv/pull/19909)](https://github.com/astral-sh/uv/pull/19909) makes `uv check --no-sync` perform the normal lock operation while skipping environment synchronization. - [[#19892](https://github.com/astral-sh/uv/pull/19892)](https://github.com/astral-sh/uv/pull/19892) adds the hidden `uv check --show-version` flag used to verify version selection. - [[#19913](https://github.com/astral-sh/uv/pull/19913)](https://github.com/astral-sh/uv/pull/19913) moves related `uv check` tests from live PyPI dependencies to deterministic Packse fixtures. --- crates/uv-cli/src/lib.rs | 4 +- crates/uv-resolver/src/lock/mod.rs | 187 +++++++++++ crates/uv/src/commands/project/check.rs | 74 ++++- crates/uv/src/commands/project/environment.rs | 2 +- crates/uv/src/commands/project/mod.rs | 1 + crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/src/commands/project/toolchain.rs | 101 ++++++ crates/uv/tests/project/check.rs | 295 +++++++++++++++++- 8 files changed, 657 insertions(+), 9 deletions(-) create mode 100644 crates/uv/src/commands/project/toolchain.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index e7174c01a2703..d9686bf0eed21 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5410,7 +5410,9 @@ pub struct CheckArgs { /// Accepts either a version (e.g., `0.0.1`) which will be treated as an exact pin, /// a version specifier (e.g., `>=0.0.1`), or `latest` to use the latest available version. /// - /// By default, a constrained version range of ty will be used (e.g., `>=0.0,<0.1`). + /// By default, the exact version resolved in `uv.lock` will be used when `ty` is a project + /// dependency or a dependency in the project's `dev` group. Otherwise, a constrained version + /// range of ty will be used (e.g., `>=0.0,<0.1`). #[arg(long, value_hint = ValueHint::Other)] pub ty_version: Option, diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 4f83499c1c63a..6b32d5e07346e 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -796,6 +796,193 @@ impl Lock { &self.manifest.dependency_groups } + /// Returns the package selected by a direct dependency in a dependency group. + /// + /// If `project_name` is provided, the dependency group attached to that package is used. + /// Otherwise, the dependency group attached directly to the lock manifest is used. + pub fn find_dependency_group_package( + &self, + project_name: Option<&PackageName>, + group: &GroupName, + dependency_name: &PackageName, + marker_environment: &MarkerEnvironment, + ) -> Result, String> { + match project_name { + Some(project_name) => self.find_project_dependency_group_package( + project_name, + group, + dependency_name, + marker_environment, + ), + None => self.find_virtual_root_dependency_group_package( + group, + dependency_name, + marker_environment, + ), + } + } + + /// Returns `true` if the package is selected by an enabled dependency group. + pub fn is_package_in_dependency_groups( + &self, + project_name: Option<&PackageName>, + package: &Package, + marker_environment: &MarkerEnvironment, + groups: &DependencyGroupsWithDefaults, + ) -> Result { + match project_name { + Some(project_name) => { + let Some(project) = self.find_by_name(project_name)? else { + return Ok(false); + }; + for group in project + .resolved_dependency_groups() + .keys() + .filter(|group| groups.contains(group)) + { + if self.find_project_dependency_group_package( + project_name, + group, + package.name(), + marker_environment, + )? == Some(package) + { + return Ok(true); + } + } + } + None => { + for group in self + .manifest + .dependency_groups + .keys() + .filter(|group| groups.contains(group)) + { + if self.find_virtual_root_dependency_group_package( + group, + package.name(), + marker_environment, + )? == Some(package) + { + return Ok(true); + } + } + } + } + Ok(false) + } + + /// Returns the package selected by a dependency group on a virtual workspace root. + fn find_virtual_root_dependency_group_package( + &self, + group: &GroupName, + dependency_name: &PackageName, + marker_environment: &MarkerEnvironment, + ) -> Result, String> { + let Some(requirements) = self.manifest.dependency_groups.get(group) else { + return Ok(None); + }; + + // Confirm that the requested direct dependency applies to this environment before + // selecting a package with the same name from the universal lock. For example, + // `foo; python_version < '3.12'` must not select a locked `foo` on Python 3.12. + if !requirements.iter().any(|requirement| { + &requirement.name == dependency_name + && requirement.marker.evaluate(marker_environment, &[]) + }) { + return Ok(None); + } + self.find_by_markers(dependency_name, marker_environment) + } + + /// Returns the package selected by a dependency group on a non-virtual project. + fn find_project_dependency_group_package( + &self, + project_name: &PackageName, + group: &GroupName, + dependency_name: &PackageName, + marker_environment: &MarkerEnvironment, + ) -> Result, String> { + let Some(project) = self.find_by_name(project_name)? else { + return Ok(None); + }; + let Some(dependencies) = project.resolved_dependency_groups().get(group) else { + return Ok(None); + }; + + let mut selected = None; + for dependency in dependencies + .iter() + .filter(|dependency| &dependency.package_id.name == dependency_name) + { + // The complex marker combines the dependency's PEP 508 marker with uv's conflict + // markers. Evaluate it with this dependency's extras and the selected group active. + // For example, if this group declares `foo; sys_platform == 'linux'`, another + // dependency can still keep `foo` in the universal lock on macOS; this group's edge + // must not match there. + if !dependency.complexified_marker.evaluate( + marker_environment, + std::iter::empty::<&PackageName>(), + dependency + .extra + .iter() + .map(|extra| (&dependency.package_id.name, extra)), + std::iter::once((project_name, group)), + ) { + continue; + } + + let package = self.find_by_id(&dependency.package_id); + if selected.is_some_and(|selected: &Package| selected.id != package.id) { + return Err(format!( + "found multiple packages matching `{dependency_name}` in dependency group `{group}` for `{project_name}`" + )); + } + selected = Some(package); + } + Ok(selected) + } + + /// Returns the package selected by a production dependency on a non-virtual project. + pub fn find_dependency_package( + &self, + project_name: &PackageName, + dependency_name: &PackageName, + marker_environment: &MarkerEnvironment, + ) -> Result, String> { + let Some(project) = self.find_by_name(project_name)? else { + return Ok(None); + }; + + let mut selected = None; + for dependency in project + .dependencies() + .iter() + .filter(|dependency| &dependency.package_id.name == dependency_name) + { + if !dependency.complexified_marker.evaluate( + marker_environment, + std::iter::once(project_name), + dependency + .extra + .iter() + .map(|extra| (&dependency.package_id.name, extra)), + std::iter::empty::<(&PackageName, &GroupName)>(), + ) { + continue; + } + + let package = self.find_by_id(&dependency.package_id); + if selected.is_some_and(|selected: &Package| selected.id != package.id) { + return Err(format!( + "found multiple packages matching production dependency `{dependency_name}` for `{project_name}`" + )); + } + selected = Some(package); + } + Ok(selected) + } + /// Returns the build constraints that were used to generate this lock. pub fn build_constraints(&self, root: &Path) -> Constraints { Constraints::from_requirements( diff --git a/crates/uv/src/commands/project/check.rs b/crates/uv/src/commands/project/check.rs index 51bbd27d66c9a..e8957fbfbbe2b 100644 --- a/crates/uv/src/commands/project/check.rs +++ b/crates/uv/src/commands/project/check.rs @@ -1,4 +1,5 @@ use std::path::{Path, PathBuf}; +use std::str::FromStr; use anyhow::Result; use tracing::debug; @@ -9,10 +10,11 @@ use uv_configuration::{ Concurrency, DependencyGroups, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, InstallOptions, }; -use uv_normalize::DefaultExtras; +use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName}; use uv_preview::{Preview, PreviewFeature}; use uv_python::{ - EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, + EnvironmentPreference, PythonDownloads, PythonEnvironment, PythonInstallation, + PythonPreference, PythonRequest, }; use uv_scripts::Pep723Script; use uv_settings::{MalwareCheckSettings, PythonInstallMirrors}; @@ -21,6 +23,7 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceEr use crate::commands::pip::loggers::{SummaryInstallLogger, SummaryResolveLogger}; use crate::commands::pip::operations::Modifications; +use crate::commands::project::environment::CachedEnvironment; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::lock_target::LockTarget; @@ -228,6 +231,7 @@ pub(crate) async fn check( // Select an environment and, if we found a project, sync it before running checks. let mut workspace_metadata = None; + let mut locked_ty_path = None; let venv_path = if let Some(script) = &script { let extras = extras.with_defaults(DefaultExtras::default()); let venv = if let Some(venv) = isolated_venv { @@ -486,6 +490,70 @@ pub(crate) async fn check( target.validate_extras(&extras)?; target.validate_groups(&groups)?; + if ty_path.is_none() + && ty_version.is_none() + && let Some(tool) = project::toolchain::find_locked_tool( + project, + result.lock(), + lock_interpreter, + &PackageName::from_str("ty")?, + &DEV_DEPENDENCIES, + &groups, + )? + { + locked_ty_path = Some(if !tool.requires_separate_environment() && !no_sync { + // Synchronization will install the locked tool into the selected project or + // isolated environment. + venv.scripts() + .join(format!("ty{}", std::env::consts::EXE_SUFFIX)) + } else { + // Do not modify the selected environment when synchronization is disabled or the + // locked tool is excluded from it. Install only the locked `ty` subgraph. + let base_interpreter = + CachedEnvironment::base_interpreter(lock_interpreter, cache)?; + let resolution = project::toolchain::resolution_from_lock( + project, + result.lock(), + tool.package(), + &base_interpreter, + &settings.resolver.build_options, + )?; + project::sync::store_credentials_from_target(target, &client_builder)?; + let ty_state = state.fork(); + let environment = match CachedEnvironment::from_resolution( + &resolution, + result + .lock() + .build_constraints(project.workspace().install_path()), + &base_interpreter, + &settings, + &client_builder, + &ty_state, + Box::new(SummaryInstallLogger), + installer_metadata, + &concurrency, + cache, + printer, + preview, + ) + .await + { + Ok(environment) => environment, + Err(ProjectError::Operation(err)) => { + return diagnostics::OperationDiagnostic::with_system_certs( + client_builder.system_certs(), + ) + .report(err) + .map_or(Ok(ExitStatus::Failure), |err| Err(err.into())); + } + Err(err) => return Err(err.into()), + }; + PythonEnvironment::from(environment) + .scripts() + .join(format!("ty{}", std::env::consts::EXE_SUFFIX)) + }); + } + if no_sync { debug!("Skipping environment synchronization due to `--no-sync`"); } else { @@ -563,7 +631,7 @@ pub(crate) async fn check( ty::run( ty_version, - ty_path, + ty_path.or(locked_ty_path), &target_dir, script.as_ref().map(|script| script.path.as_path()), venv_path.as_deref(), diff --git a/crates/uv/src/commands/project/environment.rs b/crates/uv/src/commands/project/environment.rs index 595a9229fa9e1..d910f0c4e6ecb 100644 --- a/crates/uv/src/commands/project/environment.rs +++ b/crates/uv/src/commands/project/environment.rs @@ -311,7 +311,7 @@ impl CachedEnvironment { /// /// When caching, always use the base interpreter, rather than that of the virtual /// environment. - fn base_interpreter( + pub(super) fn base_interpreter( interpreter: &Interpreter, cache: &Cache, ) -> Result { diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 4507dcd7b0ad7..94adb789d12ac 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -77,6 +77,7 @@ pub(crate) mod lock_target; pub(crate) mod remove; pub(crate) mod run; pub(crate) mod sync; +pub(crate) mod toolchain; pub(crate) mod tree; pub(crate) mod upgrade; pub(crate) mod version; diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 777a93bc1772d..ba76d6ff92ce5 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -1025,7 +1025,7 @@ fn apply_no_virtual_project(resolution: Resolution) -> Resolution { /// /// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`, /// `project.dependencies`, and `project.optional-dependencies`. -fn store_credentials_from_target( +pub(super) fn store_credentials_from_target( target: InstallTarget<'_>, client_builder: &BaseClientBuilder, ) -> Result<()> { diff --git a/crates/uv/src/commands/project/toolchain.rs b/crates/uv/src/commands/project/toolchain.rs new file mode 100644 index 0000000000000..1016eb589b844 --- /dev/null +++ b/crates/uv/src/commands/project/toolchain.rs @@ -0,0 +1,101 @@ +use anyhow::Result; + +use uv_configuration::{ + BuildOptions, DependencyGroupsWithDefaults, ExtrasSpecification, InstallOptions, +}; +use uv_distribution_types::Resolution; +use uv_normalize::{DefaultExtras, GroupName, PackageName}; +use uv_python::Interpreter; +use uv_resolver::{Lock, Package}; +use uv_workspace::VirtualProject; + +use crate::commands::pip::{resolution_markers, resolution_tags}; + +/// A locked package selected for use as a project tool. +pub(crate) struct LockedTool<'lock> { + package: &'lock Package, + requires_separate_environment: bool, +} + +impl<'lock> LockedTool<'lock> { + pub(crate) fn package(&self) -> &'lock Package { + self.package + } + + /// Returns `true` if the tool must be installed outside the selected project environment. + pub(crate) fn requires_separate_environment(&self) -> bool { + self.requires_separate_environment + } +} + +/// Find a tool in a dependency group, falling back to the current project's production +/// dependencies. +pub(crate) fn find_locked_tool<'lock>( + project: &VirtualProject, + lock: &'lock Lock, + interpreter: &Interpreter, + package_name: &PackageName, + dependency_group: &GroupName, + groups: &DependencyGroupsWithDefaults, +) -> Result>> { + let marker_environment = interpreter.resolver_marker_environment(); + let group_package = lock + .find_dependency_group_package( + project.project_name(), + dependency_group, + package_name, + marker_environment.markers(), + ) + .map_err(anyhow::Error::msg)?; + let production_package = if group_package.is_none() + && let Some(project_name) = project.project_name() + { + lock.find_dependency_package(project_name, package_name, marker_environment.markers()) + .map_err(anyhow::Error::msg)? + } else { + None + }; + let Some(package) = group_package.or(production_package) else { + return Ok(None); + }; + + let installed_by_production = groups.prod() && production_package == Some(package); + let installed_by_group = lock + .is_package_in_dependency_groups( + project.project_name(), + package, + marker_environment.markers(), + groups, + ) + .map_err(anyhow::Error::msg)?; + + Ok(Some(LockedTool { + package, + requires_separate_environment: !(installed_by_production || installed_by_group), + })) +} + +/// Materialize the exact dependency subgraph rooted at a locked package. +pub(crate) fn resolution_from_lock( + project: &VirtualProject, + lock: &Lock, + package: &Package, + interpreter: &Interpreter, + build_options: &BuildOptions, +) -> Result { + let marker_environment = resolution_markers(None, None, interpreter); + let tags = resolution_tags(None, None, interpreter)?; + let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default()); + let groups = DependencyGroupsWithDefaults::none(); + Ok(lock.to_resolution( + project.workspace().install_path(), + [package], + project.project_name(), + &marker_environment, + &tags, + &extras, + &groups, + build_options, + &InstallOptions::default(), + )?) +} diff --git a/crates/uv/tests/project/check.rs b/crates/uv/tests/project/check.rs index ae1d882a48c42..e76d2d282a146 100644 --- a/crates/uv/tests/project/check.rs +++ b/crates/uv/tests/project/check.rs @@ -494,6 +494,280 @@ fn check_no_sync_isolated_does_not_write_lock_or_sync() -> Result<()> { Ok(()) } +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_exact_ty_version_from_selected_included_group() -> Result<()> { + let context = + uv_test::test_context!("3.12").with_filter((r"ty 0\.0\.17(?: \([^)]*\))?", "ty 0.0.17")); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + typing = ["ty>=0.0.1"] + dev = [{ include-group = "typing" }] + + [tool.uv] + constraint-dependencies = ["ty==0.0.17"] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--no-default-groups") + .arg("--group") + .arg("typing") + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 0.0.17 + " + ); + + assert!(context.temp_dir.child("uv.lock").exists()); + assert!(context.site_packages().join("ty").exists()); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_ty_version_from_production_dependency() -> Result<()> { + let context = + uv_test::test_context!("3.12").with_filter((r"ty 0\.0\.16(?: \([^)]*\))?", "ty 0.0.16")); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = [ + "ty==0.0.17 ; python_version < '3.12'", + "ty==0.0.16 ; python_version >= '3.12'", + ] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 0.0.16 + " + ); + + assert!(context.site_packages().join("ty").exists()); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_ty_version_from_forked_lock() -> Result<()> { + let context = + uv_test::test_context!("3.12").with_filter((r"ty 0\.0\.17(?: \([^)]*\))?", "ty 0.0.17")); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = [] + + [dependency-groups] + dev = [ + "ty==0.0.16 ; python_version < '3.12'", + "ty==0.0.17 ; python_version >= '3.12'", + ] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 0.0.17 + " + ); + + assert!(context.site_packages().join("ty").exists()); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_workspace_ty_subgraph_from_lock() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + dev = ["ty"] + + [tool.uv.sources] + ty = { workspace = true } + + [tool.uv.workspace] + members = ["ty"] + "#})?; + let ty = context.temp_dir.child("ty"); + ty.create_dir_all()?; + ty.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "ty" + version = "1.2.3" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + ty = "ty:main" + + [build-system] + requires = ["uv_build>=0.7,<10000"] + build-backend = "uv_build" + "#})?; + let ty_package = ty.child("src").child("ty"); + ty_package.create_dir_all()?; + ty_package.child("__init__.py").write_str(indoc! {r#" + import sys + + def main(): + if "--version" in sys.argv: + print("ty 1.2.3") + else: + print("All checks passed!") + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context.check().arg("--no-sync").arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 1.2.3 + " + ); + + assert!(!context.site_packages().join("ty").exists()); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_virtual_root_uses_own_ty() -> Result<()> { + let context = + uv_test::test_context!("3.12").with_filter((r"ty 0\.0\.17(?: \([^)]*\))?", "ty 0.0.17")); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [dependency-groups] + dev = ["ty==0.0.17 ; python_version >= '3.12'"] + + [tool.uv.workspace] + members = ["member"] + "#})?; + let member = context.temp_dir.child("member"); + member.create_dir_all()?; + member.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "member" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = [] + + [dependency-groups] + dev = ["ty==0.0.16 ; python_version < '3.12'"] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + context + .lock() + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .assert() + .success(); + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--no-sync") + .arg("--show-version"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + Installed 1 package in [TIME] + Using ty 0.0.17 + " + ); + + Ok(()) +} + #[test] #[cfg(feature = "test-pypi")] fn check_uses_ty_from_environment() -> Result<()> { @@ -511,18 +785,32 @@ fn check_uses_ty_from_environment() -> Result<()> { .assert() .success(); - let ty = bin_dir.child(format!("ty{}", std::env::consts::EXE_SUFFIX)); + let ty_path = bin_dir.child(format!("ty{}", std::env::consts::EXE_SUFFIX)); + // `TY` takes precedence over both an explicit version and a locked project version. + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + dev = ["ty==0.0.16"] + "#})?; context.temp_dir.child("main.py").write_str("x = 1")?; uv_snapshot!( context.filters(), context .check() - .arg("--no-project") + .arg("--no-sync") .arg("--ty-version") .arg(">=999.0.0") .arg("--show-version") - .env(EnvVars::TY, ty.as_os_str()), + .env(EnvVars::TY, ty_path.as_os_str()), @" success: true exit_code: 0 @@ -632,6 +920,7 @@ fn check_passes_workspace_metadata_to_ty() -> Result<()> { fn check_no_sync_errors_on_invalid_lockfile() -> Result<()> { let context = uv_test::test_context!("3.12"); + // An explicit version bypasses implicit `ty` selection, but not project locking. context .temp_dir .child("pyproject.toml")