From d2bdd1c4f6d6f397d652a26dd127567d0b0f5614 Mon Sep 17 00:00:00 2001 From: Yves Brissaud Date: Thu, 18 Jun 2026 12:57:46 +0200 Subject: [PATCH] feat: workspace-scoped SDK settings via PythonSdk constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare a PythonSdk constructor with defaultPythonVersion, defaultUseUv and defaultBaseImage. The engine's settings discovery introspects the main object's constructor arguments, so these become the typed, described settings surfaced by `dagger settings python-sdk` (camelCase maps to kebab-case: default-python-version, default-use-uv, default-base-image) and stored under [modules.python-sdk.settings]. initModule now falls back to these workspace defaults when the matching per-init flag is omitted; an explicit flag still wins. useUv becomes nullable so an omitted flag is distinguishable from an explicit --use-uv=false, mirroring the existing nullable-boolean pattern in mod-config.dang. The defaulting is the only new logic: configuredTemplate still consumes the resolved values. Add an e2e check (initSettingsDefaultCheck) covering both inheritance of the constructor defaults and per-init override, and document the settings in the README. Discoverability and the `dagger call python-sdk …` path land with this change. End-to-end inheritance through the privileged `dagger module init python` dispatch additionally requires an engine change that threads [modules.python-sdk.settings] into the SDK constructor at init time. Signed-off-by: Yves Brissaud --- .dagger/modules/e2e/main.dang | 30 +++++++++++++++++++ README.md | 30 +++++++++++++++++++ python-sdk.dang | 55 +++++++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 3 deletions(-) diff --git a/.dagger/modules/e2e/main.dang b/.dagger/modules/e2e/main.dang index f3befa1..5cd9968 100644 --- a/.dagger/modules/e2e/main.dang +++ b/.dagger/modules/e2e/main.dang @@ -225,4 +225,34 @@ type E2e { null } + + """ + Workspace-scoped SDK settings (the PythonSdk constructor args, surfaced by + `dagger settings python-sdk`) should act as init defaults: when an init flag + is omitted, initModule inherits the constructor value, and an explicit + per-init flag still wins. + """ + pub initSettingsDefaultCheck(ws: Workspace!): Void @check { + let configuredSdk = pythonSdk(defaultPythonVersion: "3.13", defaultUseUv: false, defaultBaseImage: "python:3.13-slim") + + # Flags omitted: the constructor (workspace) defaults flow into pyproject.toml. + let inheritedPath = outputRoot + "/init-settings-inherited" + let inherited = configuredSdk.initModule(ws, name: "init-settings-inherited", path: inheritedPath) + let inheritedPyproj = inheritedPath + "/pyproject.toml" + assertAdded(inherited, inheritedPyproj) + assertContains(inherited.layer.file(inheritedPyproj).contents, ">=3.13", "default-python-version setting not applied at init") + assertContains(inherited.layer.file(inheritedPyproj).contents, "use-uv = false", "default-use-uv setting not applied at init") + assertContains(inherited.layer.file(inheritedPyproj).contents, "python:3.13-slim", "default-base-image setting not applied at init") + assertContains(inherited.layer.file(inheritedPyproj).contents, "dagger-io", "settings-default init dropped template data") + + # Explicit per-init flags override the workspace defaults. + let overridePath = outputRoot + "/init-settings-override" + let override = configuredSdk.initModule(ws, name: "init-settings-override", path: overridePath, pythonVersion: "3.12", useUv: true, baseImage: "python:3.12-slim") + let overridePyproj = overridePath + "/pyproject.toml" + assertContains(override.layer.file(overridePyproj).contents, ">=3.12", "per-init python version did not override default-python-version") + assertContains(override.layer.file(overridePyproj).contents, "python:3.12-slim", "per-init base image did not override default-base-image") + assertNotContains(override.layer.file(overridePyproj).contents, "use-uv = false", "per-init use-uv=true did not override default-use-uv=false") + + null + } } diff --git a/README.md b/README.md index b640b88..3a15814 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,36 @@ engine supplies it in the dispatched path): dagger call python-sdk init-module --name my-module --path .dagger/modules/my-module ``` +## Configure workspace defaults + +Set SDK defaults once per workspace and have them apply to the modules you +create. List the settings this SDK exposes: + +```sh +dagger settings python-sdk +``` + +Set a default (the SDK's constructor arguments are the settings; camelCase maps +to kebab-case on the CLI): + +```sh +dagger settings python-sdk default-python-version 3.13 +dagger settings python-sdk default-use-uv false +dagger settings python-sdk default-base-image python:3.13-slim +``` + +These act as fallbacks for `initModule`: when you create a module without the +matching `dagger module init python` flag (`--python-version`, `--use-uv`, +`--base-image`), the workspace default is applied. An explicit per-init flag +always wins. + +> [!NOTE] +> Settings are stored under `[modules.python-sdk.settings]` in `dagger.toml`, +> are discoverable and typed today, and populate the SDK constructor on the +> `dagger call python-sdk …` path. Inheriting them through the privileged +> `dagger module init python` dispatch additionally requires the engine to +> thread `[modules.python-sdk.settings]` into the SDK at init time. + ## Configure an existing module Read the current configuration. Settings that are not explicitly written to diff --git a/python-sdk.dang b/python-sdk.dang index 1e54f18..f64bc9b 100644 --- a/python-sdk.dang +++ b/python-sdk.dang @@ -12,6 +12,48 @@ type PythonSdk { """ pub targetRuntime: String! = "python" + """ + Workspace default Python version applied to modules this SDK creates. + + Surfaced as the `default-python-version` setting of + `dagger settings python-sdk`. Empty leaves the template's Python version + untouched. `initModule` falls back to this when `--python-version` is omitted. + """ + pub defaultPythonVersion: String! + + """ + Workspace default for building modules with uv (true) instead of pip. + + Surfaced as the `default-use-uv` setting of `dagger settings python-sdk`. + `initModule` falls back to this when `--use-uv` is omitted. + """ + pub defaultUseUv: Boolean! + + """ + Workspace default base container image for modules this SDK creates. + + Surfaced as the `default-base-image` setting of `dagger settings python-sdk`. + Empty writes no base image override. `initModule` falls back to this when + `--base-image` is omitted. + """ + pub defaultBaseImage: String! + + """ + Construct the SDK with its workspace-scoped settings. + + The engine populates these constructor arguments from + `[modules.python-sdk.settings]`, and `dagger settings python-sdk` surfaces + them as typed settings (camelCase maps to kebab-case on the CLI: + default-python-version, default-use-uv, default-base-image). They act as + defaults that `initModule` applies when the matching per-init flag is omitted. + """ + new(defaultPythonVersion: String! = "", defaultUseUv: Boolean! = true, defaultBaseImage: String! = "") { + self.defaultPythonVersion = defaultPythonVersion + self.defaultUseUv = defaultUseUv + self.defaultBaseImage = defaultBaseImage + self + } + """ Return every legacy dagger.json Dagger module whose sdk.source is "python". @@ -118,10 +160,12 @@ type PythonSdk { default uses this module's minimal template. Pass `pythonVersion`, `useUv`, or `baseImage` to customize the generated - pyproject.toml. Defaults leave the template untouched: the template's Python + pyproject.toml. When one is omitted, the matching workspace setting + (`default-python-version`, `default-use-uv`, `default-base-image`) applies. If + that is also unset the template is left 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! = "", pythonVersion: String! = "", useUv: Boolean = null, baseImage: String! = ""): Changeset! { let rawPath = path.trimPrefix("./").trimPrefix("/") let modPath = if (rawPath == "" or rawPath == ".") { @@ -137,7 +181,12 @@ type PythonSdk { if (currentModule.source.exists("templates/" + selectedTemplate) == false) { raise "unknown init template: " + template } else { - let templateSource = configuredTemplate(renderedTemplate(name, selectedTemplate), pythonVersion, useUv, baseImage) + # Per-init flags win; otherwise fall back to the workspace-scoped defaults. + let effectivePythonVersion = if (pythonVersion == "") { defaultPythonVersion } else { pythonVersion } + let effectiveUseUv = if (useUv != null) { useUv } else { defaultUseUv } + let effectiveBaseImage = if (baseImage == "") { defaultBaseImage } else { baseImage } + + let templateSource = configuredTemplate(renderedTemplate(name, selectedTemplate), effectivePythonVersion, effectiveUseUv, effectiveBaseImage) polyfill.workspace(ws).fork .withDirectory(modPath, templateSource)