[ty] Only skip uv workspace metadata for a single script (#28581)

This commit is contained in:
Micha Reiser authored and GitHub committed 2026-09-17 19:09:35 +02:00
1 parent 69a1f2f3c7
commit af73c077aa
8 files changed
+148 -40

No files matched your search

+1 -1
View File
@@ -20,6 +20,7 @@ doctest = false
[dependencies]
ruff_db = { workspace = true, features = ["os", "cache", "junit"] }
ruff_diagnostics = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_ranged_value = { workspace = true }
ty_combine = { workspace = true }
ty_project = { workspace = true, features = ["zstd", "junit"] }
@@ -52,7 +53,6 @@ tikv-jemallocator = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_ast = { workspace = true }
ruff_python_trivia = { workspace = true }
ty_module_resolver = { workspace = true }
ty_python_core = { workspace = true }
-2
View File
@@ -3963,8 +3963,6 @@ and environments from the enclosing workspace or other scripts do not apply.
**Known limitations**
Explicit file arguments bypass uv workspace discovery unless `--config-file` is specified.
Imports guarded by `TYPE_CHECKING` are not reported because they are not executed at runtime. They
can use development-only dependencies, such as type stub packages, without requiring those packages
as runtime dependencies.
+19 -11
View File
@@ -25,6 +25,7 @@ use ruff_db::files::File;
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use ruff_db::{STACK_SIZE, max_parallelism};
use ruff_diagnostics::Applicability;
use ruff_python_ast::script::ScriptTag;
use salsa::Database;
use ty_project::metadata::settings::TerminalSettings;
use ty_project::watch::ProjectWatcher;
@@ -147,19 +148,26 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
.map(|path| SystemPath::absolute(path, &cwd));
let force_exclude = args.force_exclude();
let use_uv = UseUv::from_system(&system);
let use_uv = if use_uv == UseUv::On
&& let [path] = check_paths.as_slice()
&& system.is_file(path)
&& system
.read_to_string(path)
.is_ok_and(|source| ScriptTag::parse(source.as_bytes()).is_some())
{
// A single PEP 723 script uses its own uv environment, even with an explicit ty
// configuration file. The other explicit paths still discover workspace metadata.
UseUv::Scripts
} else {
use_uv
};
let mut project_metadata = match &config_file {
Some(config_file) => ProjectMetadata::from_config_file(
config_file.clone(),
&project_path,
&system,
UseUv::from_system(&system),
)?,
None if check_paths.iter().any(|path| system.is_file(path)) => {
// `uv check --script` passes a file as its check path. Standalone scripts must not
// inherit the enclosing workspace; their environments are synchronized separately.
ProjectMetadata::discover_without_uv(&project_path, &system)?
Some(config_file) => {
ProjectMetadata::from_config_file(config_file.clone(), &project_path, &system, use_uv)?
}
None => ProjectMetadata::discover(&project_path, &system)?,
None => ProjectMetadata::discover_with_uv(&project_path, &system, use_uv)?,
};
project_metadata.apply_configuration_files(&system)?;
+2 -1
View File
@@ -1364,7 +1364,7 @@ fn ordinary_files_do_not_initialize_scripts() -> anyhow::Result<()> {
assert_cmd_snapshot!(
case.command()
.arg("ordinary.py")
.env(EnvVars::TY_UV, "1")
.env(EnvVars::TY_UV, "scripts")
.env(EnvVars::UV, "missing-uv-executable"),
@"
success: true
@@ -1769,6 +1769,7 @@ mod uv_metadata {
assert_cmd_snapshot!(
command_with_script_uv(&case)
.args(["first.py", "second.py"])
.env(EnvVars::TY_UV, "scripts")
.env(EnvVars::TY_MAX_PARALLELISM, "1"),
@"
success: true
+125 -13
View File
@@ -82,6 +82,12 @@ pub(super) fn uv_sync_command(
);
let mut command = case.command();
set_uv_envs(&mut command, case, virtual_env);
Ok(command)
}
#[cfg(feature = "test-uv")]
fn set_uv_envs(command: &mut Command, case: &CliTest, virtual_env: Option<&Path>) {
command
.envs(uv_test_env_vars())
.env("TY_UV", "1")
@@ -93,8 +99,6 @@ pub(super) fn uv_sync_command(
if let Some(virtual_env) = virtual_env {
command.env("VIRTUAL_ENV", virtual_env);
}
Ok(command)
}
#[cfg(feature = "test-uv")]
@@ -400,29 +404,72 @@ fn uses_uv_workspace_root_without_checking_siblings() -> anyhow::Result<()> {
Ok(())
}
/// An explicit file is treated as a script, so workspace discovery stays disabled even when
/// `TY_UV` is set.
/// Checking a script uses its own environment; the project's uv metadata
/// is not queried.
#[cfg(feature = "test-uv")]
#[test]
fn explicit_file_path_disables_uv_workspace_discovery() -> anyhow::Result<()> {
let case = workspace_case()?;
fn explicit_script_path_disables_uv_workspace_discovery() -> anyhow::Result<()> {
// uv's formatting of resolution failures varies by version; retain the missing dependency.
let case = workspace_case()?
.with_filter(r"exit code: 1", "exit status: 1")
.with_filter(
concat!(
r"(?s)[ \t]*(?:×|error:) No solution found when resolving dependencies:?",
r".*?missing-workspace-dependency==99\.0\.0",
r".*?hint: Packages were unavailable because the network was disabled\.[^\n]*",
),
" <missing-workspace-dependency==99.0.0 unavailable offline>",
);
case.write_file(
"packages/member/pyproject.toml",
r#"
[project]
name = "member"
version = "0.1.0"
requires-python = ">=3.8"
dependencies = ["missing-workspace-dependency==99.0.0"]
"#,
)?;
let member_directory = case.root().join("packages/member");
let mut command = case.command();
set_uv_envs(&mut command, &case, None);
assert_cmd_snapshot!(command.current_dir(&member_directory).arg("member.py"), @r#"
success: false
exit_code: 1
----- stdout -----
member.py:1:14: error[invalid-assignment] Object of type `Literal["selected-member"]` is not assignable to `int`
pyproject.toml: warning[uv-metadata] `uv workspace metadata` failed with status exit status: 1: <missing-workspace-dependency==99.0.0 unavailable offline>
Found 2 diagnostics
----- stderr -----
"#);
case.write_file("shared.py", "value: int = 'unselected-workspace-root'")?;
case.write_file(
"packages/member/member.py",
"import shared\nvalue: int = 'selected-script'",
r#"
# /// script
# dependencies = []
# ///
import shared
value: int = 'selected-script'
"#,
)?;
let mut command = uv_sync_command(&case, None)?;
command
.current_dir(case.root().join("packages/member"))
.arg("member.py");
// Checking a script reports its own diagnostics without querying the project's uv metadata.
let mut command = case.command();
set_uv_envs(&mut command, &case, None);
command.current_dir(&member_directory).arg("member.py");
assert_cmd_snapshot!(command, @r#"
success: false
exit_code: 1
----- stdout -----
member.py:1:8: error[unresolved-import] Cannot resolve imported module `shared`
member.py:2:14: error[invalid-assignment] Object of type `Literal["selected-script"]` is not assignable to `int`
member.py:5:8: error[unresolved-import] Cannot resolve imported module `shared`
member.py:6:14: error[invalid-assignment] Object of type `Literal["selected-script"]` is not assignable to `int`
Found 2 diagnostics
----- stderr -----
@@ -431,6 +478,71 @@ fn explicit_file_path_disables_uv_workspace_discovery() -> anyhow::Result<()> {
Ok(())
}
/// Checking an ordinary file in a uv workspace uses the project's uv metadata.
#[cfg(feature = "test-uv")]
#[test]
fn explicit_ordinary_file_uses_uv_workspace_discovery() -> anyhow::Result<()> {
let case = dependency_workspace_case()?;
let mut command = uv_sync_command(&case, None)?;
command
.current_dir(case.root().join("packages/member"))
.args(["member.py", "--error", "missing-direct-dependency"]);
assert_cmd_snapshot!(command, @"
success: false
exit_code: 1
----- stdout -----
member.py:3:6: error[missing-direct-dependency] Import of `indirect_module` requires a direct dependency on `indirect-dependency`
member.py:4:8: error[missing-direct-dependency] Import of `indirect_module` requires a direct dependency on `indirect-dependency`
Found 2 diagnostics
----- stderr -----
");
Ok(())
}
/// When ty directly checks a script alongside another file, the other file still gets dependency
/// diagnostics from uv workspace metadata.
#[cfg(feature = "test-uv")]
#[test]
fn multiple_explicit_files_use_uv_workspace_discovery() -> anyhow::Result<()> {
let case = dependency_workspace_case()?;
case.write_file(
"packages/member/script.py",
r#"
# /// script
# dependencies = []
# ///
value = 1
"#,
)?;
let mut command = uv_sync_command(&case, None)?;
command
.current_dir(case.root().join("packages/member"))
.args([
"script.py",
"member.py",
"--error",
"missing-direct-dependency",
]);
assert_cmd_snapshot!(command, @"
success: false
exit_code: 1
----- stdout -----
member.py:3:6: error[missing-direct-dependency] Import of `indirect_module` requires a direct dependency on `indirect-dependency`
member.py:4:8: error[missing-direct-dependency] Import of `indirect_module` requires a direct dependency on `indirect-dependency`
Found 2 diagnostics
----- stderr -----
");
Ok(())
}
/// An explicitly selected member inherits ty rule configuration from the uv workspace root.
#[cfg(feature = "test-uv")]
#[test]
-9
View File
@@ -239,15 +239,6 @@ impl ProjectMetadata {
}
}
/// Discovers the closest project without considering uv workspace metadata.
pub fn discover_without_uv(
path: &SystemPath,
system: &dyn System,
) -> Result<ProjectMetadata, ProjectMetadataError> {
Self::discover_with_uv_workspace(path, system, ProjectEnvironment::default())
.map(|metadata| metadata.with_use_uv(UseUv::from_system(system)))
}
fn discover_with_uv_workspace(
path: &SystemPath,
system: &dyn System,
@@ -36,8 +36,6 @@ and environments from the enclosing workspace or other scripts do not apply.
## Known limitations
Explicit file arguments bypass uv workspace discovery unless `--config-file` is specified.
Imports guarded by `TYPE_CHECKING` are not reported because they are not executed at runtime. They
can use development-only dependencies, such as type stub packages, without requiring those packages
as runtime dependencies.
+1 -1
View File
@@ -1166,7 +1166,7 @@
},
"missing-direct-dependency": {
"title": "detects imports of dependencies that are not declared directly",
"description": "## What it does\n\nChecks for imports from installable packages that the current project or PEP 723 script does not\ndeclare as direct dependencies.\n\nThe name used in dependency declarations can differ from the import name: for example, the `pillow`\npackage is imported as `PIL`.\n\n## Why is this bad?\n\nA dependency can be installed because another package requires it. Importing that dependency without\ndeclaring it makes your code rely on another package's dependency list. If that package removes the\ndependency, your imports can fail.\n\nDeclare the packages that provide your imports in `project.dependencies` or\n`project.optional-dependencies` in `pyproject.toml`. Non-package files, such as tests and\ndevelopment scripts, can also use dependencies declared in dependency groups.\n\nSee uv's [guide to managing dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/)\nfor how to add these declarations.\n\n## Rule status\n\nThis rule is disabled by default and requires uv integration.\n\nFor projects, enable uv workspace integration (`TY_UV=1`) and use an existing, synchronized\nenvironment. Running [`uv check`](https://docs.astral.sh/uv/reference/cli/#uv-check) synchronizes\nthe environment automatically before invoking ty, unless `--no-sync` is passed. For these checks, ty\nreads the dependency graph and module ownership returned by `uv workspace metadata` without changing\ninstalled packages. uv may update the lockfile to match the current dependency declarations. uv\n0.12.3 or later is required.\n\nFor PEP 723 scripts, enable uv script integration with `TY_UV=scripts` or `TY_UV=1`. ty synchronizes\neach script's environment and checks imports against its inline `dependencies` list. Declarations\nand environments from the enclosing workspace or other scripts do not apply.\n\n## Known limitations\n\nExplicit file arguments bypass uv workspace discovery unless `--config-file` is specified.\n\nImports guarded by `TYPE_CHECKING` are not reported because they are not executed at runtime. They\ncan use development-only dependencies, such as type stub packages, without requiring those packages\nas runtime dependencies.\n\nStandard-library imports and imports whose owning package cannot be identified unambiguously are\nalso not reported.\n\nImports of [namespace packages](https://docs.python.org/3/reference/import.html#namespace-packages)\nthemselves, such as `import ns`, are not reported: the namespace can contain modules from several\ninstallable packages. Imports of their submodules, such as `import ns.child`, are checked when the\nowning package is known. An `__init__.pyi` stub does not change this distinction.\n\nNative packages that ty can resolve only as namespace packages at runtime are also skipped. For\nother native modules, ty can use stubs to resolve the import and uv's ownership map to identify\nwhich package to declare.\n\nSome editable installations add the whole project directory to Python's import path, making both\npackage code and files such as `tests/test_app.py` importable. If uv does not identify which modules\nbelong to the installable package, ty allows dependency-group imports throughout that directory,\nincluding in package code, to avoid incorrectly flagging imports in tests and scripts.\n\n## Examples\n\nWith `requests` as a direct dependency, `urllib3` may also be installed because `requests` depends\non it:\n\n```python {data-mdtest=\"ignore\"}\nimport requests\nimport urllib3 # error: [missing-direct-dependency]\n```\n\nAdd `urllib3` to `project.dependencies` if your code imports it directly.",
"description": "## What it does\n\nChecks for imports from installable packages that the current project or PEP 723 script does not\ndeclare as direct dependencies.\n\nThe name used in dependency declarations can differ from the import name: for example, the `pillow`\npackage is imported as `PIL`.\n\n## Why is this bad?\n\nA dependency can be installed because another package requires it. Importing that dependency without\ndeclaring it makes your code rely on another package's dependency list. If that package removes the\ndependency, your imports can fail.\n\nDeclare the packages that provide your imports in `project.dependencies` or\n`project.optional-dependencies` in `pyproject.toml`. Non-package files, such as tests and\ndevelopment scripts, can also use dependencies declared in dependency groups.\n\nSee uv's [guide to managing dependencies](https://docs.astral.sh/uv/concepts/projects/dependencies/)\nfor how to add these declarations.\n\n## Rule status\n\nThis rule is disabled by default and requires uv integration.\n\nFor projects, enable uv workspace integration (`TY_UV=1`) and use an existing, synchronized\nenvironment. Running [`uv check`](https://docs.astral.sh/uv/reference/cli/#uv-check) synchronizes\nthe environment automatically before invoking ty, unless `--no-sync` is passed. For these checks, ty\nreads the dependency graph and module ownership returned by `uv workspace metadata` without changing\ninstalled packages. uv may update the lockfile to match the current dependency declarations. uv\n0.12.3 or later is required.\n\nFor PEP 723 scripts, enable uv script integration with `TY_UV=scripts` or `TY_UV=1`. ty synchronizes\neach script's environment and checks imports against its inline `dependencies` list. Declarations\nand environments from the enclosing workspace or other scripts do not apply.\n\n## Known limitations\n\nImports guarded by `TYPE_CHECKING` are not reported because they are not executed at runtime. They\ncan use development-only dependencies, such as type stub packages, without requiring those packages\nas runtime dependencies.\n\nStandard-library imports and imports whose owning package cannot be identified unambiguously are\nalso not reported.\n\nImports of [namespace packages](https://docs.python.org/3/reference/import.html#namespace-packages)\nthemselves, such as `import ns`, are not reported: the namespace can contain modules from several\ninstallable packages. Imports of their submodules, such as `import ns.child`, are checked when the\nowning package is known. An `__init__.pyi` stub does not change this distinction.\n\nNative packages that ty can resolve only as namespace packages at runtime are also skipped. For\nother native modules, ty can use stubs to resolve the import and uv's ownership map to identify\nwhich package to declare.\n\nSome editable installations add the whole project directory to Python's import path, making both\npackage code and files such as `tests/test_app.py` importable. If uv does not identify which modules\nbelong to the installable package, ty allows dependency-group imports throughout that directory,\nincluding in package code, to avoid incorrectly flagging imports in tests and scripts.\n\n## Examples\n\nWith `requests` as a direct dependency, `urllib3` may also be installed because `requests` depends\non it:\n\n```python {data-mdtest=\"ignore\"}\nimport requests\nimport urllib3 # error: [missing-direct-dependency]\n```\n\nAdd `urllib3` to `project.dependencies` if your code imports it directly.",
"default": "ignore",
"oneOf": [
{