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
18 changes: 17 additions & 1 deletion crates/uv/src/commands/project/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use crate::commands::project::{
validate_project_requires_python,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::workspace::list::find_scripts;
use crate::commands::{ExitStatus, diagnostics, project};
use crate::printer::Printer;
use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings};
Expand Down Expand Up @@ -245,7 +246,7 @@ pub(crate) async fn check(
// The most common case this is handling is a non-virtual workspace, where the root
// package will almost always have the other packages nested under it, and we need a
// way to select just the workspace root.
let excluded_targets = if let Some(project) = project.as_ref()
let mut excluded_targets = if let Some(project) = project.as_ref()
&& !defacto_all_packages
{
project
Expand All @@ -269,6 +270,21 @@ pub(crate) async fn check(
Vec::new()
};

// PEP 723 scripts have independent environments and must be checked explicitly with
// `uv check --script`. Reuse `uv workspace list --scripts` discovery so scripts nested in
// selected workspace members are not checked against the project environment.
if let Some(project) = project.as_ref() {
excluded_targets.extend(
find_scripts(project.workspace().install_path(), cache)?
.into_iter()
.filter(|script| {
check_targets
.iter()
.any(|target| script.starts_with(target))
}),
);
}

let groups = if let Some(project) = &project {
groups.with_defaults(default_dependency_groups(project.pyproject_toml())?)
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/workspace/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pub(crate) async fn list(
///
/// Respects ignore files and excludes repository internals, virtual environments, and the uv cache
/// from traversal.
fn find_scripts(workspace_root: &Path, cache: &Cache) -> Result<Vec<PathBuf>> {
pub(crate) fn find_scripts(workspace_root: &Path, cache: &Cache) -> Result<Vec<PathBuf>> {
// Avoid descending into the cache when it is inside the workspace. If the workspace itself is
// inside the cache, it is still the requested search root and must not be excluded.
let cache_root = if cache.root().is_absolute() {
Expand Down
86 changes: 86 additions & 0 deletions crates/uv/tests/project/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,92 @@ fn check_project() -> Result<()> {
Ok(())
}

/// Check PEP 723 scripts only when explicitly selected, not as part of a workspace member.
#[test]
fn check_workspace_excludes_pep723_scripts() -> Result<()> {
let context =
uv_test::test_context!("3.12").with_filter((r"WARN Failed to fetch `ty`[^\n]*\n", ""));
context
.temp_dir
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[tool.uv.workspace]
members = ["packages/*"]
"#})?;
context
.temp_dir
.child("main.py")
.write_str("value: int = 'project'\n")?;
context.temp_dir.child("script.py").write_str(indoc! {r#"
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
value: int = "script"
"#})?;

write_workspace_member(&context, "member", "value: int = 'member'\n")?;
context
.temp_dir
.child("packages/member/script.py")
.write_str(indoc! {r#"
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
value: int = "member-script"
"#})?;

uv_snapshot!(context.filters(), workspace_check(&context), @r#"
exit_code: 1 (failure)
----- stdout -----
main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
Found 1 diagnostic

----- stderr -----
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
"#);

uv_snapshot!(context.filters(), workspace_check(&context).arg("--package").arg("member"), @r#"
exit_code: 1 (failure)
----- stdout -----
main.py:1:14: error[invalid-assignment] Object of type `Literal["member"]` is not assignable to `int`
Found 1 diagnostic

----- stderr -----
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
"#);

uv_snapshot!(context.filters(), workspace_check(&context).arg("--all-packages"), @r#"
exit_code: 1 (failure)
----- stdout -----
main.py:1:14: error[invalid-assignment] Object of type `Literal["project"]` is not assignable to `int`
packages/member/main.py:1:14: error[invalid-assignment] Object of type `Literal["member"]` is not assignable to `int`
Found 2 diagnostics

----- stderr -----
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
"#);

uv_snapshot!(context.filters(), workspace_check(&context).arg("--script").arg("script.py"), @r#"
exit_code: 1 (failure)
----- stdout -----
script.py:5:14: error[invalid-assignment] Object of type `Literal["script"]` is not assignable to `int`
Found 1 diagnostic

----- stderr -----
warning: `uv check` is experimental and may change without warning. Pass `--preview-features check-command` to disable this warning.
"#);

Ok(())
}

/// Check only the selected workspace member, whether selected implicitly or explicitly.
#[test]
fn check_workspace_member_selection() -> Result<()> {
Expand Down
Loading