Skip to content

feat(pyproject): add a project and workspace discovery module - #126

Merged
danielgafni merged 4 commits into
mainfrom
feat/pyproject-module
Oct 23, 2026
Merged

danielgafni merged 4 commits into
mainfrom
feat/pyproject-module

Conversation

@danielgafni

@danielgafni danielgafni commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What

A new pyproject module: the shared discovery layer for our Python modules (uv, ruff, deptry, and later pytest/pyrefly). It runs no tools itself and exposes two collections:

  • projects: every directory holding a pyproject.toml. Fields: name, workspace (the workspace that installs it as a member), nested, local_dependencies. Functions: source(ws), pyproject(ws).
  • workspaces: every directory holding a workspace marker, uv.lock and pylock.toml by default (configurable with the workspaceMarkers setting). Fields: lockfiles, members, local_dependencies. Functions: projects(), source(ws), manifests(ws).

Both are keyed by path relative to the workspace root and take an exclude glob setting.

Why

These fix problems found in dagger/python's shared pyproject library and in our own modules:

Problem Here
Path deps and members outside the project dir (../lib) aren't mounted, so uv sync breaks source loads the project's tree plus its transitive path/workspace sources, wherever they live
A monorepo root's source carries every nested project Every file belongs to the deepest project containing it; nested projects nothing needs are excluded, including fixtures nested inside a member
Regex TOML parsing misses dotted keys, quoting and marker-split sources tomllib
Scope globs silently match nothing pathlib full_match; ** crosses directories, and a trailing /** also matches the directory itself
The dependency layer is invalidated by unrelated edits workspace.manifests(ws) holds only resolver inputs (markers, uv.toml, members' and local deps' pyproject.toml/.python-version)
Whole-repo Directory input: any edit busts everything The Workspace API with per-item include/exclude loads

uv semantics come from pyproject.toml, the same source uv itself uses, not the lockfile:

  • Membership follows [tool.uv.workspace] members/exclude, and the deepest workspace wins.
  • Members inherit the root's [tool.uv.sources], overridden per package.
  • { workspace = true } resolves by normalized name.
  • Missing path sources load nothing instead of failing (the partial-workspace case).

A shared discovery layer for the Python modules. It exposes two
collections:

- `projects`: every directory holding a `pyproject.toml`.
- `workspaces`: every directory holding a workspace marker (`uv.lock` and
  `pylock.toml` by default, configurable with `workspaceMarkers`).

Every file belongs to the deepest project containing it. Each item loads
only the source it needs through the Workspace API: its own tree plus its
local path/workspace dependencies (transitively, including ones outside
the workspace root), less nested projects nothing needs. A workspace also
exposes `manifests`, just the resolver inputs, so a dependency layer built
from it survives source edits.

Manifests are parsed with tomllib, so dotted keys, marker-split sources
and single quotes work. uv workspace membership follows
`[tool.uv.workspace]` members/exclude globs, and members inherit the
root's sources. Missing path sources load nothing instead of failing.
Workspace.find_roots walks gitignored directories on git-backed and
synthetic workspaces alike, so a gitignored copy of the repository (e.g.
an agent worktree under .claude/worktrees) showed up as a second set of
projects. Discovery now reads the workspace's .gitignore files (found
with the same pruned find_roots walk) and drops roots whose marker they
ignore.
uv applies a [tool.uv.sources] entry only to a package the project
declares as a dependency. Discovery followed every entry, so a project
depending on one member of a large uv workspace inherited the root's
sources for every member and loaded all of them: Infra's
tools/cluster-secret pulled in ~40 pulumi members through one path
dependency.

Sources now apply only to names declared in [project].dependencies,
optional dependencies, dependency groups or the legacy
tool.uv.dev-dependencies.
@danielgafni
danielgafni merged commit 8a949a7 into main Oct 23, 2026
16 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant