uv check — CLI contract and user-facing args
The new ProjectCommand::Check(CheckArgs) variant and its full CheckArgs struct define what users can pass to uv check.
0 Contract
ProjectCommand::Check
Registers check as a project-level subcommand with the uv help check after-help link.
check as a project-level subcommand with the uv help check after-help link.after_long_help = "" )] Format(FormatArgs),+ /// Type check the project.+ ///+ /// Type checks Python code using ty. By default, all Python files in the project+ /// are checked. This command has the same behavior as running `ty check` in the project+ /// root.+ ///+ /// Additional arguments can be passed to ty after `--`.+ #[command(+ after_help = "Use `uv help check` for more details.",+ after_long_help = ""+ )]+ Check(CheckArgs), /// Audit the project's dependencies. /// /// Dependencies are audited for known vulnerabilities, as well as 'adverse' statuses such as
0 Contract
CheckArgs
Full set of flags: extras/groups for env sync, --locked/--frozen/--no-sync, --python, --version (ty version), --extra-args (pass-through to ty), --no-project, --show-version. Mirrors the shape of FormatArgs but adds sync-related flags since check needs a live environment.
FormatArgs but adds sync-related flags since check needs a live environment.pub show_version: bool, } +#[derive(Args)]+pub struct CheckArgs {+ /// Include optional dependencies from the specified extra name.+ ///+ /// May be provided more than once.+ ///+ /// When multiple extras or groups are specified that appear in `tool.uv.conflicts`, uv will+ /// report an error.+ ///+ /// Note that all optional dependencies are always included in the resolution; this option only+ /// affects the selection of packages to install.+ #[arg(+ long,+ conflicts_with = "all_extras",+ conflicts_with = "only_group",+ value_delimiter = ',',+ value_parser = extra_name_with_clap_error,+ value_hint = ValueHint::Other,+ )]+ pub extra: Option<Vec<ExtraName>>,+ + /// Include all optional dependencies.+ ///+ /// When two or more extras are declared as conflicting in `tool.uv.conflicts`, using this flag+ /// will always result in an error.+ ///+ /// Note that all optional dependencies are always included in the resolution; this option only+ /// affects the selection of packages to install.+ #[arg(long, conflicts_with = "extra", conflicts_with = "only_group")]+ pub all_extras: bool,+ + /// Exclude the specified optional dependencies, if `--all-extras` is supplied.+ ///+ /// May be provided multiple times.+ #[arg(long, value_hint = ValueHint::Other)]+ pub no_extra: Vec<ExtraName>,+ + #[arg(long, overrides_with("all_extras"), hide = true)]+ pub no_all_extras: bool,+ + /// Include the development dependency group [env: UV_DEV=]+ ///+ /// This option is an alias for `--group dev`.+ #[arg(long, overrides_with("no_dev"), hide = true, value_parser = clap::builder::BoolishValueParser::new())]+ pub dev: bool,+ + /// Disable the development dependency group [env: UV_NO_DEV=]+ ///+ /// This option is an alias of `--no-group dev`.+ /// See `--no-default-groups` to disable all default groups instead.+ #[arg(long, overrides_with("dev"), value_parser = clap::builder::BoolishValueParser::new())]+ pub no_dev: bool,+ + /// Only include the development dependency group.+ ///+ /// The project and its dependencies will be omitted.+ ///+ /// This option is an alias for `--only-group dev`. Implies `--no-default-groups`.+ #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])]+ pub only_dev: bool,+ + /// Include dependencies from the specified dependency group.+ ///+ /// When multiple extras or groups are specified that appear in+ /// `tool.uv.conflicts`, uv will report an error.+ ///+ /// May be provided multiple times.+ #[arg(long, conflicts_with_all = ["only_group", "only_dev"], value_hint = ValueHint::Other)]+ pub group: Vec<GroupName>,+ + /// Disable the specified dependency group [env: `UV_NO_GROUP`=]+ ///+ /// This option always takes precedence over default groups,+ /// `--all-groups`, and `--group`.+ ///+ /// May be provided multiple times.+ #[arg(long, value_delimiter = ' ', value_hint = ValueHint::Other)]+ pub no_group: Vec<GroupName>,+ + /// Ignore the default dependency groups.+ ///+ /// uv includes the groups defined in `tool.uv.default-groups` by default.+ /// This disables that option, however, specific groups can still be included with `--group`.+ #[arg(long, env = EnvVars::UV_NO_DEFAULT_GROUPS, value_parser = clap::builder::BoolishValueParser::new())]+ pub no_default_groups: bool,+ + /// Only include dependencies from the specified dependency group.+ ///+ /// The project and its dependencies will be omitted.+ ///+ /// May be provided multiple times. Implies `--no-default-groups`.+ #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"], value_hint = ValueHint::Other)]+ pub only_group: Vec<GroupName>,+ + /// Include dependencies from all dependency groups.+ ///+ /// `--no-group` can be used to exclude specific groups.+ #[arg(long, conflicts_with_all = ["only_group", "only_dev"])]+ pub all_groups: bool,+ + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=]+ ///+ /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated,+ /// uv will exit with an error.+ #[arg(long, conflicts_with_all = ["frozen", "upgrade"])]+ pub locked: bool,+ + /// Sync without updating the `uv.lock` file [env: UV_FROZEN=]+ ///+ /// Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the+ /// source of truth. If the lockfile is missing, uv will exit with an error. If the+ /// `pyproject.toml` includes changes to dependencies that have not been included in the+ /// lockfile yet, they will not be present in the environment.+ #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])]+ pub frozen: bool,+ + /// Avoid syncing the virtual environment [env: UV_NO_SYNC=]+ ///+ /// Implies `--frozen`, as the project dependencies will be ignored (i.e., the lockfile will not+ /// be updated, since the environment will not be synced regardless).+ #[arg(long)]+ pub no_sync: bool,+ + /// The Python interpreter to use for the project environment.+ ///+ /// By default, the first interpreter that meets the project's+ /// `requires-python` constraint is used.+ ///+ /// See `uv python` for more details on Python discovery and requests.+ #[arg(+ long,+ short,+ env = EnvVars::UV_PYTHON,+ value_parser = parse_maybe_string,+ value_hint = ValueHint::Other,+ )]+ pub python: Option<Maybe<String>>,+ + /// The version of ty to use for type checking.+ ///+ /// 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`).+ #[arg(long, value_hint = ValueHint::Other)]+ pub version: Option<String>,+ + /// Additional arguments to pass to ty.+ ///+ /// For example, use `uv check -- --warn-on error` to treat warnings as errors or+ /// `uv check -- src/module/foo.py` to check a specific file.+ #[arg(last = true, value_hint = ValueHint::Other)]+ pub extra_args: Vec<String>,+ + /// Avoid discovering a project or workspace.+ ///+ /// Instead of running the type checker in the context of the current project, run it in the+ /// context of the current directory. This is useful when the current directory is not a+ /// project.+ #[arg(+ long,+ env = EnvVars::UV_NO_PROJECT,+ value_parser = clap::builder::BoolishValueParser::new()+ )]+ pub no_project: bool,+ + /// Display the version of ty that will be used for type checking.+ #[arg(long, hide = true)]+ pub show_version: bool,+ + #[command(flatten)]+ pub installer: ResolverInstallerArgs,+ + #[command(flatten)]+ pub build: BuildOptionsArgs,+ + #[command(flatten)]+ pub refresh: RefreshArgs,+}+ #[derive(Args)] pub struct AuditArgs { /// Don't audit the specified optional dependencies.
uv check — settings resolution (CLI → typed struct)
CheckSettings resolves the raw Clap args into a typed, normalized struct that the command handler consumes.
1 Entrypoint
CheckSettings
Introduces CheckSettings and its resolve() impl. Handles flag conflicts (locked vs frozen), dev/no-dev pair resolution, and propagates install mirrors and malware settings — the same resolution pattern used by sync/run.
CheckSettings and its resolve() impl. Handles flag conflicts (locked vs frozen), dev/no-dev pair resolution, and propagates install mirrors and malware settings — the same resolution pattern used by sync/run.ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBumpSpec, VersionFormat, }; use uv_cli::{- AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs,+ AuthorFrom, BuildArgs, CheckArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, ToolUpgradeArgs, options::{ Flag, FlagSource, check_conflicts, flag, resolve_flag, resolve_flag_pair,⋯ } } +/// The resolved settings to use for a `check` invocation.+#[derive(Debug, Clone)]+pub(crate) struct CheckSettings {+ pub(crate) extras: ExtrasSpecification,+ pub(crate) groups: DependencyGroups,+ pub(crate) lock_check: LockCheck,+ pub(crate) frozen: Option<FrozenSource>,+ pub(crate) no_sync: bool,+ pub(crate) python: Option<String>,+ pub(crate) install_mirrors: PythonInstallMirrors,+ pub(crate) refresh: Refresh,+ pub(crate) settings: ResolverInstallerSettings,+ pub(crate) extra_args: Vec<String>,+ pub(crate) version: Option<String>,+ pub(crate) no_project: bool,+ pub(crate) show_version: bool,+ pub(crate) malware_settings: MalwareCheckSettings,+}+ ⋯+impl CheckSettings {+ /// Resolve the [`CheckSettings`] from the CLI and filesystem configuration.+ pub(crate) fn resolve(+ args: CheckArgs,+ filesystem: Option<FilesystemOptions>,+ environment: EnvironmentOptions,+ ) -> Self {+ let CheckArgs {+ extra,+ all_extras,+ no_extra,+ no_all_extras,+ dev,+ no_dev,+ only_dev,+ group,+ no_group,+ no_default_groups,+ only_group,+ all_groups,+ locked,+ frozen,+ no_sync,+ python,+ version,+ extra_args,+ no_project,+ show_version,+ installer,+ build,+ refresh,+ } = args;+ + let filesystem_install_mirrors = filesystem+ .clone()+ .map(|fs| fs.install_mirrors.clone())+ .unwrap_or_default();+ + let locked = resolve_flag(locked, "locked", environment.locked);+ let frozen = resolve_flag(frozen, "frozen", environment.frozen);+ let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);+ check_conflicts(locked, frozen);+ + let (dev, no_dev) = resolve_flag_pair(+ dev,+ no_dev,+ "dev",+ "no-dev",+ Some(environment.dev),+ Some(environment.no_dev),+ );+ let settings = ResolverInstallerSettings::combine(+ resolver_installer_options(installer, build),+ filesystem,+ &environment,+ );+ let malware_settings = MalwareCheckSettings::from(&environment);+ + Self {+ extras: ExtrasSpecification::from_args(+ extra.unwrap_or_default(),+ no_extra,+ false,+ vec![],+ flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(),+ ),+ groups: DependencyGroups::from_args(+ dev.into(),+ no_dev.into(),+ only_dev,+ group,+ if no_group.is_empty() {+ environment.no_group.clone().unwrap_or_default()+ } else {+ no_group+ },+ no_default_groups,+ only_group,+ all_groups,+ ),+ lock_check: resolve_lock_check(locked),+ frozen: resolve_frozen(frozen),+ no_sync: no_sync.is_enabled(),+ python: python.and_then(Maybe::into_option),+ install_mirrors: environment+ .install_mirrors+ .combine(filesystem_install_mirrors),+ refresh: Refresh::from(refresh),+ settings,+ extra_args,+ version,+ no_project,+ show_version,+ malware_settings,+ }+ }+}+ /// The resolved settings to use for an `audit` invocation. #[derive(Debug, Clone)] pub(crate) struct AuditSettings {
uv check — command dispatch in run_project
The match arm in run_project wires CheckSettings into the check() command call.
2 Application
ProjectCommand::Check dispatch
Resolves settings, checks refresh/offline conflicts, initialises the cache with combined refresh, then calls commands::check with all resolved arguments. Passes client_builder.subcommand(["check"]) so network attribution is correct.
)) .await }+ ProjectCommand::Check(args) => {+ // Resolve the settings from the command-line arguments and workspace configuration.+ let args = settings::CheckSettings::resolve(args, filesystem, environment);+ show_settings!(args);+ + // Check for conflicts between offline and refresh.+ globals+ .network_settings+ .check_refresh_conflict(&args.refresh);+ + // Initialize the cache.+ let cache = cache.init().await?.with_refresh(+ args.refresh+ .combine(Refresh::from(args.settings.reinstall.clone()))+ .combine(Refresh::from(args.settings.resolver.upgrade.clone())),+ );+ + Box::pin(commands::check(+ project_dir,+ args.lock_check,+ args.frozen,+ args.no_sync,+ args.extras,+ args.groups,+ args.python,+ args.install_mirrors,+ args.settings,+ args.extra_args,+ args.version,+ args.show_version,+ client_builder.subcommand(vec!["check".to_owned()]),+ globals.python_preference,+ globals.python_downloads,+ globals.installer_metadata,+ globals.concurrency,+ &cache,+ workspace_cache,+ printer,+ globals.preview,+ args.no_project,+ no_config,+ args.malware_settings,+ ))+ .await+ } ProjectCommand::Audit(audit_args) => { let args = settings::AuditSettings::resolve(audit_args, filesystem, environment); show_settings!(args);
2 Application
mod / pub(crate) use exports
Declares and re-exports the check module so the dispatch arm can call commands::check.
pub(crate) use pip::uninstall::pip_uninstall; pub(crate) use project::add::add; pub(crate) use project::audit::audit;+pub(crate) use project::check::check; pub(crate) use project::export::export; pub(crate) use project::format::format; pub(crate) use project::init::{InitKind, InitProjectKind, init};⋯ pub(crate) mod add; pub(crate) mod audit;+pub(crate) mod check; pub(crate) mod environment; pub(crate) mod export; pub(crate) mod format;
uv check — core command implementation
The async check() function: project discovery, optional env sync, ty download, and exec.
3 Domain
check()
Implements the full check pipeline in one function: (1) warn if not in preview mode; (2) discover project or fall back to cwd; (3) warn when project-only flags are used without a project; (4) if a project was found, get-or-init the virtual env and run lock+sync (same path as uv run); (5) resolve ty version from BinVersion enum (default constraints >=0.0,<0.1, pinned, latest, or constraint); (6) download and cache ty via bin_install; (7) spawn ty check in the project root with VIRTUAL_ENV set, forwarding extra_args. The venv path is passed as VIRTUAL_ENV so ty can find installed stubs.
uv run); (5) resolve ty version from BinVersion enum (default constraints >=0.0,<0.1, pinned, latest, or constraint); (6) download and cache ty via bin_install; (7) spawn ty check in the project root with VIRTUAL_ENV set, forwarding extra_args. The venv path is passed as VIRTUAL_ENV so ty can find installed stubs.+use std::fmt::Write;+use std::path::Path;+use std::str::FromStr;+ ⋯+use anyhow::{Context, Result};+use tokio::process::Command;+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::{+ Concurrency, DependencyGroups, DryRun, ExtrasSpecification, InstallOptions,+};⋯+use uv_normalize::DefaultExtras;+use uv_preview::{Preview, PreviewFeature};+use uv_python::{PythonDownloads, PythonPreference, PythonRequest};+use uv_settings::{MalwareCheckSettings, PythonInstallMirrors};+use uv_warnings::warn_user;+use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError};+ ⋯+use crate::child::run_to_completion;+use crate::commands::pip::loggers::{SummaryInstallLogger, SummaryResolveLogger};+use crate::commands::pip::operations::Modifications;+use crate::commands::project::install_target::InstallTarget;+use crate::commands::project::lock::LockMode;+use crate::commands::project::{+ ProjectEnvironment, ProjectError, UniversalState, default_dependency_groups,+};⋯+use crate::commands::reporters::BinaryDownloadReporter;+use crate::commands::{ExitStatus, diagnostics, project};+use crate::printer::Printer;+use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings};+ ⋯+/// Run the type checker.+#[expect(clippy::fn_params_excessive_bools)]+pub(crate) async fn check(+ project_dir: &Path,+ lock_check: LockCheck,+ frozen: Option<FrozenSource>,+ no_sync: bool,+ extras: ExtrasSpecification,+ groups: DependencyGroups,+ python: Option<String>,+ install_mirrors: PythonInstallMirrors,+ settings: ResolverInstallerSettings,+ extra_args: Vec<String>,+ version: Option<String>,+ show_version: bool,+ client_builder: BaseClientBuilder<'_>,+ python_preference: PythonPreference,+ python_downloads: PythonDownloads,+ installer_metadata: bool,+ concurrency: Concurrency,+ cache: &Cache,+ workspace_cache: &WorkspaceCache,+ printer: Printer,+ preview: Preview,+ no_project: bool,+ no_config: bool,+ malware_settings: MalwareCheckSettings,+) -> Result<ExitStatus> {+ if !preview.is_enabled(PreviewFeature::Check) {+ warn_user!(+ "`uv check` is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",+ PreviewFeature::Check+ );+ }+ + // Discover the project.+ let project = if no_project {+ None+ } else {+ match VirtualProject::discover(project_dir, &DiscoveryOptions::default(), workspace_cache)+ .await+ {+ Ok(project) => Some(project),+ Err(+ WorkspaceError::MissingPyprojectToml+ | WorkspaceError::MissingProject(_)+ | WorkspaceError::NonWorkspace(_),+ ) => None,+ Err(err) => return Err(err.into()),+ }+ };+ + if no_project {+ for flag in extras.history().as_flags_pretty() {+ warn_user!("`{flag}` has no effect when used alongside `--no-project`");+ }+ for flag in groups.history().as_flags_pretty() {+ warn_user!("`{flag}` has no effect when used alongside `--no-project`");+ }+ if let LockCheck::Enabled(lock_check) = lock_check {+ warn_user!("`{lock_check}` has no effect when used alongside `--no-project`");+ }+ if frozen.is_some() {+ warn_user!("`--frozen` has no effect when used alongside `--no-project`");+ }+ if no_sync {+ warn_user!("`--no-sync` has no effect when used alongside `--no-project`");+ }+ } else if project.is_none() {+ for flag in extras.history().as_flags_pretty() {+ warn_user!("`{flag}` has no effect when used outside of a project");+ }+ for flag in groups.history().as_flags_pretty() {+ warn_user!("`{flag}` has no effect when used outside of a project");+ }+ if let LockCheck::Enabled(lock_check) = lock_check {+ warn_user!("`{lock_check}` has no effect when used outside of a project");+ }+ if frozen.is_some() {+ warn_user!("`--frozen` has no effect when used outside of a project");+ }+ if no_sync {+ warn_user!("`--no-sync` has no effect when used outside of a project");+ }+ }+ + let target_dir = project+ .as_ref()+ .map(|p| p.root().to_owned())+ .unwrap_or_else(|| project_dir.to_owned());+ + // If we found a project, sync the environment before type checking.+ let venv_path = if let Some(project) = &project {+ let default_groups = default_dependency_groups(project.pyproject_toml())?;+ let default_extras = DefaultExtras::default();+ let groups = groups.with_defaults(default_groups);+ let extras = extras.with_defaults(default_extras);+ + let venv = ProjectEnvironment::get_or_init(+ project.workspace(),+ &groups,+ python.as_deref().map(PythonRequest::parse),+ &install_mirrors,+ &client_builder,+ python_preference,+ python_downloads,+ no_sync,+ no_config,+ None,+ cache,+ DryRun::Disabled,+ printer,+ preview,+ )+ .await?+ .into_environment()?;+ + if no_sync {+ debug!("Skipping environment synchronization due to `--no-sync`");+ } else {+ let _lock = venv+ .lock()+ .await+ .inspect_err(|err| {+ tracing::warn!("Failed to acquire environment lock: {err}");+ })+ .ok();+ + let lock_state = UniversalState::default();+ let sync_state = lock_state.fork();+ + let mode = if let Some(frozen_source) = frozen {+ LockMode::Frozen(frozen_source.into())+ } else if let LockCheck::Enabled(lock_check) = lock_check {+ LockMode::Locked(venv.interpreter(), lock_check)+ } else {+ LockMode::Write(venv.interpreter())+ };+ + let result = match Box::pin(+ project::lock::LockOperation::new(+ mode,+ &settings.resolver,+ &client_builder,+ &lock_state,+ Box::new(SummaryResolveLogger),+ &concurrency,+ cache,+ workspace_cache,+ printer,+ preview,+ )+ .execute(project.workspace().into()),+ )+ .await+ {+ Ok(result) => result,+ 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()),+ };+ + let target = match project {+ VirtualProject::Project(project) => InstallTarget::Project {+ workspace: project.workspace(),+ name: project.project_name(),+ lock: result.lock(),+ },+ VirtualProject::NonProject(workspace) => InstallTarget::NonProjectWorkspace {+ workspace,+ lock: result.lock(),+ },+ };+ + target.validate_extras(&extras)?;+ target.validate_groups(&groups)?;+ + match project::sync::do_sync(+ target,+ &venv,+ &extras,+ &groups,+ None,+ InstallOptions::default(),+ Modifications::Sufficient,+ None,+ (&settings).into(),+ &client_builder,+ &sync_state,+ Box::new(SummaryInstallLogger),+ installer_metadata,+ &concurrency,+ cache,+ workspace_cache,+ DryRun::Disabled,+ printer,+ preview,+ &malware_settings,+ )+ .await+ {+ Ok(_) => {}+ 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()),+ }+ }+ + Some(venv.root().to_owned())+ } else {+ None+ };+ + // Download and install ty.+ let exclude_newer = settings+ .resolver+ .exclude_newer+ .global+ .map(|v| v.timestamp());+ let retry_policy = client_builder.retry_policy();+ let ty_client = client_builder.clone().retries(0).build()?;+ + let reporter = BinaryDownloadReporter::single(printer);+ let bin_version = version+ .as_deref()+ .map(BinVersion::from_str)+ .transpose()?+ .unwrap_or(BinVersion::Default);+ + let resolved = match bin_version {+ BinVersion::Default => {+ let constraints = Binary::Ty.default_constraints();+ let resolved = find_matching_version(+ Binary::Ty,+ Some(&constraints),+ exclude_newer,+ &ty_client,+ &retry_policy,+ )+ .await+ .with_context(|| {+ format!("Failed to find ty version matching default constraints: {constraints}")+ })?;+ debug!("Resolved `ty@{constraints}` to `ty=={}`", resolved.version);+ resolved+ }+ BinVersion::Pinned(version) => {+ if exclude_newer.is_some() {+ debug!("`--exclude-newer` is ignored for pinned version `{version}`");+ }+ ResolvedVersion::from_version(Binary::Ty, version)?+ }+ BinVersion::Latest => {+ let resolved =+ find_matching_version(Binary::Ty, None, exclude_newer, &ty_client, &retry_policy)+ .await+ .with_context(|| "Failed to find latest ty version")?;+ debug!("Resolved `ty@latest` to `ty=={}`", resolved.version);+ resolved+ }+ BinVersion::Constraint(constraints) => {+ let resolved = find_matching_version(+ Binary::Ty,+ Some(&constraints),+ exclude_newer,+ &ty_client,+ &retry_policy,+ )+ .await+ .with_context(|| format!("Failed to find ty version matching: {constraints}"))?;+ debug!("Resolved `ty@{constraints}` to `ty=={}`", resolved.version);+ resolved+ }+ };+ + if show_version {+ writeln!(printer.stderr(), "ty {}", resolved.version)?;+ }+ + let ty_path = bin_install(+ Binary::Ty,+ &resolved,+ &ty_client,+ &retry_policy,+ cache,+ &reporter,+ )+ .await+ .with_context(|| format!("Failed to install ty {}", resolved.version))?;+ + let mut command = Command::new(&ty_path);+ command.current_dir(&target_dir);+ command.arg("check");+ + if let Some(venv_path) = &venv_path {+ command.env("VIRTUAL_ENV", venv_path);+ }+ + command.args(extra_args.iter());+ + let handle = command.spawn().context("Failed to spawn `ty check`")?;+ run_to_completion(handle).await+}
Binary download support for ty (uv-bin-install)
Teaches the uv-bin-install crate about the ty binary: URL patterns, default version constraints, and mirror support.
4 Adapter
Binary::Ty — enum variant and constants
Adds the Ty variant to the Binary enum, its name ("ty"), default constraints (>=0.0,<0.1), and the two URL constants TY_GITHUB_URL_PREFIX and TY_MIRROR_SUFFIX. The default constraint range mirrors how Ruff is pinned: a narrow range that allows patch updates without a uv release.
Ty variant to the Binary enum, its name ("ty"), default constraints (>=0.0,<0.1), and the two URL constants TY_GITHUB_URL_PREFIX and TY_MIRROR_SUFFIX. The default constraint range mirrors how Ruff is pinned: a narrow range that allows patch updates without a uv release.#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Binary { Ruff,+ Ty, Uv, } ⋯ ] .into_iter() .collect(),+ Self::Ty => [+ VersionSpecifier::greater_than_equal_version(Version::new([0, 0])),+ VersionSpecifier::less_than_version(Version::new([0, 1])),+ ]+ .into_iter()+ .collect(), Self::Uv => VersionSpecifiers::empty(), } }⋯ pub fn name(&self) -> &'static str { match self { Self::Ruff => "ruff",+ Self::Ty => "ty", Self::Uv => "uv", } }⋯ /// The canonical GitHub URL prefix for Ruff releases. const RUFF_GITHUB_URL_PREFIX: &str = "https://github.com/astral-sh/ruff/releases/download/"; +/// The canonical GitHub URL prefix for ty releases.+const TY_GITHUB_URL_PREFIX: &str = "https://github.com/astral-sh/ty/releases/download/";+ /// The canonical GitHub URL prefix for uv releases. const UV_GITHUB_URL_PREFIX: &str = "https://github.com/astral-sh/uv/releases/download/"; ⋯ /// The suffix appended to the Astral mirror base for Ruff releases. const RUFF_MIRROR_SUFFIX: &str = "/github/ruff/releases/download/"; +/// The suffix appended to the Astral mirror base for ty releases.+const TY_MIRROR_SUFFIX: &str = "/github/ty/releases/download/";+ /// The suffix appended to the Astral mirror base for the versions manifest. const VERSIONS_MANIFEST_MIRROR_SUFFIX: &str = "/github/versions/main/v1";
4 Adapter
download_urls / mirror_urls for Ty
Implements the same mirror-first, GitHub-fallback URL pattern that Ruff uses, but pointed at astral-sh/ty releases.
} Ok(urls) }+ Self::Ty => {+ let suffix = format!("{version}/ty-{platform}.{}", format.extension());+ let mirror_base = astral_mirror_base_url(astral_mirror_url);+ let mirror = format!("{mirror_base}{TY_MIRROR_SUFFIX}{suffix}");+ let mut urls = vec![parse_url(mirror)?];+ // When using the default mirror, also fall back to GitHub.+ if astral_mirror_url.is_none() {+ let canonical = format!("{TY_GITHUB_URL_PREFIX}{suffix}");+ urls.push(parse_url(canonical)?);+ }+ Ok(urls)+ } Self::Uv => { let canonical = format!( "{UV_GITHUB_URL_PREFIX}{version}/uv-{platform}.{}",⋯ } Ok(vec![canonical_url]) }+ Self::Ty => {+ if let Some(suffix) = canonical_url.as_str().strip_prefix(TY_GITHUB_URL_PREFIX) {+ let mirror_base = astral_mirror_base_url(astral_mirror_url);+ let mirror = format!("{mirror_base}{TY_MIRROR_SUFFIX}{suffix}");+ let mirror_url = parse_url(mirror)?;+ if astral_mirror_url.is_some() {+ return Ok(vec![mirror_url]);+ }+ return Ok(vec![mirror_url, canonical_url]);+ }+ Ok(vec![canonical_url])+ } Self::Uv => Ok(vec![canonical_url]), } }
6 Tests/Docs
uv-bin-install tests for ty
Unit tests for ty download URL generation (custom mirror, default mirror + GitHub fallback) and for mirror URL rewriting, matching the existing test coverage for Ruff.
assert_eq!(default_urls, empty_urls); } + #[test]+ fn test_ty_download_urls_custom_astral_mirror() {+ let urls = Binary::Ty+ .download_urls_with_astral_mirror(+ &Version::new([0, 0, 1]),+ "x86_64-unknown-linux-gnu",+ ArchiveFormat::TarGz,+ Some("https://nexus.example.com/repository/releases.astral.sh/"),+ )+ .expect("ty download URLs should be valid");+ + let urls = urls+ .into_iter()+ .map(|url| url.to_string())+ .collect::<Vec<_>>();+ assert_eq!(+ urls,+ vec![+ "https://nexus.example.com/repository/releases.astral.sh/github/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz"+ .to_string(),+ ]+ );+ }+ + #[test]+ fn test_ty_download_urls_use_default_astral_mirror_then_github() {+ let default_urls = Binary::Ty+ .download_urls_with_astral_mirror(+ &Version::new([0, 0, 1]),+ "x86_64-unknown-linux-gnu",+ ArchiveFormat::TarGz,+ None,+ )+ .expect("ty download URLs should be valid");+ let empty_urls = Binary::Ty+ .download_urls_with_astral_mirror(+ &Version::new([0, 0, 1]),+ "x86_64-unknown-linux-gnu",+ ArchiveFormat::TarGz,+ Some(""),+ )+ .expect("ty download URLs should be valid");+ + assert_eq!(default_urls, empty_urls);+ assert_eq!(+ default_urls,+ vec![+ DisplaySafeUrl::parse(+ "https://releases.astral.sh/github/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz",+ )+ .expect("default Astral mirror ty URL should be valid"),+ DisplaySafeUrl::parse(+ "https://github.com/astral-sh/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz",+ )+ .expect("canonical ty URL should be valid"),+ ]+ );+ }+ #[test] fn test_manifest_urls_custom_astral_mirror() {⋯ #[test] fn test_manifest_urls_custom_astral_mirror() {- for (binary, filename) in [(Binary::Ruff, "ruff.ndjson"), (Binary::Uv, "uv.ndjson")] {+ for (binary, filename) in [+ (Binary::Ruff, "ruff.ndjson"),+ (Binary::Ty, "ty.ndjson"),+ (Binary::Uv, "uv.ndjson"),+ ] { let urls = binary .manifest_urls_with_astral_mirror(Some( "https://nexus.example.com/repository/releases.astral.sh/",⋯ ); } + #[test]+ fn test_ty_mirror_urls_custom_astral_mirror() {+ let canonical_url = DisplaySafeUrl::parse(+ "https://github.com/astral-sh/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz",+ )+ .expect("canonical ty URL should be valid");+ let urls = Binary::Ty+ .mirror_urls_with_astral_mirror(+ canonical_url,+ Some("https://nexus.example.com/repository/releases.astral.sh/"),+ )+ .expect("mirror URLs should be valid");+ + let urls = urls+ .into_iter()+ .map(|url| url.to_string())+ .collect::<Vec<_>>();+ assert_eq!(+ urls,+ vec![+ "https://nexus.example.com/repository/releases.astral.sh/github/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz"+ .to_string(),+ ]+ );+ }+ + #[test]+ fn test_ty_mirror_urls_use_default_astral_mirror_then_github() {+ let canonical_url = DisplaySafeUrl::parse(+ "https://github.com/astral-sh/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz",+ )+ .expect("canonical ty URL should be valid");+ let default_urls = Binary::Ty+ .mirror_urls_with_astral_mirror(canonical_url.clone(), None)+ .expect("mirror URLs should be valid");+ let empty_urls = Binary::Ty+ .mirror_urls_with_astral_mirror(canonical_url.clone(), Some(""))+ .expect("mirror URLs should be valid");+ + assert_eq!(default_urls, empty_urls);+ assert_eq!(+ default_urls,+ vec![+ DisplaySafeUrl::parse(+ "https://releases.astral.sh/github/ty/releases/download/0.0.1/ty-x86_64-unknown-linux-gnu.tar.gz",+ )+ .expect("default Astral mirror ty URL should be valid"),+ canonical_url,+ ]+ );+ }+ #[test] fn test_manifest_urls_empty_astral_mirror_uses_default() {⋯ #[test] fn test_manifest_urls_empty_astral_mirror_uses_default() {- for binary in [Binary::Ruff, Binary::Uv] {+ for binary in [Binary::Ruff, Binary::Ty, Binary::Uv] { let default_urls = binary .manifest_urls_with_astral_mirror(None) .expect("manifest URLs should be valid");
PreviewFeature::Check — feature flag gate
Registers 'check' as a named preview feature so the experimental warning can be suppressed with --preview-features check.
5 Cross-cutting
PreviewFeature::Check
Adds Check = 1 << 33 to the PreviewFeature bitflag enum, maps it to the string "check" in as_str/FromStr, and tests the round-trip. Without this flag the command still works but prints an experimental warning.
TomlBackwardsCompatibility = 1 << 30, MalwareCheck = 1 << 31, VenvSafeClear = 1 << 32,+ Check = 1 << 33, } impl PreviewFeature {⋯ Self::TomlBackwardsCompatibility => "toml-backwards-compatibility", Self::MalwareCheck => "malware-check", Self::VenvSafeClear => "venv-safe-clear",+ Self::Check => "check", } } }⋯ "toml-backwards-compatibility" => Self::TomlBackwardsCompatibility, "malware-check" => Self::MalwareCheck, "venv-safe-clear" => Self::VenvSafeClear,+ "check" => Self::Check, _ => return Err(PreviewFeatureParseError), }) }⋯ ); assert_eq!(PreviewFeature::MalwareCheck.as_str(), "malware-check"); assert_eq!(PreviewFeature::VenvSafeClear.as_str(), "venv-safe-clear");+ assert_eq!(PreviewFeature::Check.as_str(), "check"); } #[test]
Test infrastructure and integration tests for uv check
TestContext::check() helper and the full integration test suite covering the main scenarios.
6 Tests/Docs
TestContext::check
Adds context.check() mirroring context.format(): sets UV_EXCLUDE_NEWER to a future date so ty versions are resolvable in tests.
command } + /// Create a `uv check` command with options shared across scenarios.+ pub fn check(&self) -> Command {+ let mut command = self.new_command();+ command.arg("check");+ self.add_shared_options(&mut command, false);+ // Override to a more recent date for ty version resolution+ command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");+ command+ }+ /// Create a `uv build` command with options shared across scenarios. pub fn build(&self) -> Command { let mut command = self.new_command();
6 Tests/Docs
mod check in main.rs
Gates the check test module behind #[cfg(feature = "test-python")], matching the level of the audit tests.
#[cfg(all(feature = "test-python", feature = "test-pypi"))] mod export; +#[cfg(feature = "test-python")]+mod check;+ #[cfg(all(feature = "test-python", feature = "test-pypi"))] mod format;
6 Tests/Docs
check_project
Basic success case: project with pyproject.toml, valid Python code, expects exit 0 and the experimental warning.
+use anyhow::Result;+use assert_fs::prelude::*;+use indoc::indoc;+ ⋯+use uv_test::uv_snapshot;+ ⋯+#[test]+fn check_project() -> 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 = []+ "#})?;+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r"+ x: int = 1+ "})?;+ + uv_snapshot!(context.filters(), context.check(), @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ ");+ + Ok(())+}+
6 Tests/Docs
check_missing_pyproject_toml
Verifies that check still runs (against cwd) when no pyproject.toml exists, and that project-only flags (--group, --frozen, --no-sync) are warned about and ignored.
+#[test]+fn check_missing_pyproject_toml() -> Result<()> {+ let context = uv_test::test_context_with_versions!(&[]);+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r"+ x: int = 1+ "})?;+ + uv_snapshot!(context.filters(), context.check(), @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ ");+ + // Project-only settings are ignored without a discovered project.+ uv_snapshot!(context.filters(), context.check().arg("--group").arg("dev").arg("--frozen").arg("--no-sync"), @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ warning: `--group dev` has no effect when used outside of a project+ warning: `--frozen` has no effect when used outside of a project+ warning: `--no-sync` has no effect when used outside of a project+ ");+ + Ok(())+}+
6 Tests/Docs
check_no_project
Tests that --no-project skips project discovery even when a pyproject.toml is present with an unsatisfiable Python requirement, and that project-only flags generate warnings.
+#[test]+fn check_no_project() -> Result<()> {+ let context = uv_test::test_context_with_versions!(&[]).with_filtered_python_sources();+ + let pyproject_toml = context.temp_dir.child("pyproject.toml");+ pyproject_toml.write_str(indoc! {r#"+ [project]+ name = "project"+ version = "0.1.0"+ requires-python = ">=4.0"+ dependencies = []+ "#})?;+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r"+ x: int = 1+ "})?;+ + 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` to disable this warning.+ error: No interpreter found for Python >=4.0 in [PYTHON SOURCES]+ ");+ + // The unavailable project environment is not initialized when project discovery is disabled.+ uv_snapshot!(context.filters(), context.check().arg("--no-project"), @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ ");+ + // Project-only settings are ignored when project discovery is disabled.+ uv_snapshot!(+ context.filters(),+ context+ .check()+ .arg("--no-project")+ .arg("--extra")+ .arg("foo")+ .arg("--group")+ .arg("bar")+ .arg("--locked")+ .arg("--no-sync"),+ @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ warning: `--extra foo` has no effect when used alongside `--no-project`+ warning: `--group bar` has no effect when used alongside `--no-project`+ warning: `--locked` has no effect when used alongside `--no-project`+ warning: `--no-sync` has no effect when used alongside `--no-project`+ "+ );+ + Ok(())+}+
6 Tests/Docs
check_type_error
Verifies that ty's actual error output (invalid-assignment) is surfaced with exit code 1, confirming end-to-end integration with ty.
+#[test]+fn check_type_error() -> Result<()> {+ let context = uv_test::test_context_with_versions!(&[]);+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r#"+ name: str = "project"+ version: int = name+ "#})?;+ + uv_snapshot!(context.filters(), context.check(), @r#"+ success: false+ exit_code: 1+ ----- stdout -----+ error[invalid-assignment]: Object of type `Literal["project"]` is not assignable to `int`+ --> main.py:2:10+ |+ 1 | name: str = "project"+ 2 | version: int = name+ | --- ^^^^ Incompatible value of type `Literal["project"]`+ | |+ | Declared type+ |+ info: rule `invalid-assignment` is enabled by default+ + Found 1 diagnostic+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ "#);+ + Ok(())+}+
6 Tests/Docs
check_with_declared_dependency
Tests that uv syncs the declared dependency (iniconfig) into the venv and that ty resolves the import through VIRTUAL_ENV. Gated behind test-pypi.
+#[test]+#[cfg(feature = "test-pypi")]+fn check_with_declared_dependency() -> 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 = ["iniconfig"]+ "#})?;+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r"+ import iniconfig+ "})?;+ + // ty should resolve the import via the synced virtual environment.+ uv_snapshot!(context.filters(), context.check(), @"+ success: true+ exit_code: 0+ ----- stdout -----+ All checks passed!+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ Installed 1 package in [TIME]+ ");+ + context+ .assert_command(+ "from importlib.metadata import distribution; assert distribution('iniconfig').read_text('INSTALLER') == 'uv'",+ )+ .success();+ + Ok(())+}+
6 Tests/Docs
check_with_undeclared_dependency
Verifies that an undeclared import produces a ty unresolved-import diagnostic, confirming the venv is correctly passed and ty inspects it.
+#[test]+fn check_with_undeclared_dependency() -> 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 = []+ "#})?;+ + let main_py = context.temp_dir.child("main.py");+ main_py.write_str(indoc! {r"+ import iniconfig+ "})?;+ + let filters = context+ .filters()+ .into_iter()+ .chain([(+ r"info: \d+\. \[VENV\]/lib64/python3\.12/site-packages \(site-packages\)\n",+ "",+ )])+ .collect::<Vec<_>>();+ + // ty should report a diagnostic for the unresolvable import.+ uv_snapshot!(filters, context.check(), @"+ success: false+ exit_code: 1+ ----- stdout -----+ error[unresolved-import]: Cannot resolve imported module `iniconfig`+ --> main.py:1:8+ |+ 1 | import iniconfig+ | ^^^^^^^^^+ |+ info: Searched in the following paths during module resolution:+ info: 1. [TEMP_DIR]/ (first-party code)+ info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)+ info: 3. [SITE_PACKAGES]/ (site-packages)+ info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment+ info: rule `unresolved-import` is enabled by default+ + Found 1 diagnostic+ + ----- stderr -----+ warning: `uv check` is experimental and may change without warning. Pass `--preview-features check` to disable this warning.+ ");+ + Ok(())+}
6 Tests/Docs
help snapshot updates
Snapshot updates adding 'check Type check the project' to all help output variants.
export Export the project's lockfile to an alternate format tree Display the project's dependency tree format Format Python code in the project+ check Type check the project audit Audit the project's dependencies tool Run and install commands provided by Python packages python Manage Python versions and installations⋯ export Export the project's lockfile to an alternate format tree Display the project's dependency tree format Format Python code in the project+ check Type check the project audit Audit the project's dependencies tool Run and install commands provided by Python packages python Manage Python versions and installations⋯ export Export the project's lockfile to an alternate format tree Display the project's dependency tree format Format Python code in the project+ check Type check the project audit Audit the project's dependencies tool Run and install commands provided by Python packages python Manage Python versions and installations⋯ export tree format+ check audit tool python⋯ export tree format+ check audit tool python⋯ export Export the project's lockfile to an alternate format tree Display the project's dependency tree format Format Python code in the project+ check Type check the project audit Audit the project's dependencies tool Run and install commands provided by Python packages python Manage Python versions and installations⋯ export Export the project's lockfile to an alternate format tree Display the project's dependency tree format Format Python code in the project+ check Type check the project audit Audit the project's dependencies tool Run and install commands provided by Python packages python Manage Python versions and installations
6 Tests/Docs
show_settings preview_features snapshot
Adds Check to the preview features list in the show_settings snapshot.
TomlBackwardsCompatibility, MalwareCheck, VenvSafeClear,+ Check, ], }, python_preference: Managed,⋯ TomlBackwardsCompatibility, MalwareCheck, VenvSafeClear,+ Check, ], }, python_preference: Managed,