Skip to content

Commit 9e9df21

Browse files
authored
Drop the polyfill for native workspace APIs
2 parents 6dca4e9 + 72d7c16 commit 9e9df21

9 files changed

Lines changed: 135 additions & 105 deletions

File tree

‎.dagger/modules/e2e/main.dang‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ type E2e {
109109
null
110110
}
111111

112+
"""
113+
Lookup should refuse a module this SDK does not manage, rather than handing
114+
back a Mod for someone else's module.
115+
"""
116+
pub unmanagedLookupCheck(ws: Workspace!): Void @check {
117+
let found = pythonSdk.mod(ws, path: nonPythonModulePath).rootPath rescue "raised"
118+
119+
assert(found == "raised", "lookup returned a module this SDK does not manage: " + found)
120+
121+
null
122+
}
123+
112124
"""
113125
Lookup must choose the nearest module config even when its filename differs
114126
from an ancestor config, and modern TOML modules must be supported.
@@ -168,6 +180,32 @@ type E2e {
168180
null
169181
}
170182

183+
"""
184+
Init into a directory that already holds files should layer the template onto
185+
it, never replace it: a user running init inside an existing project keeps
186+
their files.
187+
"""
188+
pub initOverExistingCheck(ws: Workspace!): Void @check {
189+
let changes = pythonSdk.initModule(ws, name: "init-over-existing", path: lookupModulePath)
190+
191+
assert(
192+
changes.removedPaths.length == 0,
193+
"init removed existing files: " + changes.removedPaths.join(", "),
194+
)
195+
assertContains(
196+
changes.after.file(lookupModulePath + "/src/lookup_app/__init__.py").contents,
197+
"class LookupApp:",
198+
"init must leave the existing module source in place",
199+
)
200+
assertContains(
201+
changes.after.file(lookupModulePath + "/src/init_over_existing/__init__.py").contents,
202+
"class InitOverExisting:",
203+
"init did not render the template into the existing directory",
204+
)
205+
206+
null
207+
}
208+
171209
"""
172210
The default template should render a working module that reads source from
173211
the workspace and returns a ready-to-build container. The empty template

‎README.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Shared, language-agnostic operations — editing a module's dependencies or its
1010
required engine version — are owned by the core CLI (`dagger module deps`,
1111
`dagger module engine`) and are no longer part of this module's surface.
1212

13-
Backed by [`github.com/dagger/sdk-sdk/polyfill`](https://github.com/dagger/sdk-sdk/tree/main/polyfill).
13+
It uses the engine's native `Workspace` and `ModuleSource` APIs directly.
1414

1515
## Install
1616

@@ -48,8 +48,9 @@ dagger module init python my-module \
4848
--base-image python:3.13-slim
4949
```
5050

51-
`--template` picks a starter template (`minimal` is the default; `legacy` gives
52-
you a container-echo example). The three `pyproject.toml` flags are optional; by
51+
`--template` picks a starter template: `default` (a small working module) when
52+
you pass nothing, `empty` for a bare object class, or `legacy` for a
53+
container-echo example. The three `pyproject.toml` flags are optional; by
5354
default the template's Python version is used, uv is enabled, and no base image
5455
override is written.
5556

@@ -120,10 +121,9 @@ dagger call python-sdk modules path
120121
```
121122

122123
> [!NOTE]
123-
> `modules` and `generate-all` discover modules by scanning legacy
124-
> `dagger.json` files for `sdk.source == "python"`. This is obsolete for
125-
> workspace-managed modules, where the engine owns the
126-
> `modules.<sdk>.as-sdk.modules` source of truth.
124+
> `modules` and `generate-all` read the modules registered under
125+
> `modules.<sdk>.as-sdk.modules`, which the engine owns and narrows to the
126+
> caller's cwd. Nothing scans module config files.
127127
128128
See [`python-sdk.dang`](./python-sdk.dang) for the full type surface.
129129

‎dagger.json‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
{
22
"name": "python-sdk",
3-
"engineVersion": "v1.0.0-0",
3+
"engineVersion": "v1.0.0-beta.10",
44
"sdk": {
55
"source": "dang"
6-
},
7-
"dependencies": [
8-
{
9-
"name": "polyfill",
10-
"source": "github.com/dagger/polyfill@main",
11-
"pin": "16627066d1852106320bdc0cfa0e5f901efe5970"
12-
}
13-
]
6+
}
147
}

‎dagger.lock‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
[["version","1"]]
2-
["","git.head",["https://github.com/dagger/sdk-sdk"],"e1747f4b6221fa24da080701e027243e0cc5fa33","float"]
3-
["","git.ref",["https://github.com/dagger/polyfill","main"],"ec3ea84a2351b4beb06ecece951f2e5ef66509ff","float"]
1+
[["version","2"]]
2+
["","container.from",["docker.io/library/alpine:3.22","linux/amd64"],"sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce"]
3+
["","container.from",["docker.io/library/golang:1.25-alpine","linux/amd64"],"sha256:1ae0735f00daffa3aaf1363a5184c0d2dc55c78e3db4ec70241cdac97bf84b59"]
4+
["","git.ref",["https://github.com/dagger/sdk-sdk","HEAD"],{"ref":"refs/heads/main","sha":"00bb06748bcf22d724ed467f2298d31f1fb49be0"}]

‎dagger.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ path = ".dagger/modules/e2e/fixtures/toml/app"
3636

3737
[[modules.python-sdk.as-sdk.modules]]
3838
path = ".dagger/modules/e2e/fixtures/mixed-discovery/ancestor/work/app"
39+
40+
[[modules.python-sdk.as-sdk.modules]]
41+
path = ".dagger/modules/e2e/fixtures/config/app"
42+
43+
[[modules.python-sdk.as-sdk.modules]]
44+
path = ".dagger/modules/e2e/fixtures/config/configured"

‎docs/cwd-aware-discovery.md‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
11
# CWD-aware module discovery
22

3-
The Python SDK delegates config discovery to `github.com/dagger/polyfill`, then
4-
intersects the discovered directories with `currentModule.asSDK.modules`. The
5-
engine's managed-module list remains authoritative while the caller's current
6-
directory determines scope.
3+
The Python SDK asks
4+
`currentModule.asSDK(workspace: ws).modules` for the registered modules
5+
relevant to the caller's current directory. The engine owns both membership and
6+
scope selection, so the SDK does not scan config files or reconstruct the cwd
7+
policy.
78

8-
Discovery returns modules at or below the cwd and, when the cwd has no module
9-
config, its nearest enclosing module. Both `dagger-module.toml` and legacy
10-
`dagger.json` are considered together, so the nearest config wins regardless of
11-
filename. Virtual environments and installed packages are excluded.
9+
Selection returns modules at or below the cwd and, when the cwd itself is not a
10+
registered module, its nearest registered ancestor.
11+
12+
`mod` resolves a single module from the same list, re-anchoring the workspace at
13+
the requested path so the engine scopes the selection there. A path that no
14+
registered module contains is an error rather than a module this SDK does not
15+
manage.
1216

1317
```console
1418
dagger check -l
1519
dagger call e-2-e mixed-config-lookup-check
1620
dagger call e-2-e module-discovery-check
21+
dagger call e-2-e unmanaged-lookup-check
1722
```
1823

19-
The fixtures cover mixed nested config formats, modern and legacy configs,
20-
non-Python exclusion, root discovery, and discovery from inside a module.
24+
The fixtures cover mixed nested config formats, modern and legacy modules,
25+
non-Python exclusion, root selection, and selection from inside a module.

‎mod-config.dang‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ type ModConfig {
8383
}
8484

8585
let edited = withImage.file(toolPath).contents
86-
polyfill.workspace(ws).fork.withNewFile(pyprojectPath, edited).changes
86+
ws.withNewFile("/" + pyprojectPath, edited).changes(ws)
8787
}
8888

8989
"""

‎mod.dang‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,9 @@ type Mod {
5656
"""
5757
pub generate: Changeset! {
5858
if (skipGenerate) {
59-
polyfill.workspace(ws).fork.changes
59+
ws.changes(ws)
6060
} else {
61-
# Stage the local dependency closure so this module's codegen sees
62-
# up-to-date dependency bindings before generating it.
63-
let stagedWs = ws.withChanges(polyfill.workspace(ws).moduleSource("/" + rootPath).core.generateLocalDependencies(ws))
64-
polyfill.workspace(stagedWs).moduleSource("/" + rootPath).generate.changes
61+
ws.moduleSource("/" + rootPath).generate(ws).changes(ws)
6562
}
6663
}
6764
}

‎python-sdk.dang‎

Lines changed: 60 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
Manage Dagger modules that use the Python SDK.
33
"""
44
type PythonSdk {
5-
let legacyConfigPattern = "\"sdk\"\\s*:\\s*\\{[^}]*\"source\"\\s*:\\s*\"python\""
6-
let tomlConfigPattern = "\\[runtime\\][^\\[]*source\\s*=\\s*\"python\""
7-
let moduleConfigFilenames: [String!]! = ["dagger-module.toml", "dagger.json"]
8-
95
"""
106
Marker filename that skips generate when found at or above a Python SDK module root.
117
"""
@@ -17,34 +13,16 @@ type PythonSdk {
1713
pub targetRuntime: String! { "python" }
1814

1915
"""
20-
Return every managed Python SDK module visible from the client's cwd: the
21-
nearest enclosing module plus modules at or below the cwd. Discovery uses the
22-
shared polyfill and intersects its results with the SDK list on the passed
23-
workspace.
16+
Return every managed Python SDK module in the client's cwd scope: every module
17+
at or below the cwd, plus the nearest enclosing module when the cwd itself is
18+
not managed. The engine selects this directly from the workspace's registered
19+
SDK modules; no filesystem discovery is required.
2420
"""
2521
pub modules(ws: Workspace!): [Mod!]! {
26-
let managed = currentModule.asSDK(workspace: ws).modules.{{path}}
27-
let cwd = normalizePath(ws.cwd)
28-
polyfill.workspace(ws)
29-
.findConfigDirs(moduleConfigFilenames, exclude: ["**/.venv/**", "**/site-packages/**"])
30-
.map { dir => workspacePath(cwd, dir) }
31-
.uniq
32-
.filter { path => managed.filter { m => normalizePath(m.path) == path }.length > 0 }
33-
.map { path => Mod(rootPath: path, ws: ws, skipGenerateFilename: skipGenerateFilename) }
34-
}
35-
36-
let workspacePath(cwd: String!, path: String!): String! {
37-
let base = if (cwd == ".") { [] } else { cwd.split("/") }
38-
let segments = path.split("/").reduce(base) { acc, segment =>
39-
if (segment == "..") {
40-
acc.dropLast(1)
41-
} else if (segment == "." or segment == "") {
42-
acc
43-
} else {
44-
acc + [segment]
45-
}
46-
}
47-
if (segments.length == 0) { "." } else { segments.join("/") }
22+
currentModule
23+
.asSDK(workspace: ws)
24+
.modules.{{path}}
25+
.map { module => Mod(rootPath: module.path, ws: ws, skipGenerateFilename: skipGenerateFilename) }
4826
}
4927

5028
let normalizePath(path: String!): String! {
@@ -56,8 +34,8 @@ type PythonSdk {
5634
if (path == null) { -1 } else if (path == ".") { 0 } else { path.split("/").length }
5735
}
5836

59-
let configDir(path: String!, filename: String!): String! {
60-
normalizePath(path.trimPrefix("/").trimSuffix(filename))
37+
let pathContains(root: String!, path: String!): Boolean! {
38+
root == "." or path == root or path.trimPrefix(root + "/") != path
6139
}
6240

6341
"""
@@ -87,26 +65,15 @@ type PythonSdk {
8765
}
8866

8967
"""
90-
Return the Python SDK module at or above a workspace path.
68+
Return the managed Python SDK module at or above a workspace path.
9169

92-
When `findUp` is true, `path` may point inside the module.
70+
When `findUp` is true, `path` may point inside the module; the nearest
71+
enclosing module wins. When it is false, `path` is taken as the module root
72+
as given, without asking whether this SDK manages it.
9373
"""
9474
pub mod(ws: Workspace!, path: String! = ".", findUp: Boolean! = true): Mod! {
9575
let modPath = if (findUp) {
96-
let foundToml = ws.findUp("dagger-module.toml", path)
97-
let foundJson = ws.findUp("dagger.json", path)
98-
let tomlBase = if (foundToml == null) { null } else { configDir(foundToml, "dagger-module.toml") }
99-
let jsonBase = if (foundJson == null) { null } else { configDir(foundJson, "dagger.json") }
100-
101-
if (tomlBase == null and jsonBase == null) {
102-
raise "no Dagger module found containing path: " + path
103-
} else if (tomlBase != null and pathDepth(tomlBase) > pathDepth(jsonBase)) {
104-
validateConfig(ws, tomlBase, "dagger-module.toml", tomlConfigPattern, path)
105-
} else if (jsonBase != null) {
106-
validateConfig(ws, jsonBase, "dagger.json", legacyConfigPattern, path)
107-
} else {
108-
raise "no Dagger module found containing path: " + path
109-
}
76+
managedModuleAtOrAbove(ws, path)
11077
} else {
11178
normalizePath(path)
11279
}
@@ -118,15 +85,30 @@ type PythonSdk {
11885
)
11986
}
12087

121-
let validateConfig(ws: Workspace!, base: String!, filename: String!, pattern: String!, requestedPath: String!): String! {
122-
let configPath = if (base == ".") { filename } else { base + "/" + filename }
123-
let matches = ws
124-
.directory("/", include: [configPath])
125-
.file(configPath)
126-
.search(pattern: pattern, multiline: true, dotall: true, limit: 1)
127-
.{{id}}
128-
.length > 0
129-
if (matches) { base } else { raise "Dagger module does not use the Python SDK: " + requestedPath }
88+
"""
89+
Root of the nearest managed Python SDK module containing `path`.
90+
91+
Re-anchoring the workspace at `path` makes the engine scope its SDK module
92+
list to that directory, which is the same selection `modules` reads. Nothing
93+
here parses module config: registration is what makes a module ours.
94+
"""
95+
let managedModuleAtOrAbove(ws: Workspace!, path: String!): String! {
96+
let scopedWs = ws.withWorkdir(path)
97+
let requested = normalizePath(scopedWs.cwd)
98+
let found = currentModule
99+
.asSDK(workspace: scopedWs)
100+
.modules.{{path}}
101+
.map { module => normalizePath(module.path) }
102+
.filter { root => pathContains(root, requested) }
103+
.reduce(null) { best, root =>
104+
if (pathDepth(root) > pathDepth(best)) { root } else { best }
105+
}
106+
107+
if (found == null) {
108+
raise "no managed Python SDK module found containing path: " + path
109+
} else {
110+
found
111+
}
130112
}
131113

132114
"""
@@ -162,9 +144,24 @@ type PythonSdk {
162144
} else {
163145
let templateSource = configuredTemplate(renderedTemplate(name, selectedTemplate), pythonVersion, useUv, baseImage)
164146

165-
polyfill.workspace(ws).fork
166-
.withDirectory(modPath, templateSource)
167-
.changes
147+
# withNewDirectory replaces the directory it writes, so layer the
148+
# template onto what is already there: init must never remove a user's
149+
# files.
150+
ws
151+
.withNewDirectory("/" + modPath, existingDir(ws, modPath).withDirectory(".", templateSource))
152+
.changes(ws)
153+
}
154+
}
155+
156+
"""
157+
Existing contents of a workspace directory, empty when it does not exist yet.
158+
"""
159+
let existingDir(ws: Workspace!, path: String!): Directory! {
160+
let filtered = ws.directory("/", include: [path + "/**"])
161+
if (filtered.exists(path)) {
162+
filtered.directory(path)
163+
} else {
164+
directory
168165
}
169166
}
170167

@@ -219,18 +216,11 @@ type PythonSdk {
219216
Modules with the generate skip marker are skipped.
220217
"""
221218
pub generateAll(ws: Workspace!): Changeset! @generate {
222-
let pws = polyfill.workspace(ws)
223-
224219
modules(ws)
225220
.filter { mod => mod.skipGenerate == false }
226-
.reduce(pws.fork) { fork, mod =>
227-
# Stage this module's local dependency closure first (leaf-first, possibly
228-
# across SDKs) so its codegen sees up-to-date dependency bindings. The dep
229-
# codegen is ephemeral: it appears in both the fork's before and after, so
230-
# it cancels in the merge, leaving only each module's own changes.
231-
let stagedWs = ws.withChanges(pws.moduleSource("/" + mod.rootPath).core.generateLocalDependencies(ws))
232-
fork.merge(polyfill.workspace(stagedWs).moduleSource("/" + mod.rootPath).generate)
221+
.reduce(ws) { stagedWs, mod =>
222+
stagedWs.moduleSource("/" + mod.rootPath).generate(stagedWs)
233223
}
234-
.changes
224+
.changes(ws)
235225
}
236226
}

0 commit comments

Comments
 (0)