Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion .dagger/modules/e2e/main.dang
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,33 @@ type E2e {
assert(value.contains(want) == false, message)
}

"""
Assert that a string contains every expected substring.
"""
let assertContainsAll(value: String!, wants: [String!]!): Void {
wants.each { want =>
assertContains(value, want, "expected value to contain: " + want)
}
null
}

"""
Assert that a string contains none of the unwanted substrings.
"""
let assertContainsNone(value: String!, unwanted: [String!]!): Void {
unwanted.each { want =>
assertNotContains(value, want, "expected value not to contain: " + want)
}
null
}

"""
Return the rendered Python module source from an init changeset.
"""
let initSource(changes: Changeset!, path: String!, package: String!): String! {
changes.layer.file(path + "/src/" + package + "/__init__.py").contents
}

"""
The helper should expose the generate skip marker used by callers.
"""
Expand Down Expand Up @@ -134,13 +161,56 @@ type E2e {
assert(contains(defaultChanges.addedPaths, defaultPath + "/" + generatedMarkerPath) == false, "init should not produce SDK-generated files")
assert(defaultChanges.modifiedPaths.length == 0, "init unexpectedly modified existing files")
assert(defaultChanges.removedPaths.length == 0, "init unexpectedly removed files")
assertContains(defaultChanges.layer.file(defaultPath + "/src/init_default/__init__.py").contents, "class InitDefault:", "minimal template did not render the module type")
assertContains(defaultChanges.layer.file(defaultPath + "/src/init_default/__init__.py").contents, "class InitDefault:", "default template did not render the module type")

assertContains(legacyChanges.layer.file(legacyPath + "/src/init_legacy/main.py").contents, "class InitLegacy:", "legacy template did not render the module type")

null
}

"""
The default template should render a working module that reads source from
the workspace and returns a ready-to-build container. The empty template
should remain available as a bare object class.
"""
pub templateCheck(ws: Workspace!): Void @check {
let defaultPath = outputRoot + "/template-default"
let emptyPath = outputRoot + "/template-empty"

let defaultSource = initSource(
pythonSdk.initModule(ws, name: "template-default", path: defaultPath),
defaultPath,
"template_default",
)
assertContainsAll(defaultSource, [
"class TemplateDefault:",
"@classmethod\n def create(",
"ws: dagger.Workspace",
"base_image_address: str = \"alpine:3.24\"",
"return cls(",
"def container(self) -> dagger.Container:",
".with_directory(\"/src\", self.source)",
])
assertContainsNone(defaultSource, ["def __init__", "{{"])

let compatibleDefaultSource = initSource(
pythonSdk.initModule(ws, name: "template-default", path: defaultPath, template: ""),
defaultPath,
"template_default",
)
assert(compatibleDefaultSource == defaultSource, "an explicitly empty template should select the default template")

let emptySource = initSource(
pythonSdk.initModule(ws, name: "template-empty", path: emptyPath, template: "empty"),
emptyPath,
"template_empty",
)
assertContainsAll(emptySource, ["class TemplateEmpty:", " pass"])
assertContainsNone(emptySource, ["def container", "{{"])

null
}

"""
Generating an existing module should produce generated files rooted at that
module without touching unrelated paths.
Expand Down
9 changes: 5 additions & 4 deletions python-sdk.dang
Original file line number Diff line number Diff line change
Expand Up @@ -136,14 +136,15 @@ type PythonSdk {
new module's `path`. Engine-owned files (dagger-module.toml, workspace config
updates) are produced by the engine and merged with this changeset.

Pass `template` to materialize files from templates/<template>. The empty
default uses this module's minimal template.
Pass `template` to pick a starter under templates/<template>. It defaults to
`default` (a small working module). Use `empty` for a bare object class. An
explicitly empty template also selects `default` for compatibility.

Pass `pythonVersion`, `useUv`, or `baseImage` to customize the generated
pyproject.toml. Defaults leave the template untouched: the template's Python
version is used, uv is enabled, and no base image override is written.
"""
pub initModule(ws: Workspace!, name: String!, path: String!, template: String! = "", pythonVersion: String! = "", useUv: Boolean! = true, baseImage: String! = ""): Changeset! {
pub initModule(ws: Workspace!, name: String!, path: String!, template: String! = "default", pythonVersion: String! = "", useUv: Boolean! = true, baseImage: String! = ""): Changeset! {
let rawPath = path.trimPrefix("./").trimPrefix("/")

let modPath = if (rawPath == "" or rawPath == ".") {
Expand All @@ -154,7 +155,7 @@ type PythonSdk {
rawPath.trimSuffix("/")
}

let selectedTemplate = if (template == "") { "minimal" } else { template }
let selectedTemplate = if (template == "") { "default" } else { template }

if (currentModule.source.exists("templates/" + selectedTemplate) == false) {
raise "unknown init template: " + template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires-python = ">=3.14"
dependencies = ["dagger-io"]

[build-system]
requires = ["uv_build>=0.8.4,<0.9.0"]
requires = ["uv_build>=0.8.4,<0.12.0"]
build-backend = "uv_build"

[tool.uv.sources]
Expand Down
39 changes: 39 additions & 0 deletions templates/default/src/{{.ModulePackage}}/__init__.py.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import dagger
from dagger import dag, function, object_type


@object_type
class {{ .ModuleType }}:
source: dagger.Directory
base_image_address: str

@classmethod
def create(
cls,
ws: dagger.Workspace,
base_image_address: str = "alpine:3.24",
):
return cls(
source=ws.directory(
"/",
exclude=[
"**/.dagger",
"**/.git",
"**/.venv",
"**/__pycache__",
"**/node_modules",
"**/dist",
],
),
base_image_address=base_image_address,
)

@function
def container(self) -> dagger.Container:
"""A container with the workspace source, ready to build."""
return (
dag.container()
.from_(self.base_image_address)
.with_directory("/src", self.source)
.with_workdir("/src")
)
12 changes: 12 additions & 0 deletions templates/empty/pyproject.toml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "{{ .ModuleName }}"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = ["dagger-io"]

[build-system]
requires = ["uv_build>=0.8.4,<0.12.0"]
build-backend = "uv_build"

[tool.uv.sources]
dagger-io = { path = "sdk", editable = true }
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dagger import object_type


@object_type
class {{ .ModuleType }}:
pass
2 changes: 1 addition & 1 deletion templates/legacy/pyproject.toml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires-python = ">=3.14"
dependencies = ["dagger-io"]

[build-system]
requires = ["uv_build>=0.8.4,<0.9.0"]
requires = ["uv_build>=0.8.4,<0.12.0"]
build-backend = "uv_build"

[tool.uv.sources]
Expand Down