22Manage Dagger modules that use the Python SDK.
33"""
44type 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