Repository navigation
Expand file tree
/
Copy pathpython-sdk.dang
More file actions
753 lines (678 loc) · 28.2 KB
/
Copy pathpython-sdk.dang
File metadata and controls
753 lines (678 loc) · 28.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
"""
Manage Dagger modules that use the Python SDK.
"""
type PythonSdk {
"""
Starter under templates/<template> for a module generated without source.
"""
pub template: String! = "default"
"""
Python version written to a new module's pyproject.toml; empty keeps the template's.
"""
pub pythonVersion: String! = ""
"""
Whether a new module builds with uv instead of pip.
"""
pub useUv: Boolean! = true
"""
Base container image written to a new module's pyproject.toml; empty keeps the SDK default.
"""
pub baseImage: String! = ""
"""
Put a module on the shared entrypoint this SDK publishes instead of one
generated into it. A module that names a runtime of its own keeps it.
"""
pub remoteEntrypoint: Boolean! = false
"""
Run a module on the module runtime this SDK publishes instead of an
entrypoint: the runtime builds the module and answers every call, as the
runtime built into the engine did. A module whose files are not its
directory alone takes this path whether the flag is set or not. A module
that names a runtime of its own keeps it.
"""
pub legacyRuntime: Boolean! = false
"""
Find the Python client root containing the workspace cwd: the directory of
the nearest pyproject.toml, relative to the workspace root. A generated
member, marked `[tool.dagger] generated` in its pyproject.toml, belongs to
the scope above it, so that scope answers. A module's vendored client
library from before the marker carries its own pyproject.toml too, so the
Python module owning it answers instead; a directory with a module config
of its own is a module, never a vendored library. Null when there is none.
"""
pub findClientRoot(ws: Workspace!): String {
let found = ws.findUp("pyproject.toml")
if (found == null) {
null
} else {
clientRootOf(ws, normalizePath(found.trimSuffix("pyproject.toml")))
}
}
"""
The scope that a pyproject.toml belongs to.
"""
let clientRootOf(ws: Workspace!, scope: String!): String! {
if (scope != "." and isGeneratedMember(ws, scope)) {
# Members sit at most two levels below their scope, but a scope can be
# a member of something else, so lift until the file is not a member's.
let above = ws.findUp("pyproject.toml", from: "/" + parentPath(scope))
if (above == null) {
scope
} else {
clientRootOf(ws, normalizePath(above.trimSuffix("pyproject.toml")))
}
} else {
let vendorParent = if (scope == vendorDirName) {
"."
} else if (scope.trimSuffix("/" + vendorDirName) != scope) {
scope.trimSuffix("/" + vendorDirName)
} else {
""
}
if (vendorParent == "" or hasModuleConfig(ws, scope)) {
scope
} else if (hasModuleConfig(ws, vendorParent) and scopeHasFile(ws, vendorParent, "pyproject.toml")) {
vendorParent
} else {
scope
}
}
}
"""
Whether a scope's pyproject.toml is one this SDK generated: sdk/, core or a
client, as its marker says when the file is read as TOML.
"""
let isGeneratedMember(ws: Workspace!, scope: String!): Boolean! {
scopeHasFile(ws, scope, "pyproject.toml") and
PyprojectHelper().generatedKind(ws.file("/" + scopePath(scope, "pyproject.toml"))) != ""
}
let parentPath(path: String!): String! {
let parts = path.split("/")
if (parts.length < 2) { "." } else { parts.takeFirst(parts.length - 1).join("/") }
}
"""
Directory a module's generated client library is vendored into.
"""
let vendorDirName: String! = "sdk"
"""
The shared Dang entrypoint every module names unless it has a static one,
served from this repository's entrypoint/ directory. `@v1` selects the
greatest `entrypoint/v1.*` tag of this repository, then the greatest plain
`v1.*` one.
"""
let sharedEntrypointSource: String! = "dagger.io/sdk/python/entrypoint@v1"
"""
The module runtime this SDK publishes, which builds a module and answers its
calls. A module reference is a legal [runtime] source, and `@v1` selects the
greatest v1 tag as it does for the entrypoint.
"""
let sdkRuntimeSource: String! = "dagger.io/sdk/python/runtime@v1"
"""
The engine version a manifest this SDK writes for that runtime declares: the
version the generator reads core in, so a module's bindings and the view its
calls run in are one. A pre-1.0 config's version does not survive the move,
for the same reason.
"""
let sdkEngineVersion: String! {
currentModule.source.directory("helpers/core-schema").asModuleSource.engineVersion
}
"""
The kinds of entrypoint a module runs on, as Mod.entrypointMode names them.
"""
let generatedMode: String! = "generated"
let remoteMode: String! = "remote"
let runtimeMode: String! = "runtime"
"""
Whether a module config, dagger-module.toml or the pre-1.0 dagger.json,
exists at a workspace-root-relative scope.
"""
let hasModuleConfig(ws: Workspace!, scope: String!): Boolean! {
scopeHasFile(ws, scope, "dagger-module.toml") or scopeHasFile(ws, scope, "dagger.json")
}
let scopeHasFile(ws: Workspace!, scope: String!, filename: String!): Boolean! {
let path = scopePath(scope, filename)
ws.directory("/", include: [path]).exists(path)
}
let scopePath(scope: String!, filename: String!): String! {
if (scope == ".") { filename } else { scope + "/" + filename }
}
"""
Generate one SDK scope, the directory at the workspace cwd: sdk/, core and
one member per client, with the SDK's entries in its pyproject.toml. A scope
without a module gets the same tree and no module config. In a module scope,
a module without a config file is initialized from the configured template,
every module receives a dagger-module.toml from the manifest builder, and a
pre-1.0 dagger.json is migrated and removed. The manifest names the shared
entrypoint generated into the module, unless the module already names an
entrypoint or a runtime of its own, which generation keeps.
"""
pub generateScope(ws: Workspace!, isModule: Boolean!, name: String!, clients: [ModuleSource!]!): Workspace! {
checkOneRunnerAsked
let scope = normalizePath(ws.cwd)
# Resolve local client paths from the workspace root.
let rooted = ws.withWorkdir(".")
if (isModule == false) {
Scope(path: scope, ws: rooted, clients: clients, isModule: false, engineVersion: "", migrating: false).generated.withWorkdir(scope)
} else {
let hadConfig = hasModuleConfig(ws, scope)
let initialized = if (hadConfig) {
rooted
} else {
rooted.withDirectory("/" + scope, moduleTemplate(name, template, pythonVersion, useUv, baseImage))
}
# Read before the manifest is written: that write resets the entrypoint
# table, and a module keeps the kind it came with.
let mode = entrypointModeOf(initialized, scope)
if (mode == generatedMode) {
checkStaticScope(initialized, scope)
}
let migrating = migratingModule(initialized, scope)
let configured = generateScopeManifest(initialized, scope, name)
Mod(
rootPath: scope,
ws: configured,
entrypointMode: mode,
migrating: migrating,
engineVersion: scopeEngineVersion(initialized, scope),
clients: clients,
).generated.withWorkdir(scope)
}
}
"""
Generate dagger-module.toml from the existing manifest. Preserve fields that
the SDK does not own. A client is a member of the scope, loaded by the code
that uses it, so the manifest lists none, and the [[dependencies]] of the
layout before go. Remove dagger.json so two manifest files cannot contain
different state.
The module runs on a Dang entrypoint and on nothing else. The runtime this
SDK used to write, `[runtime] source = "python"`, goes, and so does what
only a runtime reads: engineVersion, since an entrypoint runs a module on
the engine's own version. Any other runtime is one the user chose, and the
manifest keeps it with no entrypoint added, because the engine follows an
entrypoint over a runtime; for the same reason a manifest with both is
refused.
"""
let generateScopeManifest(ws: Workspace!, scope: String!, name: String!): Workspace! {
let root = "/" + scope
let hasToml = scopeHasFile(ws, scope, "dagger-module.toml")
let hasJson = scopeHasFile(ws, scope, "dagger.json")
if (hasToml) {
checkOneRunner(ws.file(root + "/dagger-module.toml").contents)
}
let base = if (hasToml) {
let manifest = ws.file(root + "/dagger-module.toml")
let config = tomlConfig(manifest.contents)
let toml = ManifestToml(manifest.contents)
let ours = isEntrypointManifest(manifest.contents)
# An entrypoint the user chose is theirs to replace; the one this SDK
# writes is not, and neither is none at all.
let usersEntrypoint = toml.entrypointKind == "dang" and ours == false
if (keepsItsRuntime(config)) {
sdkHelpers.moduleManifest(loadToml: withoutStaleEntrypoint(manifest))
} else if (legacyRuntime or (needsRuntimeSettings(config) and usersEntrypoint == false)) {
onSdkRuntime(sdkHelpers.moduleManifest(loadToml: withoutAnyEntrypoint(manifest)))
} else if (ours) {
# This SDK's static entrypoint manifest holds nothing the shared one keeps.
sdkHelpers.moduleManifest.withDangEntrypoint(source: sharedEntrypointSource)
} else {
let loaded = sdkHelpers.moduleManifest(loadToml: withoutStaleEntrypoint(manifest))
checkEntrypointOnly(config, "dagger-module.toml")
# A Dang entrypoint already there is kept: the shared one, or one the
# user chose, a pinned version, a fork or one of their own.
let entrypointed = if (usersEntrypoint) {
loaded
} else {
loaded.withDangEntrypoint(source: sharedEntrypointSource)
}
entrypointed.withoutLegacyFields
}
} else if (hasJson) {
let manifest = ws.file(root + "/dagger.json")
let config = json.withContents((manifest.contents :: Dagger.JSON!))
let loaded = sdkHelpers.moduleManifest(loadJson: manifest)
if (keepsItsRuntime(config)) {
loaded
} else if (legacyRuntime or needsRuntimeSettings(config)) {
onSdkRuntime(loaded)
} else {
checkEntrypointOnly(config, "dagger.json")
loaded.withDangEntrypoint(source: sharedEntrypointSource).withoutLegacyFields
}
} else if (legacyRuntime) {
onSdkRuntime(sdkHelpers.moduleManifest)
} else {
sdkHelpers.moduleManifest.withDangEntrypoint(source: sharedEntrypointSource)
}
base.withName(name: name).withoutLegacyRuntimeDependencies
.generate(ws.withWorkdir(scope), lock: false, legacyJson: false)
.withWorkdir(".")
}
"""
The runtime a manifest names, or empty for one that names none. Read with a
TOML or JSON parser, because the manifest is the user's content; a
dagger.json names it as `sdk`, as a string or as a table.
"""
let runtimeSource(config: JSONValue!): String! {
let fields = config.fields
let key = if (fields.contains("runtime")) { "runtime" } else if (fields.contains("sdk")) { "sdk" } else { "" }
if (key == "") {
""
} else {
let value = config.field([key])
value.asString rescue {
err: Error => value.field(["source"]).asString
}
}
}
"""
Whether a manifest names a runtime of the user's own: anything but the
builtin `python` this SDK wrote.
"""
let keepsItsRuntime(config: JSONValue!): Boolean! {
let source = runtimeSource(config)
source != "" and source != "python"
}
"""
Whether a manifest says what only a runtime can honour: which files are the
module, and where its source sits. A module that says either stays on a
runtime, because an entrypoint roots the module at its own directory and
takes all of it.
"""
let needsRuntimeSettings(config: JSONValue!): Boolean! {
let fields = config.fields
fields.contains("include") or fields.contains("exclude") or
(fields.contains("source") and normalizePath(config.field(["source"]).asString) != ".")
}
"""
Whether a manifest names the Python runtime built into the engine, which
generated the module's bindings at load time and gave its code a global
`dag`. The code is the old code, so the scope gets the global client.
"""
let namesBuiltinRuntime(config: JSONValue!): Boolean! {
runtimeSource(config) == "python"
}
"""
A scope's module config, whichever it has: dagger-module.toml, or the
pre-1.0 dagger.json generation converts. Read it only once hasModuleConfig
says there is one.
"""
let manifestSettings(ws: Workspace!, scope: String!): JSONValue! {
if (scopeHasFile(ws, scope, "dagger-module.toml")) {
tomlConfig(ws.file("/" + scopePath(scope, "dagger-module.toml")).contents)
} else {
json.withContents((ws.file("/" + scopePath(scope, "dagger.json")).contents :: Dagger.JSON!))
}
}
"""
Whether this generation converts a module the builtin Python runtime ran,
from a pre-1.0 dagger.json or from a manifest that still names that
runtime. Only the generation that rewrites the manifest can tell.
"""
let migratingModule(ws: Workspace!, scope: String!): Boolean! {
hasModuleConfig(ws, scope) and namesBuiltinRuntime(manifestSettings(ws, scope))
}
"""
The engine version a module's scope generates against: the one its manifest
declares, and only while the module keeps a runtime of its own, which runs
it on that version. A module on an entrypoint runs on the engine's own
version, and so does one this SDK takes over from the builtin runtime. A
pre-1.0 manifest's version outlives `dagger module migrate`, and generating
against that view writes bindings around a core the session does not serve,
which the generator then refuses beside a client read in the engine's view.
"""
let scopeEngineVersion(ws: Workspace!, scope: String!): String! {
if (keepsOwnRuntime(ws, scope)) {
let config = manifestSettings(ws, scope)
if (config.fields.contains("engineVersion")) {
config.field(["engineVersion"]).asString
} else {
""
}
} else {
""
}
}
"""
Refuse a manifest that names both a runtime of the user's own and an
entrypoint. The engine runs the entrypoint and never calls the runtime,
while generation would take the core it generates against from the
runtime's engineVersion: a core that is not the session's. Both tables are
the user's, so the user picks one.
"""
let checkOneRunner(toml: String!): Void {
let manifest = ManifestToml(toml)
let config = tomlConfig(toml)
if (manifest.hasEntrypoint and keepsItsRuntime(config)) {
raise "dagger-module.toml names both a runtime (" + runtimeSource(config) + ") and an entrypoint (" +
manifest.entrypointSource + "); the engine runs the entrypoint and never calls the runtime. " +
"Remove [entrypoint] to run on your runtime, or [runtime] to run on the entrypoint"
}
null
}
"""
Refuse what a runtime manifest can say and an entrypoint manifest cannot,
before anything is written: which files are the module (include, exclude)
and where its source is. Dropping either would change the module silently.
Two settings the builtin runtime read have a way out of their own, so they
are named one at a time.
"""
let checkEntrypointOnly(config: JSONValue!, file: String!): Void {
let fields = config.fields
if (fields.contains("disableDefaultFunctionCaching")) {
raise file + " sets disableDefaultFunctionCaching, which only a runtime reads: remove it, and pass cache= to the functions that must not be cached"
}
if (fields.contains("codegen")) {
raise file + " has a [codegen] table, which only a runtime reads: remove the table"
}
let listed = ["include", "exclude"].filter { key => fields.contains(key) }
let source = if (fields.contains("source") and normalizePath(config.field(["source"]).asString) != ".") {
["source"]
} else {
[] :: [String!]!
}
let refused = listed + source
if (refused.length > 0) {
raise file + " has settings an entrypoint manifest cannot carry (" + refused.join(", ") + "); remove them, or keep the module on a runtime of your own"
}
null
}
"""
The manifest moved onto the runtime this SDK publishes, with the version the
generator reads core in. The runtime builds the module itself, so what only
a runtime can honour stays as written.
"""
let onSdkRuntime(manifest: SdkHelpersModuleManifest!): SdkHelpersModuleManifest! {
manifest.withLegacyRuntime(runtime: sdkRuntimeSource, engineVersion: sdkEngineVersion)
}
"""
The manifest without any entrypoint table, for a module moving to a runtime:
the engine follows an entrypoint over a runtime, so one left behind would
take every call.
"""
let withoutAnyEntrypoint(manifest: File!): File! {
let toml = ManifestToml(manifest.contents)
if (toml.hasEntrypoint) { toml.withoutEntrypoint } else { manifest }
}
"""
Drop an entrypoint table an earlier SDK wrote, before the manifest is read.
That SDK named the runtime module as a module-kind entrypoint. An engine that
loads manifest version 2 rejects every kind but "dang", and sdk-helpers keeps
every table it loads with no way to clear one, so the table goes here instead.
A Dang entrypoint is kept.
"""
let withoutStaleEntrypoint(manifest: File!): File! {
let toml = ManifestToml(manifest.contents)
if (toml.hasEntrypoint == false or toml.entrypointKind == "dang") {
manifest
} else {
toml.withoutEntrypoint
}
}
"""
What a generated entrypoint cannot carry, refused before anything is
written. A manifest that names a runtime, or says what only a runtime can
honour, never reaches here: it keeps its runtime instead.
"""
let checkStaticScope(ws: Workspace!, scope: String!): Void {
if (scopeHasFile(ws, scope, "dagger-module.toml") and
tomlConfig(ws.file("/" + scopePath(scope, "dagger-module.toml")).contents).fields.contains("clients")) {
raise "dagger-module.toml has a [clients] table, which this SDK does not generate: a module's clients are its scope's, in dagger.toml; remove the table"
}
null
}
let tomlConfig(toml: String!): JSONValue! {
json.withContents((JSON.encode(TOML.decode(toml)) :: Dagger.JSON!))
}
"""
Whether the manifest names the static entrypoint this SDK generates into the
module: a Dang entrypoint at the path generation writes. Any other
entrypoint, a path of the user's own among them, is not this SDK's to
replace.
"""
let isEntrypointManifest(toml: String!): Boolean! {
let manifest = ManifestToml(toml)
manifest.entrypointKind == "dang" and normalizePath(manifest.entrypointSource) == staticEntrypointPath
}
"""
Where the static entrypoint lives in a module, as generation names it.
"""
let staticEntrypointPath: String! = vendorDirName + "/entrypoint"
"""
Refuse the two flags together: an entrypoint and a runtime are two ways to
run a module, and a manifest names one of them.
"""
let checkOneRunnerAsked: Void {
if (remoteEntrypoint and legacyRuntime) {
raise "--remote-entrypoint and --legacy-runtime cannot be set together: one asks for the entrypoint this SDK publishes, the other for its runtime, and a module runs on one of them"
}
null
}
"""
How the engine runs a module: the kind its manifest already names, and the
SDK setting for one that has no manifest yet.
"""
let entrypointModeOf(ws: Workspace!, scope: String!): String! {
# A runtime of the user's own comes first: a manifest that also names an
# entrypoint is refused when the manifest is written, by the one error that
# names both, rather than by a generated entrypoint's own objections.
if (keepsOwnRuntime(ws, scope)) {
runtimeMode
} else if (legacyRuntime) {
runtimeMode
} else if (remoteEntrypoint) {
remoteMode
} else if (needsOwnRuntime(ws, scope)) {
# Before the entrypoint a manifest names: no entrypoint can honour these
# settings, so the module moves to a runtime, and an entrypoint of the
# user's own beside them is refused when the manifest is written.
runtimeMode
} else if (hasEntrypointManifest(ws, scope)) {
generatedMode
} else if (namesDangEntrypoint(ws, scope)) {
remoteMode
} else {
generatedMode
}
}
"""
Whether a module's manifest names a Dang entrypoint that this SDK does not
write: the one it publishes, a pin of it, a fork, or one of the user's own.
"""
let namesDangEntrypoint(ws: Workspace!, scope: String!): Boolean! {
scopeHasFile(ws, scope, "dagger-module.toml") and
ManifestToml(ws.file("/" + scopePath(scope, "dagger-module.toml")).contents).entrypointKind == "dang"
}
"""
Whether a module's manifest names a runtime of the user's own, which
generation keeps and puts no entrypoint in front of.
"""
let keepsOwnRuntime(ws: Workspace!, scope: String!): Boolean! {
hasModuleConfig(ws, scope) and keepsItsRuntime(manifestSettings(ws, scope))
}
"""
Whether a module's manifest keeps it on a runtime by what it says, rather
than by naming one: a module of some files only, or rooted elsewhere.
"""
let needsOwnRuntime(ws: Workspace!, scope: String!): Boolean! {
hasModuleConfig(ws, scope) and needsRuntimeSettings(manifestSettings(ws, scope))
}
let hasEntrypointManifest(ws: Workspace!, scope: String!): Boolean! {
scopeHasFile(ws, scope, "dagger-module.toml") and isEntrypointManifest(ws.file("/" + scopePath(scope, "dagger-module.toml")).contents)
}
"""
Workspace-root-relative roots of the modules registered to this SDK.
"""
let managedRoots(ws: Workspace!): [String!]! {
ws.sdk(name: currentModule.name)
.modules.{{source}}
.map { module => normalizePath(module.source) }
}
let nearestRoot(roots: [String!]!): String {
roots.reduce(null) { best, root =>
if (pathDepth(root) > pathDepth(best)) { root } else { best }
}
}
let normalizePath(path: String!): String! {
let normalized = path.trimPrefix("./").trimPrefix("/").trimSuffix("/")
if (normalized == "") { "." } else { normalized }
}
let pathDepth(path: String): Int! {
if (path == null) { -1 } else if (path == ".") { 0 } else { path.split("/").length }
}
let pathContains(root: String!, path: String!): Boolean! {
root == "." or path == root or path.trimPrefix(root + "/") != path
}
"""
Return the starter templates tracked by this module.
Templates live under templates/<name> and are materialized into a new module
before generation. The `template` setting selects one; empty selects the
default.
"""
pub templates: [Template!]! {
if (currentModule.source.exists("templates")) {
let root = currentModule.source.directory("templates")
root.entries.map { name =>
Template(
name: name.trimSuffix("/"),
source: root.directory(name),
)
}
} else {
directory.entries.map { name =>
Template(
name: name,
source: directory,
)
}
}
}
"""
Return the managed Python SDK module at or above a workspace path.
When `findUp` is true, `path` may point inside the module; the nearest
enclosing module wins. When it is false, `path` is taken as the module root
as given, without asking whether this SDK manages it.
"""
pub mod(ws: Workspace!, path: String! = ".", findUp: Boolean! = true): Mod! {
let modPath = if (findUp) {
managedModuleAtOrAbove(ws, path)
} else {
normalizePath(path)
}
Mod(
rootPath: modPath,
ws: ws,
entrypointMode: entrypointModeOf(ws, modPath),
migrating: migratingModule(ws, modPath),
engineVersion: scopeEngineVersion(ws, modPath),
clients: null,
)
}
"""
Root of the nearest managed Python SDK module containing `path`.
Nothing here parses module config: registration is what makes a module ours.
"""
let managedModuleAtOrAbove(ws: Workspace!, path: String!): String! {
let requested = normalizePath(ws.withWorkdir(path).cwd)
let found = nearestRoot(managedRoots(ws).filter { root => pathContains(root, requested) })
if (found == null) {
raise "no managed Python SDK module found containing path: " + path
} else {
found
}
}
"""
The files of a new module: the named template rendered for the module name,
with the pyproject.toml settings applied. An empty template name selects the
default.
"""
let moduleTemplate(name: String!, template: String!, pythonVersion: String!, useUv: Boolean!, baseImage: String!): Directory! {
let selectedTemplate = if (template == "") { "default" } else { template }
let known = currentModule.source.directory("templates").entries.map { entry => entry.trimSuffix("/") }
if (known.filter { entry => entry == selectedTemplate }.length == 0) {
raise "unknown init template: " + template
} else {
configuredTemplate(renderedTemplate(name, selectedTemplate), pythonVersion, useUv, baseImage)
}
}
"""
Render a Python template with the requested module name.
"""
let renderedTemplate(name: String!, templateName: String!): Directory! {
container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/render-template"))
.withDirectory("/template", currentModule.source.directory("templates/" + templateName))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/render-template", "."])
.withExec(["render-template", name, "/template", "/rendered"])
.directory("/rendered")
}
"""
Apply non-default configuration flags to a rendered template's pyproject.toml.
When all flags are at their defaults, the source is returned unchanged (no
helper run, no reformatting). When any flag is non-default, the helper
re-marshals pyproject.toml, normalizing formatting while preserving all data.
"""
let configuredTemplate(source: Directory!, pythonVersion: String!, useUv: Boolean!, baseImage: String!): Directory! {
if (pythonVersion == "" and useUv and baseImage == "") {
source
} else {
let pyproj = "/rendered/pyproject.toml"
let built = container
.from("golang:1.25-alpine")
.withoutEntrypoint
.withMountedCache("/go/pkg/mod", cacheVolume("go-mod"))
.withMountedCache("/root/.cache/go-build", cacheVolume("go-build"))
.withDirectory("/helper", currentModule.source.directory("helpers/pyproject"))
.withWorkdir("/helper")
.withExec(["go", "build", "-o", "/usr/local/bin/pyproject", "."])
.withDirectory("/rendered", source)
let withPy = if (pythonVersion == "") { built } else { built.withExec(["pyproject", "set-python-version", pyproj, pythonVersion]) }
let withUv = if (useUv) { withPy } else { withPy.withExec(["pyproject", "set-use-uv", pyproj, "false"]) }
let withImg = if (baseImage == "") { withUv } else { withUv.withExec(["pyproject", "set-base-image", pyproj, baseImage]) }
withImg.directory("/rendered")
}
}
}
"""
A dagger-module.toml, read for its entrypoint table.
Values are read with a TOML parser, because the manifest is the user's
content: quoting, spacing and key order are theirs. The pyproject helper
removes the table and keeps every other byte as it was.
"""
type ManifestToml {
pub toml: String!
new(toml: String! = "") {
self.toml = toml
self
}
pub hasEntrypoint: Boolean! {
config.fields.contains("entrypoint")
}
"""The entrypoint's kind, or empty when the manifest has no entrypoint."""
pub entrypointKind: String! {
entrypointValue("kind")
}
pub entrypointSource: String! {
entrypointValue("source")
}
"""The manifest with its entrypoint table removed."""
pub withoutEntrypoint: File! {
PyprojectHelper().withoutTable(directory.withNewFile(fileName, toml).file(fileName), "entrypoint")
}
let fileName: String! = "dagger-module.toml"
let config: JSONValue! {
json.withContents((JSON.encode(TOML.decode(toml)) :: Dagger.JSON!))
}
let entrypointValue(key: String!): String! {
if (hasEntrypoint == false) {
""
} else {
let table = config.field(["entrypoint"])
if (table.fields.contains(key)) { table.field([key]).asString } else { "" }
}
}
}