Skip to content

Commit 43e407c

Browse files
authored
Merge pull request #1 from dagger/python-tooling-library
Python tooling library
2 parents 7ff2e29 + 22bf5e0 commit 43e407c

83 files changed

Lines changed: 6202 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agent/docs/prototype-plan.md‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Plan: a default-Python-tooling Dagger module
2+
3+
## Goal and boundary
4+
5+
`dagger/go` gives a Go workspace free defaults: discover every module, then lint,
6+
test, and generate across all of them. This module does the same for Python
7+
source, minus lint and format — those belong to `dagger/ruff`, which already
8+
covers them with a standalone binary and no Python runtime. Installing both
9+
gives a Python workspace the same "free defaults" experience a Go workspace
10+
gets today.
11+
12+
What is left once lint and format are removed is the part that actually needs a
13+
Python runtime and a resolver: **tests**, **type checking**, **dependency
14+
lock freshness**, and **packaging**. Those are the four things this module owns.
15+
16+
## Discovery
17+
18+
Marker: `pyproject.toml`, via the native `ws.findRoots(markers: ["pyproject.toml"])`
19+
(beta.10). No polyfill dependency. `findRoots` answers relative to the workspace
20+
cwd — `.` for the cwd, `..` for an enclosing project — so results are resolved
21+
back to workspace-root-relative paths before they become project roots, using the
22+
same `workspaceRootPath` helper shape as `dagger/go` PR #42.
23+
24+
Discovery excludes `**/.venv/**`, `**/site-packages/**`, `**/node_modules/**`:
25+
a virtualenv contains hundreds of vendored `pyproject.toml` files and every one
26+
of them would otherwise become a "project".
27+
28+
Legacy `setup.py` / `requirements.txt` projects are **not** supported in v1.
29+
It is not a matter of adding a marker: a `requirements.txt` directory has no
30+
declarative project metadata, so `uv run`/`uv lock`/`uv build` do not apply and
31+
the whole toolchain below would need a second code path (`uv pip install -r`,
32+
no lock, no build). PEP 621 `pyproject.toml` is the modern baseline and is what
33+
`uv`, `pytest`, `mypy`, and `ruff` all key off. Revisit if real workspaces ask.
34+
35+
## Toolchain
36+
37+
Astral-aligned, matching how `dagger/ruff` is positioned.
38+
39+
- **`uv`** for everything environment-shaped. Non-negotiable given the ecosystem
40+
choice. The binary is pinned via a Dependabot-trackable
41+
`images/uv/Dockerfile`, exactly like `dagger/ruff` pins the ruff binary, and
42+
is layered onto a `python:<version>-slim` base. Debian, not Alpine: Python
43+
wheels are manylinux, so musl forces source builds of half the ecosystem.
44+
- **`pytest` via `uv run --with pytest`** for tests. `--with` means the project
45+
does not have to declare pytest itself to get a test run, and a project that
46+
*does* pin pytest still resolves to its own pin.
47+
- **`mypy` via `uv run --with mypy`** for type checking, with `ty` selectable by
48+
a one-word constructor switch. mypy is the default because it is the stable,
49+
de-facto standard whose `[tool.mypy]` config already exists in real projects,
50+
and because its non-strict default only checks annotated code — so it stays
51+
quiet on codebases that have not opted in, which is what a "free defaults"
52+
module needs. `ty` is the Astral-aligned successor but is still 0.0.x/beta as
53+
of this writing; making it a switch rather than the default means flipping it
54+
later is a one-line change, not a migration.
55+
- **`uv lock`** for dependency freshness, exposed as a `@generate` changeset
56+
rather than a pass/fail `uv lock --check`. Drift then shows up in
57+
`dagger check` *and* is fixable with `dagger generate` — strictly more useful
58+
than a check that only tells you it is stale.
59+
- **`uv build`** for packaging, exposed as a plain `Directory!` of `dist/`
60+
rather than a `@check`. Not every `pyproject.toml` is buildable (app-only
61+
projects, non-packaged uv workspace members), so making it a default check
62+
would fail workspaces that are perfectly fine.
63+
64+
Each of these is gated on a real precondition so an unrelated project is never
65+
failed by a tool it does not use: tests only run where test files exist
66+
(bare `pytest` exits 5 on an empty collection), lock only runs where a `uv.lock`
67+
already exists (so nothing is forced to adopt uv locking), and type checking
68+
only runs where `.py` files exist.
69+
70+
## Include strategy
71+
72+
Deliberate v1 scope reduction: **no static import-graph analysis.** `dagger/go`
73+
ships a compiled `go-includes` helper that walks the import graph to compute a
74+
minimal per-module include set. Python's equivalent would be a real analyzer,
75+
and it would buy much less: `uv` needs the whole project tree anyway (build
76+
backends read arbitrary files, `[tool.uv.workspace]` members are path
77+
dependencies, `conftest.py` and fixture data are loaded at runtime and are
78+
invisible to imports). So the include set is layout-based:
79+
80+
<path>/** minus **/.venv/**, **/site-packages/**, **/node_modules/**,
81+
**/__pycache__/**, **/*.pyc, **/.pytest_cache/**,
82+
**/.mypy_cache/**, **/.ruff_cache/**, **/*.egg-info/**
83+
84+
plus `includeExtraFiles` for workspace-root files a project needs. Including
85+
`<path>/**` wholesale is not laziness here — it is what makes uv workspaces
86+
work, since a workspace root genuinely needs its members' sources on disk.
87+
88+
`uv.lock` must be inside the include set. That is the regenerate-idempotency
89+
trap: the changeset baseline is the project's own source, so if the lock were
90+
excluded, a second `uv lock` would report it as newly added forever. There is an
91+
e2e check for exactly this.
92+
93+
## Nested projects
94+
95+
A nested `pyproject.toml` is its own discovered project with its own run. The
96+
parent therefore *mounts* the nested subtree (uv needs it) but *excludes it from
97+
its own commands*: `pytest --ignore=<rel>` and `mypy --exclude '^<rel>/'` /
98+
`ty check --exclude <rel>/`. Without that, a nested project's tests run twice
99+
and a parent fails on a child whose dependencies it never synced.
100+
101+
## Shape
102+
103+
Mirrors `Go`/`GoModule`.
104+
105+
Python PythonProject
106+
------ -------------
107+
version / base (mutually exclusive) path
108+
typeChecker ("mypy" | "ty") version
109+
includeExtraFiles includeExtraFiles
110+
test / typeCheck / lock (selection) skipTest / skipTypeCheck / skipLock
111+
projects(ws, include:, exclude:, ...) hasTests / hasPythonFiles / hasLock
112+
project(ws, path, findUp:) nestedProjects
113+
includeBase / include / exclude / source
114+
testAll @check test @check
115+
typeCheckAll @check typeCheck @check
116+
lockAll @generate lock -> Changeset!
117+
build -> Directory!
118+
119+
Selection patterns use `dagger/go`'s rules verbatim: bare pattern includes,
120+
`"!"`-prefix excludes, exclude always wins regardless of order, empty list means
121+
everything, `"**"`/`"*"` match all, `X` and `X/**` both mean "X and below".
122+
Consistency across the two modules is worth more here than any improvement.
123+
124+
## Testing
125+
126+
`.dagger/modules/e2e` with `testdata/` fixtures, following `dagger/go`:
127+
discovery (root / nested / deep / cwd-relative `..` resolution / `.venv`
128+
exclusion), a passing and a failing test project, a passing and a failing type
129+
check, both type checkers, nested-project isolation, selection patterns,
130+
lock drift detection, and regenerate idempotency (`lockAll` twice on a current
131+
lock reports zero added/modified/removed).

0 commit comments

Comments
 (0)