Skip to content

[ty] Discover uv workspace roots - #25551

Merged
Gankra merged 5 commits into
mainfrom
gankra/ty-uv-workspace-discovery
Jul 24, 2026
Merged

[ty] Discover uv workspace roots#25551
Gankra merged 5 commits into
mainfrom
gankra/ty-uv-workspace-discovery

Conversation

@Gankra

@Gankra Gankra commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

This introduces minimal support for acquiring uv workspace metadata when, and only when, ty is invoked through uv check, and packages/workspaces are being requested (and not e.g. --script, see below). All other invocations of metadata are "future work".

uv check Recap

As a recap, uv check on current main will:

  • Apply standard package selection (usually default to the package that owns cwd, with --package to select a different one, and --all-packages to select all of them)
  • Run uv sync for the selected packages, and to fetch the appropriate version of ty
  • Set VIRTUAL_ENV=/path/to/.venv/
  • Set UV=/path/to/uv.exe
  • Set TY_UV=1
  • Set the working dir to either be the "right" one for the selected packages (generally the one package that will be worked on, or the workspace if there's multiple or the package lives outside the workspace)
  • Run something like ty check --exclude=packages/a/subpkg/ -- packages/a/ packages/b/
    • in this example invocation, a and b were selected and subpkg was not, and an explicit exclude was introduced to prevent it from being implicitly analyzed as part of a, under normal circumstances no exclude is needed, but any non-virtual workspace may need this kind of thing, and non-virtual workspaces are orthodox)

The consequences of this are that uv check already "just works" without this PR, however there are a few corner cases that are debatable.

What This Changes

The following behaviours should only apply when TY_UV=1 mode is set, indicating we are being invoked through uv check (if you set env-var we simply assume you are uv check and GLHF if you're not; interactions with flags uv will never set are not guaranteed to make sense).

  • We will run uv workspace metadata --frozen --active in the cwd that uv told us to run in
    • --frozen because we assume apriori that uv check already ran lock+sync
    • --active to ensure we continue to use the VIRTUAL_ENV that uv told us to use (notably important for --isolated)
    • Notably we do not pass --sync because the new default main uv behaviour for uv workspace metadata is to opportunistically emit environment metadata (python interpreter, package-to-module mappings) even if --sync isn't requested. We should not pass --sync in this case because uv has performed package/extra/group selection and we don't know what that was, and we are trying to avoid defining a protocol for forwarding that for the MVP.
    • Corner Case 1: If the user runs uv check --isolated and there is no uv.lock then this will fail as the rerun with --frozen will not find any lock to use. This is fine for an MVP.
    • Corner Case 2: If the workspace has conflicts the package versions reported may be subtly incorrect. In general "how does ty operate in a world with conflicts" is "that's the neat part, it doesn't" for the forseeable future (uv check can maybe be made to work but how the heck does the LSP?)
    • Corner Case 3: flags like --no-project and --no-sync are whacky nonsense I am refusing to put anymore thought into for an MVP.
  • The relevant parts of the metadata will be stored on ProjectMetadata (currently only a few fields)
  • ty will preferentially read config from the uv workspace root that metadata reports (notably quite important for the member-outside-the-workspace-dir case, where otherwise ty could get lost)
  • ty will preferentially use the python version/interpreter the metadata reports, deferring to uv as "the authority" on these configs (and ignoring it's own).

Future Work for --watch

If TY_UV=1 mode is enabled, --watch is forbidden (which is fine uv will never pass that). In the future it would be nice to have uv check --watch, but this will require us to have a proper system for ty actually telling uv to resync with the proper package selection, and for uv metadata to have some way to report what members to watch.

Future Work for ty check

It would be nice if ty check could opportunistically detect that uv is available and auto-use TY_UV=1 mode. This is explicitly out of scope for the MVP as it causes endless UX arguments. I think under the current architecture it will probably "just" be ty instead opting to run uv workspace metadata --sync to sync the entire workspace (maybe with --isolated if we don't want to clobber the "real" venv... also it would then remember the venv location and pass --active for all subsequent invocations to keep reusing the same ephemeral venv).

Note to self: uv workspace metadata --isolated exists but is fake because of the accursed "global deprecated --isolated flag that is different from the --isolated flag other commands have, so need to add that in the future if we decide we need it.

Future Work For Scripts

In the current implementation TY_UV=1 mode is disabled if a path to a file is passed, as this should only happen if uv check --script is run. This is a temporary measure to keep scope tight for an MVP.

uv check --script already works better than ty check path/to/script.py because uv check will set VIRTUAL_ENV to the script's isolated venv -- the above just means we won't immediately have richer features TY_UV mode adds.

In "package mode" where TY_UV=1 applies, if a package contains random pep723 scripts, ty will attempt to analyze them with the workspace's venv, which is wrong in the same way ty is wrong already. In a followup we will make ty check if a file is a script, and if so, not analyze it unless the CLI was passed an explicit path to exactly that file (indicating --script).

In a followup we can teach ty to run uv workspace metadata --script=... when it finds a script and considers it in-scope to analyze it. Some fiddly corners around whether VIRTUAL_ENV should be cleared for such invocations, in general script venvs work kinda like ephemeral venvs but consistently resolved to the same path so you don't need to try super hard to be consistent, uv will figure it out.

Future Work For LSP

LSP is out of scope for the MVP.

The rough idea for how it should work is that it will initially start up as it does now and opportunistically kick off uv workspace metadata --sync (--isolated?) in the background. If that returns successfully, it's answers will then be respected and integrated in much the same way they are when invoked with uv check. This may cause mass invalidations but ty is fast so it's fine right? :)

(The idea of this architecture is ty will give okish results early and fast and then as soon as uv is done "snap" into a richer form, in a similar way to how rust-analyzer can "snap to life" when cargo check completes.)

After the first run it can then setup proper watchers and opportunistically invoke things like uv workspace metadata --script or reinvoke uv workspace metadata --sync as needed. See the future work for scripts and watchers for details of that.

Future Work for meow

Meow lints will opportunistically be enabled when the UvMetadata is attached to the ProjectMetadata and contains package-to-module maps. Meow and attaching more metadata will be implemented in a followup PR.

@Gankra Gankra added the ty Multi-file analysis & type inference label Jun 2, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 96.84%. The percentage of expected errors that received a diagnostic held steady at 92.11%. The number of fully passing files held steady at 99/133.

@astral-sh-bot

astral-sh-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@Gankra
Gankra force-pushed the gankra/ty-uv-workspace-discovery branch from 338b62d to 933d2df Compare June 2, 2026 15:43
@astral-sh-bot

astral-sh-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

Comment thread crates/ty/src/lib.rs Outdated
Comment on lines +131 to +134
let uv_workspace_root = explicit_project_path
.is_none()
.then(|| uv::discover_workspace_root(&cwd))
.flatten();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we want the project cli arg for ty to completely trump the uv signal or to get synthesized with it (longterm we want to "always" call uv for dependency graph stuff so avoiding calling it doesn't get us anywhere).

@Gankra
Gankra force-pushed the gankra/ty-uv-workspace-discovery branch from 170a10c to d440e53 Compare June 2, 2026 16:19
@MichaReiser
MichaReiser self-requested a review June 10, 2026 06:31

@MichaReiser MichaReiser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have to consider the precedence between uv and ty a bit more in detail.

I'd also prefer if we move this into ProjectMetadata::discover instead, to get a consistent experience when opening such a project in the LSP. This may also require changing the file watching to reload the project whenever a uv.toml changes.

Comment thread crates/ty/src/args.rs Outdated
Comment thread crates/ty/src/lib.rs Outdated
Comment thread crates/ty/src/lib.rs Outdated
Comment thread crates/ty_static/src/env_vars.rs
Comment thread crates/ty/src/uv.rs Outdated
Comment thread crates/ty/src/uv.rs Outdated
@codspeed-hq

codspeed-hq Bot commented Jun 12, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 161 untouched benchmarks


Comparing gankra/ty-uv-workspace-discovery (fe6439d) with main (7d3e61e)

Open in CodSpeed

Comment thread crates/ty_project/src/metadata/uv.rs Outdated
Comment thread crates/ty_project/src/metadata/uv.rs Outdated
Comment thread crates/ty_project/src/metadata/uv.rs Outdated
Comment thread crates/ty_project/src/metadata/uv.rs Outdated
Comment thread crates/ty/src/lib.rs Outdated
@Gankra
Gankra marked this pull request as ready for review June 15, 2026 12:45
@astral-sh-bot
astral-sh-bot Bot requested a review from MichaReiser June 15, 2026 12:45
@AlexWaygood
AlexWaygood removed their request for review June 15, 2026 13:07
@Gankra
Gankra requested review from a team as code owners July 17, 2026 10:56
@Gankra
Gankra requested review from a team as code owners July 17, 2026 20:20
@astral-sh-bot

astral-sh-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@MichaReiser
MichaReiser self-requested a review July 20, 2026 15:19

@MichaReiser MichaReiser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I find it somewhat difficult to review this PR, because the summary contains very little detail and I don't know how the integration works on uv's side.

Would you mind extending the summary and explaining:

  • The use case. What use cases are intentionally out of scope
  • How is this called form uv. What argument does uv pass to ty? What was the reasoning behind this design.
  • Document why we now call uv metadata eagerly. This is not that important to me, but i'm sure we'll forget in the future and having it document somewhere will be helpful
  • What are the precedence rules and why did you define them as they are (I'm aware of most of them, but again, I think this will be valuable for us in the future)

Codex claims this finding

VIRTUAL_ENV incorrectly overrides an explicit UV_PROJECT_ENVIRONMENT, causing --sync and ty to use the wrong environment. uv normally gives the explicit project environment precedence. Reproduced with an import available only in the explicit environment. uv environment semantics

Comment thread crates/ty/src/lib.rs Outdated
Comment thread crates/ty/src/lib.rs Outdated
Comment thread crates/ty_project/src/metadata/uv.rs Outdated
Comment thread crates/ty_project/src/metadata/uv.rs Outdated
@MichaReiser MichaReiser added the cli Related to the command-line interface label Jul 21, 2026
Comment thread crates/ty_project/src/metadata.rs Outdated
Comment on lines +315 to +317
// No explicitly configured project was found. Prefer the uv workspace over the closest
// plain `pyproject.toml`, if available.
let metadata = if let Some(uv_project) = uv_project {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit weird, especially because it also applies if the uv integration isn't used?

What's the reason that we change the configuration precedence as part of this PR? Should this only apply if opted in with the environment variable?

@Gankra
Gankra force-pushed the gankra/ty-uv-workspace-discovery branch 2 times, most recently from e22b433 to 59d2ba6 Compare July 23, 2026 21:07
@MichaReiser
MichaReiser self-requested a review July 24, 2026 12:39
@MichaReiser
MichaReiser requested a review from zsol July 24, 2026 12:45
Comment on lines +40 to +56
if !output.status.success() {
tracing::debug!(
"`uv workspace metadata` failed with status {}: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
return None;
}

match Self::from_metadata(&output.stdout, system) {
Ok(workspace) => Some(workspace),
Err(error) => {
tracing::debug!("Failed to use `uv workspace metadata` output: {error}");
None
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to surface these are errors humans see by default..? (still warnings)

Comment thread crates/ty_project/src/metadata.rs

@MichaReiser MichaReiser left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an excellent summary. Thank you. It helped me a ton to understand the decision made in this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest adding a link to the corresponding uv tests somewhere in this file (or in metadata/uv?. It helped me understand what uv does and what the expected behavior at the workspace level is. I know, it's not ideal, the link is likely to get outdated. But it at least gives me a pointer to where to look.

Comment thread crates/ty_project/src/metadata.rs Outdated
Comment on lines +49 to +53
/// Options derived from the uv workspace, with higher precedence than project and user-level
/// configuration.
#[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))]
uv_workspace_options: Option<Box<Options>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't entirely clear to me what uv_workspace_options are used for. It may be helpful to document that we only use those for requires_python and the interpreter path today

Comment thread crates/ty_project/src/metadata.rs Outdated
.filter(|root| !path.starts_with(root));

for project_root in path.ancestors() {
for project_root in path.ancestors().chain(external_uv_workspace_root) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The chain here is a bit confusing.

I'm not sure if still possible, but I once split this method into two:

  • discover (does the outer loop)
  • discover_in (discovers a config in a single directory)

I could help here.

1e77da4

@zsol zsol added the great writeup A wonderful example of a quality contribution label Jul 24, 2026
Gankra added a commit that referenced this pull request Jul 24, 2026
Latest releases has updates to metadata and uv check that our tests want
to rely on in #25551
Gankra added 4 commits July 24, 2026 11:32
Discover uv workspace roots and selected environments for uv check, preserve configuration-source attribution, and cover workspace, package, nested-member, active-environment, and script-path behavior.
@Gankra
Gankra force-pushed the gankra/ty-uv-workspace-discovery branch from 053586d to 64ada2f Compare July 24, 2026 15:39
@Gankra

Gankra commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Pushed up some minor fixups based on review, and some test cleanups.

@Gankra
Gankra merged commit d297b16 into main Jul 24, 2026
62 checks passed
@Gankra
Gankra deleted the gankra/ty-uv-workspace-discovery branch July 24, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli Related to the command-line interface great writeup A wonderful example of a quality contribution ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants