From 24d6472e6692272326436f7604c7795bfdeaa2e2 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Tue, 16 Jun 2026 17:14:38 +0100 Subject: [PATCH] Use locked ty versions in uv check --- crates/uv-cli/src/lib.rs | 4 +- crates/uv-resolver/src/lock/mod.rs | 58 +++ crates/uv/src/commands/project/check.rs | 30 +- crates/uv/src/commands/project/check/ty.rs | 132 ++++- crates/uv/tests/project/check.rs | 550 ++++++++++++++++++++- 5 files changed, 769 insertions(+), 5 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8bc087b038..f1dc808c36 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -5384,7 +5384,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 the project has an + /// active `ty` development dependency. 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 a2d648ed22..a2b1d1152b 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -775,6 +775,64 @@ 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> { + let Some(project_name) = project_name else { + let Some(requirements) = self.manifest.dependency_groups.get(group) else { + return Ok(None); + }; + if !requirements.iter().any(|requirement| { + &requirement.name == dependency_name + && requirement.marker.evaluate(marker_environment, &[]) + }) { + return Ok(None); + } + return self.find_by_markers(dependency_name, marker_environment); + }; + + 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 { + if &dependency.package_id.name != dependency_name + || !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 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 974ad3c193..63b48389aa 100644 --- a/crates/uv/src/commands/project/check.rs +++ b/crates/uv/src/commands/project/check.rs @@ -204,6 +204,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_version = None; let venv_path = if let Some(project) = &project { let extras = extras.with_defaults(DefaultExtras::default()); @@ -229,18 +230,34 @@ pub(crate) async fn check( .into_environment()? }; + let ty_declaration = if ty_path.is_none() && ty_version.is_none() { + ty::active_declaration(project, venv.interpreter(), &settings.resolver.sources)? + } else { + None + }; + let state = UniversalState::default(); let _environment_lock; let lock = if no_sync { debug!("Skipping environment synchronization due to `--no-sync`"); - match LockTarget::Workspace(project.workspace()).read().await { + let lock = match LockTarget::Workspace(project.workspace()).read().await { Ok(lock) => lock, + Err(err) if ty_declaration.is_some() => return Err(err.into()), Err(err) => { debug!("Failed to read lockfile; skipping workspace metadata: {err}"); None } + }; + if let Some(declaration) = ty_declaration.as_ref() { + let Some(lock) = lock.as_ref() else { + anyhow::bail!( + "The active `ty` development dependency requires an existing lockfile when `--no-sync` is used; update `uv.lock`, remove `--no-sync`, or use `--ty-version` or the `TY` environment variable" + ); + }; + locked_ty_version = Some(ty::version_from_lock(declaration, project, lock, &venv)?); } + lock } else { // Keep the environment locked through synchronization and metadata collection. _environment_lock = venv @@ -291,6 +308,15 @@ pub(crate) async fn check( Err(err) => return Err(err.into()), }; + if let Some(declaration) = ty_declaration.as_ref() { + locked_ty_version = Some(ty::version_from_lock( + declaration, + project, + result.lock(), + &venv, + )?); + } + let target = match project { VirtualProject::Project(project) => InstallTarget::Project { workspace: project.workspace(), @@ -381,7 +407,7 @@ pub(crate) async fn check( .map(|value| value.timestamp()); ty::run( - ty_version, + ty_version.or(locked_ty_version), ty_path, &target_dir, venv_path.as_deref(), diff --git a/crates/uv/src/commands/project/check/ty.rs b/crates/uv/src/commands/project/check/ty.rs index c087682c88..a58b1e99dd 100644 --- a/crates/uv/src/commands/project/check/ty.rs +++ b/crates/uv/src/commands/project/check/ty.rs @@ -3,7 +3,7 @@ use std::process::Stdio; use std::str::FromStr; use std::time::Duration; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use tokio::io::AsyncWriteExt; use tokio::process::{ChildStdin, Command}; use tracing::debug; @@ -11,6 +11,13 @@ use tracing::debug; use uv_bin_install::{BinVersion, Binary, ResolvedVersion, bin_install, find_matching_version}; use uv_cache::Cache; use uv_client::BaseClientBuilder; +use uv_configuration::NoSources; +use uv_normalize::{DEV_DEPENDENCIES, PackageName}; +use uv_python::{Interpreter, PythonEnvironment}; +use uv_resolver::Lock; +use uv_workspace::VirtualProject; +use uv_workspace::dependency_groups::FlatDependencyGroups; +use uv_workspace::pyproject::{Source, ToolUvSources}; use crate::child::run_to_completion; use crate::commands::ExitStatus; @@ -20,6 +27,129 @@ use crate::printer::Printer; /// Limit how long uv can block if a version of ty does not consume metadata from stdin. const WORKSPACE_METADATA_WRITE_TIMEOUT: Duration = Duration::from_mins(1); +/// An active `ty` declaration in the current project's development dependencies. +pub(super) struct ActiveDeclaration { + package_name: PackageName, +} + +/// Find and validate an active `ty` declaration for the selected interpreter. +pub(super) fn active_declaration( + project: &VirtualProject, + interpreter: &Interpreter, + no_sources: &NoSources, +) -> Result> { + let package_name = PackageName::from_str("ty")?; + let dependency_groups = + FlatDependencyGroups::from_pyproject_toml(project.root(), project.pyproject_toml())?; + let Some(dev_dependencies) = dependency_groups.get(&DEV_DEPENDENCIES) else { + return Ok(None); + }; + + let active_requirements = dev_dependencies + .requirements + .iter() + .filter(|requirement| { + requirement.name == package_name + && requirement.evaluate_markers(interpreter.markers(), &[]) + }) + .collect::>(); + if active_requirements.is_empty() { + return Ok(None); + } + + let source = if no_sources.for_package(&package_name) { + None + } else { + project + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner) + .and_then(|sources| sources.get(&package_name)) + .or_else(|| project.workspace().sources().get(&package_name)) + .and_then(|sources| { + sources.iter().find(|source| { + source.extra().is_none() + && source + .group() + .is_none_or(|group| group == &*DEV_DEPENDENCIES) + && source.marker().evaluate(interpreter.markers(), &[]) + }) + }) + }; + + match source { + Some(Source::Registry { .. }) => {} + Some(source) => { + bail!( + "The active `ty` development dependency uses the non-registry source `{}`, but `uv check` can only install standalone `ty` releases by version; use a registry source, `--ty-version`, or the `TY` environment variable", + source_reference(source) + ); + } + None if let Some(url) = active_requirements.iter().find_map(|requirement| { + let Some(uv_pep508::VersionOrUrl::Url(url)) = requirement.version_or_url.as_ref() + else { + return None; + }; + Some(url) + }) => + { + bail!( + "The active `ty` development dependency uses the direct URL `{url}`, but `uv check` can only install standalone `ty` releases by version; use a registry requirement, `--ty-version`, or the `TY` environment variable" + ); + } + None => {} + } + + Ok(Some(ActiveDeclaration { package_name })) +} + +fn source_reference(source: &Source) -> String { + match source { + Source::Git { git, .. } => git.to_string(), + Source::Url { url, .. } => url.to_string(), + Source::Path { path, .. } => path.to_string(), + Source::Registry { index, .. } => index.to_string(), + Source::Workspace { workspace, .. } => format!("workspace = {workspace}"), + } +} + +/// Select the exact registry version of `ty` reachable from the current project in the lockfile. +pub(super) fn version_from_lock( + declaration: &ActiveDeclaration, + project: &VirtualProject, + lock: &Lock, + environment: &PythonEnvironment, +) -> Result { + let marker_environment = environment.interpreter().resolver_marker_environment(); + let package = lock + .find_dependency_group_package( + project.project_name(), + &DEV_DEPENDENCIES, + &declaration.package_name, + marker_environment.markers(), + ) + .map_err(anyhow::Error::msg)?; + let Some(package) = package else { + bail!( + "The active `ty` development dependency is not present in the lockfile for the selected Python environment; update `uv.lock`, or use `--ty-version` or the `TY` environment variable" + ); + }; + if package.index(project.workspace().install_path())?.is_none() { + bail!( + "The locked `ty` package uses a non-registry source, but `uv check` can only install standalone `ty` releases by version; use a registry source, `--ty-version`, or the `TY` environment variable" + ); + } + let Some(version) = package.version() else { + bail!( + "The locked `ty` package has no version, but `uv check` can only install standalone `ty` releases by version; use a registry source, `--ty-version`, or the `TY` environment variable" + ); + }; + Ok(version.to_string()) +} + async fn write_workspace_metadata(mut stdin: ChildStdin, workspace_metadata: String) -> Result<()> { match tokio::time::timeout( WORKSPACE_METADATA_WRITE_TIMEOUT, diff --git a/crates/uv/tests/project/check.rs b/crates/uv/tests/project/check.rs index af65418918..834acb19c3 100644 --- a/crates/uv/tests/project/check.rs +++ b/crates/uv/tests/project/check.rs @@ -38,6 +38,536 @@ fn check_project() -> Result<()> { Ok(()) } +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_exact_ty_version_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] + typing = ["ty>=0.0.1"] + dev = [{ include-group = "typing" }] + + [tool.uv] + constraint-dependencies = ["ty==0.0.17"] + + [tool.uv.sources] + ty = { path = "does-not-exist" } + "#})?; + context + .lock() + .arg("--no-sources") + .env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z") + .assert() + .success(); + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--no-sources") + .arg("--no-sync") + .arg("--no-dev") + .arg("--no-build-package") + .arg("ty") + .arg("--no-binary-package") + .arg("ty") + .arg("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @" + 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. + DEBUG `--exclude-newer` is ignored for pinned version `0.0.17` + DEBUG Using `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + " + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_exact_ty_version_from_legacy_dev_dependencies() -> 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<0.0.18"] + + [tool.uv] + dev-dependencies = ["ty>=0.0.17"] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--no-dev") + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .arg("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + warning: The `tool.uv.dev-dependencies` field (used in `pyproject.toml`) is deprecated and will be removed in a future release; use `dependency-groups.dev` instead + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + DEBUG `--exclude-newer` is ignored for pinned version `0.0.17` + DEBUG Using `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + " + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_uses_applicable_ty_version_from_forked_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.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("--no-dev") + .arg("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @" + 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. + DEBUG `--exclude-newer` is ignored for pinned version `0.0.17` + DEBUG Using `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + " + ); + + Ok(()) +} + +#[test] +fn check_ignores_inactive_ty_declaration() -> 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>=999 ; python_version < '3.12'"] + "#})?; + context.temp_dir.child("main.py").write_str("x = 1")?; + + uv_snapshot!( + context.filters(), + context + .check() + .arg("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @" + 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. + DEBUG Resolved `ty@>=0.0, <0.1` to `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + " + ); + + Ok(()) +} + +#[test] +fn check_rejects_non_registry_ty_source() -> 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 = { path = "does-not-exist" } + "#})?; + + uv_snapshot!(context.filters(), context.check(), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency uses the non-registry source `does-not-exist`, but `uv check` can only install standalone `ty` releases by version; use a registry source, `--ty-version`, or the `TY` environment variable + "); + + 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 @ https://example.com/ty.whl"] + "#})?; + + uv_snapshot!(context.filters(), context.check(), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency uses the direct URL `https://example.com/ty.whl`, but `uv check` can only install standalone `ty` releases by version; use a registry requirement, `--ty-version`, or the `TY` environment variable + "); + + 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 @ https://example.com/ty.whl"] + + [tool.uv.sources] + ty = { index = "test" } + + [[tool.uv.index]] + name = "test" + url = "https://example.com/simple" + explicit = true + "#})?; + + // The registry override makes the inline URL registry-backed, so selection proceeds to the + // missing-lock error instead of rejecting the declaration as a direct URL. + uv_snapshot!(context.filters(), context.check().arg("--no-sync"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency requires an existing lockfile when `--no-sync` is used; update `uv.lock`, remove `--no-sync`, or use `--ty-version` or the `TY` environment variable + "); + + Ok(()) +} + +#[test] +fn check_active_ty_requires_lock_with_no_sync() -> 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==0.0.17"] + "#})?; + + uv_snapshot!(context.filters(), context.check().arg("--no-sync"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency requires an existing lockfile when `--no-sync` is used; update `uv.lock`, remove `--no-sync`, or use `--ty-version` or the `TY` environment variable + "); + + Ok(()) +} + +#[test] +fn check_active_ty_rejects_stale_frozen_lock() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + context.lock().assert().success(); + + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [dependency-groups] + dev = ["ty==0.0.17"] + "#})?; + + uv_snapshot!(context.filters(), context.check().arg("--frozen"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency is not present in the lockfile for the selected Python environment; update `uv.lock`, or use `--ty-version` or the `TY` environment variable + "); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_virtual_root_does_not_use_member_ty() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [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.12" + dependencies = [] + + [dependency-groups] + dev = ["ty==0.0.17"] + "#})?; + context + .lock() + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .assert() + .success(); + + pyproject_toml.write_str(indoc! {r#" + [dependency-groups] + dev = ["ty==0.0.17"] + + [tool.uv.workspace] + members = ["member"] + "#})?; + + uv_snapshot!(context.filters(), context.check().arg("--frozen"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning. + error: The active `ty` development dependency is not present in the lockfile for the selected Python environment; update `uv.lock`, or use `--ty-version` or the `TY` environment variable + "); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_virtual_root_uses_exact_ty_version_from_lock() -> Result<()> { + let context = uv_test::test_context!("3.12"); + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [dependency-groups] + dev = ["ty==0.0.17"] + + [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.12" + dependencies = [] + "#})?; + 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("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @r###" + 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. + DEBUG `--exclude-newer` is ignored for pinned version `0.0.17` + DEBUG Using `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + "### + ); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-pypi")] +fn check_member_ty_ignores_virtual_root_dev_dependencies() -> Result<()> { + let context = uv_test::test_context!("3.12"); + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [dependency-groups] + dev = ["iniconfig==2.0.0"] + + [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.12" + dependencies = [] + + [dependency-groups] + dev = ["ty==0.0.17"] + "#})?; + member.child("main.py").write_str("x = 1")?; + context + .lock() + .arg("--exclude-newer") + .arg("2026-02-15T00:00:00Z") + .assert() + .success(); + + // These mutually exclusive build settings would make the workspace root's development + // dependency unusable if it leaked into the member-only version extraction. + uv_snapshot!( + context.filters(), + context + .check() + .current_dir(member.path()) + .arg("--no-sync") + .arg("--no-build-package") + .arg("iniconfig") + .arg("--no-binary-package") + .arg("iniconfig") + .arg("--verbose") + .env(EnvVars::RUST_LOG, "uv::commands::project::check::ty=debug"), + @" + 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. + DEBUG `--exclude-newer` is ignored for pinned version `0.0.17` + DEBUG Using `ty==0.0.17` + DEBUG Passing workspace metadata to `ty check` via stdin + " + ); + + Ok(()) +} + #[test] #[cfg(feature = "test-pypi")] fn check_uses_ty_from_environment() -> Result<()> { @@ -56,13 +586,27 @@ fn check_uses_ty_from_environment() -> Result<()> { .success(); let ty = bin_dir.child(format!("ty{}", std::env::consts::EXE_SUFFIX)); + // `TY` takes precedence over both an explicit version and an unsupported project source. + 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 @ https://example.com/ty.whl"] + "#})?; 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("--verbose") @@ -130,6 +674,7 @@ fn check_passes_workspace_metadata_to_ty() -> Result<()> { fn check_no_sync_ignores_invalid_lockfile() -> Result<()> { let context = uv_test::test_context!("3.12"); + // An explicit version bypasses both project-source validation and lockfile parsing. context .temp_dir .child("pyproject.toml") @@ -139,6 +684,9 @@ fn check_no_sync_ignores_invalid_lockfile() -> Result<()> { version = "0.1.0" requires-python = ">=3.12" dependencies = [] + + [dependency-groups] + dev = ["ty @ https://example.com/ty.whl"] "#})?; context.temp_dir.child("uv.lock").write_str("invalid")?; context.temp_dir.child("main.py").write_str(indoc! {r"