How uv Discovers Project Configuration Files Like pyproject.toml
uv discovers project configuration by walking up the directory tree from the current working directory to locate the nearest pyproject.toml containing a [project] table, then optionally continues upward to find a workspace root defined by tool.uv.workspace, finally assembling a Workspace object that respects inclusion globs, exclusion patterns, and validation rules.
The uv package manager from Astral implements a deterministic, filesystem-based discovery mechanism to locate and load Python project configurations. When executing commands from any subdirectory, uv must resolve which pyproject.toml defines the current project and whether that project belongs to a larger workspace. This resolution process, implemented primarily in crates/uv-workspace/src/workspace.rs, combines ancestor directory traversal with TOML parsing and glob-based membership testing.
The Three-Step Discovery Process
uv's configuration discovery operates through three coordinated phases, each handled by specific functions in the workspace crate.
Step 1: Locating the Nearest Project Root
The discovery begins with ProjectWorkspace::discover (lines 1681-1689 in crates/uv-workspace/src/workspace.rs). Starting from the current working directory, uv climbs the filesystem tree using path.ancestors(), stopping at the first directory containing a pyproject.toml file that includes a [project] table.
let project_root = path
.ancestors()
.take_while(|p| { /* stop_discovery_at handling */ })
.find(|p| p.join("pyproject.toml").is_file())
.ok_or(WorkspaceError::MissingPyprojectToml)?;
Once found, uv reads and parses the file using PyProjectToml::from_string (defined in crates/uv-workspace/src/pyproject.rs), extracting the project metadata.
Step 2: Searching for an Explicit Workspace
After locating the project root, uv continues upward via find_workspace (line 1491) to detect if the project belongs to a larger workspace. The function iterates through project_root.ancestors().skip(1), examining each ancestor directory for a pyproject.toml containing a tool.uv.workspace table.
When a potential workspace root is found, uv validates membership using is_included_in_workspace and is_excluded_from_workspace (lines 1529-1544). The project must match the workspace's members glob patterns and not match any exclude patterns. If excluded, uv returns Ok(None) at line 1537, treating the project as standalone.
Step 3: Assembling the Full Workspace
If a valid workspace root exists, Workspace::discover (line 1869) orchestrates the final assembly. This process calls collect_members (lines 3340-3430), which delegates to collect_members_only to expand workspace globs and validate each member.
The member collection process (lines 5115-5210) performs several validations:
- Each member must contain a
pyproject.tomlwith a[project]table - Members cannot specify
managed = false(line 5110) - Package names must be unique across the workspace (line 5148)
- Nested workspaces are prohibited (line 5245)
If no explicit workspace is found, uv creates an implicit single-package workspace where the project root serves as both the project and workspace root.
Edge Cases and Validation Rules
uv handles several edge cases during discovery, each with specific error handling:
- Missing
pyproject.toml: ReturnsWorkspaceError::MissingPyprojectTomlwhen no configuration file exists in the ancestor chain. - Missing
[project]table: Logs a warning (line 1578) and treats the directory as a non-project if neither[project]nortool.uv.workspaceexists. - Excluded members: Silently skips projects matching workspace
excludeglobs (line 1537). - Missing member
pyproject.toml: ReturnsMissingPyprojectTomlMember(line 5162) if a glob matches a directory without a configuration file. - Duplicate package names: Returns
DuplicatePackage(line 5148) if two members share the same package name. - Nested workspaces: Returns
NestedWorkspace(line 5245) if a member defines its owntool.uv.workspace. - Unmanaged projects: Skips projects with
managed = false(line 5110), excluding them from the workspace.
Practical Examples
CLI Usage
When running uv commands from any subdirectory, automatic discovery occurs without explicit flags:
# From a deeply nested package directory
$ uv pip install requests
uv automatically locates the nearest pyproject.toml, determines workspace membership, and resolves dependencies according to the discovered configuration.
Programmatic Discovery in Rust
To implement custom tooling around uv's discovery mechanism:
use uv_workspace::{ProjectWorkspace, DiscoveryOptions, WorkspaceCache};
#[tokio::main]
async fn main() -> Result<(), uv_workspace::WorkspaceError> {
// Configure discovery to stop at a specific ancestor
let opts = DiscoveryOptions {
stop_discovery_at: Some(std::path::PathBuf::from("/my/repo")),
..Default::default()
};
// Initialize cache for workspace resolution
let cache = WorkspaceCache::default();
// Discover from current directory
let proj_ws = ProjectWorkspace::discover(
std::env::current_dir()?.as_path(),
&opts,
&cache
).await?;
println!("Project root: {}", proj_ws.project_root.display());
println!("Workspace root: {}", proj_ws.workspace.install_path().display());
// Iterate workspace members
for (name, member) in proj_ws.workspace.packages() {
println!("Member: {} → {}", name, member.root().display());
}
Ok(())
}
Inspecting Parsed Configuration
To examine the parsed pyproject.toml structure:
use uv_workspace::pyproject::PyProjectToml;
let toml_str = std::fs::read_to_string("pyproject.toml")?;
let pyproject: PyProjectToml = PyProjectToml::from_string(toml_str)?;
println!("Project name: {}", pyproject.project.unwrap().name);
Summary
- uv discovers project configuration through a three-phase process: locating the nearest project root, searching for an explicit workspace ancestor, and assembling the complete workspace structure.
- The discovery implementation resides primarily in
crates/uv-workspace/src/workspace.rs, with key entry points atProjectWorkspace::discover(line 1681) andWorkspace::discover(line 1869). - Workspace membership is determined by glob patterns defined in
tool.uv.workspace.membersandtool.uv.workspace.exclude, validated throughis_included_in_workspaceandis_excluded_from_workspace. - The system handles edge cases including missing configuration files, duplicate package names, nested workspaces, and unmanaged projects with specific error types and warnings.
Frequently Asked Questions
How does uv handle multiple pyproject.toml files in parent directories?
uv climbs the directory tree using path.ancestors() until it finds the first pyproject.toml containing a [project] table, which becomes the project root. If searching for a workspace, it continues upward to find a pyproject.toml with tool.uv.workspace, but skips the already-found project directory to avoid self-reference.
What happens if a project is excluded from a workspace?
When uv discovers a potential workspace root, it checks if the project is included in the workspace's members globs and not excluded by exclude globs using is_included_in_workspace and is_excluded_from_workspace (lines 1529-1544). If excluded, uv returns Ok(None) at line 1537 and treats the project as a standalone single-package workspace rather than part of the larger workspace.
Can uv discover projects with a pyproject.toml but no [project] table?
If a pyproject.toml lacks both a [project] table and a tool.uv.workspace definition, uv logs a warning at line 1578 and treats the directory as a non-project. For workspace member discovery, uv explicitly requires the [project] table and returns MissingProject errors if it's absent (around line 5148 context).
How does uv prevent nested workspaces?
During member collection in collect_members_only (lines 5115-5210), uv checks each member's pyproject.toml for the presence of tool.uv.workspace. If a member defines its own workspace, uv returns a NestedWorkspace error at line 5245, preventing recursive workspace definitions that would create ambiguous ownership hierarchies.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →