Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .dagger/lock
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[["version","1"]]
["","git.head",["https://github.com/dagger/sdk-sdk"],"d6ab6406586e6b3853b8936b2b3a96bba2554071","float"]
["","container.from",["docker.io/library/golang:1.25-alpine","linux/arm64"],"sha256:8d22e29d960bc50cd025d93d5b7c7d220b1ee9aa7a239b3c8f55a57e987e8d45","pin"]
["","git.head",["https://github.com/dagger/sdk-sdk"],"09da9576688ec4b495ae4a9443a4719da86ed25e","float"]
Empty file.
7 changes: 7 additions & 0 deletions .dagger/modules/e2e/fixtures/config/app/dagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "config-app",
"engineVersion": "latest",
"sdk": {
"source": "python"
}
}
12 changes: 12 additions & 0 deletions .dagger/modules/e2e/fixtures/config/app/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
name = "config-app"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = ["dagger-io"]

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

[tool.uv.sources]
dagger-io = { path = "sdk", editable = true }
10 changes: 10 additions & 0 deletions .dagger/modules/e2e/fixtures/config/app/src/config_app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import dagger
from dagger import object_type


@object_type
class ConfigApp:
source: dagger.Directory

def __init__(self, ws: dagger.Workspace):
self.source = ws.directory("/")
7 changes: 7 additions & 0 deletions .dagger/modules/e2e/fixtures/config/configured/dagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "config-configured",
"engineVersion": "latest",
"sdk": {
"source": "python"
}
}
16 changes: 16 additions & 0 deletions .dagger/modules/e2e/fixtures/config/configured/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[project]
name = "config-configured"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["dagger-io"]

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

[tool.dagger]
use-uv = false
base-image = "python:3.12-slim"

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


@object_type
class ConfigConfigured:
source: dagger.Directory

def __init__(self, ws: dagger.Workspace):
self.source = ws.directory("/")
72 changes: 72 additions & 0 deletions .dagger/modules/e2e/main.dang
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type E2e {
let skipModulePath: String! = fixtureRoot + "/skip/app"
let generateModulePath: String! = fixtureRoot + "/generate/app"
let depsModulePath: String! = fixtureRoot + "/deps/app"
let configModulePath: String! = fixtureRoot + "/config/app"
let configuredModulePath: String! = fixtureRoot + "/config/configured"

let generatedMarkerPath: String! = "sdk/src/dagger/client/gen.py"
let generatedMarkerContents: String! = "Code generated by dagger."
Expand Down Expand Up @@ -222,4 +224,74 @@ type E2e {

null
}

"""
config.get should reflect pyproject.toml and report unset values as null
rather than guessing, and config.set should edit only pyproject.toml.
"""
pub configCheck(ws: Workspace!): Void @check {
let app = pythonSdk.mod(ws, path: configModulePath).config
let configured = pythonSdk.mod(ws, path: configuredModulePath).config

let appValues = app.get
assert(appValues.pythonVersion == "3.14", "default pythonVersion should read 3.14")
assert(appValues.useUv == null, "useUv should be reported as unset, not guessed")
assert(appValues.baseImage == null, "baseImage should be reported as unset")

let configuredValues = configured.get
assert(configuredValues.pythonVersion == "3.12", "configured pythonVersion should read 3.12")
assert(configuredValues.useUv == false, "configured useUv should read false")
assert(configuredValues.baseImage == "python:3.12-slim", "configured baseImage should read the override")

let pyproj = configModulePath + "/pyproject.toml"

let py = app.set(pythonVersion: "3.13")
assert(contains(py.modifiedPaths, pyproj), "set should modify pyproject.toml")
assert(py.modifiedPaths.length == 1, "set modified more than pyproject.toml")
assert(py.addedPaths.length == 0, "set should not add files")
assertContains(py.after.file(pyproj).contents, ">=3.13", "set did not write the new python version")
assertContains(py.after.file(pyproj).contents, "dagger-io", "set dropped unrelated keys")

let multi = app.set(pythonVersion: "3.13", useUv: false, baseImage: "python:3.13-slim")
assert(multi.modifiedPaths.length == 1, "multi-value set modified more than pyproject.toml")
assert(multi.addedPaths.length == 0, "multi-value set should not add files")
assertContains(multi.after.file(pyproj).contents, ">=3.13", "multi-value set did not write python version")
assertContains(multi.after.file(pyproj).contents, "use-uv = false", "multi-value set did not write use-uv")
assertContains(multi.after.file(pyproj).contents, "python:3.13-slim", "multi-value set did not write base image")

let configuredPyproj = configuredModulePath + "/pyproject.toml"
let partial = configured.set(pythonVersion: "3.13")
assertContains(partial.after.file(configuredPyproj).contents, ">=3.13", "partial set did not update python version")
assertContains(partial.after.file(configuredPyproj).contents, "use-uv = false", "omitting useUv should leave it untouched")
assertContains(partial.after.file(configuredPyproj).contents, "python:3.12-slim", "omitting baseImage should leave it untouched")

null
}

"""
Init flags should write configuration into the generated pyproject.toml, and
defaults should leave it unconfigured.
"""
pub initConfigCheck(ws: Workspace!): Void @check {
let configuredPath = outputRoot + "/init-configured"
let defaultPath = outputRoot + "/init-config-default"

let configured = pythonSdk.init(ws, name: "init-configured", path: configuredPath, pythonVersion: "3.13", useUv: false, baseImage: "python:3.13-slim")
let pyproj = configuredPath + "/pyproject.toml"
assertAdded(configured, pyproj)
assert(configured.modifiedPaths.length == 0, "configured init should not modify existing files")
assert(configured.removedPaths.length == 0, "configured init should not remove files")
assertContains(configured.layer.file(pyproj).contents, ">=3.13", "init --python-version not written")
assertContains(configured.layer.file(pyproj).contents, "use-uv = false", "init --use-uv=false not written")
assertContains(configured.layer.file(pyproj).contents, "python:3.13-slim", "init --base-image not written")
assertContains(configured.layer.file(pyproj).contents, "dagger-io", "init config dropped template data")

let default = pythonSdk.init(ws, name: "init-config-default", path: defaultPath)
let defaultPyproj = defaultPath + "/pyproject.toml"
assertContains(default.layer.file(defaultPyproj).contents, ">=3.14", "default init should keep the template python version")
assertNotContains(default.layer.file(defaultPyproj).contents, "use-uv", "default init should not write use-uv")
assertNotContains(default.layer.file(defaultPyproj).contents, "[tool.dagger]", "default init should not write a [tool.dagger] table")

null
}
}
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,47 @@ dagger call python-sdk init --name my-module --template legacy
`init` only seeds template files. Run `mod ... generate` to produce the
generated SDK.

### Configure a module at creation

`init` accepts configuration flags written into the module's `pyproject.toml`:

```sh
dagger call python-sdk init --name my-module \
--python-version 3.13 \
--use-uv=false \
--base-image python:3.13-slim
```

All three are optional. By default the template's Python version is used, uv is
enabled, and no base image override is written.

## Configure an existing module

Read the current configuration. Settings that are not explicitly written to
`pyproject.toml` are reported as `null` rather than guessed:

```sh
dagger call python-sdk mod --path my-module config get
```

Select a single value:

```sh
dagger call python-sdk mod --path my-module config get python-version
dagger call python-sdk mod --path my-module config get use-uv
dagger call python-sdk mod --path my-module config get base-image
```

Change one or more values at once (prints a diff to confirm before writing).
Each flag is optional; omitting one leaves that setting untouched:

```sh
dagger call python-sdk mod --path my-module config set \
--python-version 3.13 \
--use-uv=false \
--base-image python:3.13-slim
```

## Generate SDK files

For a single module:
Expand Down
4 changes: 2 additions & 2 deletions dagger.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "python-sdk",
"engineVersion": "v0.20.8",
"engineVersion": "v0.21.3",
"sdk": {
"source": "dang"
},
"dependencies": [
{
"name": "polyfill",
"source": "https://github.com/dagger/sdk-sdk/polyfill@main",
"pin": "d6ab6406586e6b3853b8936b2b3a96bba2554071"
"pin": "09da9576688ec4b495ae4a9443a4719da86ed25e"
}
]
}
5 changes: 5 additions & 0 deletions helpers/pyproject/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module pyproject

go 1.25.0

require github.com/pelletier/go-toml/v2 v2.3.1
2 changes: 2 additions & 0 deletions helpers/pyproject/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
89 changes: 89 additions & 0 deletions helpers/pyproject/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"fmt"
"os"
)

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

// run dispatches a subcommand. get-* commands print to stdout (no trailing
// newline). set-* commands edit the file in place.
//
// usage: pyproject <command> <file> [value]
func run(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: pyproject <command> <file> [value]")
}
cmd, path := args[0], args[1]

data, err := os.ReadFile(path)
if err != nil {
return err
}
doc, err := load(data)
if err != nil {
return err
}

switch cmd {
case "get-python-version":
fmt.Print(getPythonVersion(doc))
return nil
case "get-use-uv":
// Print nothing when unset so callers can tell an absent setting from
// an explicit value, instead of guessing the true default.
if v, ok := getUseUv(doc); ok {
fmt.Print(boolStr(v))
}
return nil
case "get-base-image":
fmt.Print(getBaseImage(doc))
return nil
case "set-python-version":
v, err := value(args)
if err != nil {
return err
}
setPythonVersion(doc, v)
case "set-use-uv":
v, err := value(args)
if err != nil {
return err
}
setUseUv(doc, v == "true")
case "set-base-image":
v, err := value(args)
if err != nil {
return err
}
setBaseImage(doc, v)
default:
return fmt.Errorf("unknown command: %s", cmd)
}

out, err := dump(doc)
if err != nil {
return err
}
return os.WriteFile(path, out, 0o644)
}

func value(args []string) (string, error) {
if len(args) < 3 {
return "", fmt.Errorf("%s requires a value", args[0])
}
return args[2], nil
}

func boolStr(b bool) string {
if b {
return "true"
}
return "false"
}
65 changes: 65 additions & 0 deletions helpers/pyproject/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

func writeTemp(t *testing.T, contents string) string {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, "pyproject.toml")
if err := os.WriteFile(p, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
return p
}

func TestRunSetThenGet(t *testing.T) {
p := writeTemp(t, sample)

if err := run([]string{"set-python-version", p, "3.13"}); err != nil {
t.Fatalf("set: %v", err)
}
data, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read: %v", err)
}
if !strings.Contains(string(data), ">=3.13") {
t.Errorf("file not edited:\n%s", data)
}
}

func TestRunGetDoesNotWrite(t *testing.T) {
p := writeTemp(t, sample)
before, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read: %v", err)
}
if err := run([]string{"get-python-version", p}); err != nil {
t.Fatalf("get: %v", err)
}
after, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read: %v", err)
}
if string(before) != string(after) {
t.Errorf("get-* must not modify the file:\nbefore:\n%s\nafter:\n%s", before, after)
}
}

func TestRunUnknownCommand(t *testing.T) {
p := writeTemp(t, sample)
if err := run([]string{"bogus", p}); err == nil {
t.Error("expected error for unknown command")
}
}

func TestRunRequiresValue(t *testing.T) {
p := writeTemp(t, sample)
if err := run([]string{"set-python-version", p}); err == nil {
t.Error("expected error when value is missing")
}
}
Loading