Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

Expand Down
187 changes: 187 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<&Package>, 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<bool, String> {
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<Option<&Package>, 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<Option<&Package>, 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<Option<&Package>, 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(
Expand Down
74 changes: 71 additions & 3 deletions crates/uv/src/commands/project/check.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::Result;
use tracing::debug;
Expand All @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Interpreter, uv_python::Error> {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
Loading
Loading