Constructors
Every Dagger module has a constructor. The default one is generated automatically and has no arguments.
It's possible to write a custom constructor. The mechanism to do this is SDK-specific.
This is a simple way to accept module-wide configuration, or just to set a few attributes without having to create setter functions for them.
Simple constructor
The default constructor for a module can be overridden by registering a custom constructor. Its parameters are available as flags in the dagger call
command directly.
Dagger modules have only one constructors. Constructors of custom types are not registered; they are constructed by the function that chains them.
Here is an example module with a custom constructor:
- Go
- Python
- TypeScript
// A Dagger module for saying hello world!
package main
import (
"fmt"
)
func New(
// +optional
// +default="Hello"
greeting string,
// +optional
// +default="World"
name string,
) *MyModule {
return &MyModule{
Greeting: greeting,
Name: name,
}
}
type MyModule struct {
Greeting string
Name string
}
func (hello *MyModule) Message() string {
return fmt.Sprintf("%s, %s!", hello.Greeting, hello.Name)
}
from dagger import function, object_type
@object_type
class MyModule:
greeting: str = "Hello"
name: str = "World"
@function
def message(self) -> str:
return f"{self.greeting}, {self.name}!"
In the Python SDK, the @dagger.object_type
decorator wraps @dataclasses.dataclass
, which means that an __init__()
method is automatically generated, with parameters that match the declared class attributes.
The code listing above is an example of an object that has typed attributes.
import { object, func } from "@dagger.io/dagger"
@object()
class MyModule {
greeting: string
name: string
constructor(greeting = "Hello", name = "World") {
this.greeting = greeting
this.name = name
}
@func()
message(): string {
return `${this.greeting} ${this.name}`
}
}
Here is an example call for this Dagger Function:
dagger call --name=Foo message
The result will be:
Hello, Foo!
If you plan to use constructor fields in other module functions, ensure that they are declared as public (in Go and TypeScript). This is because Dagger stores fields using serialization and private fields are omitted during the serialization process. As a result, if a field is not declared as public, calling methods that use it will produce unexpected results.
Default values for complex types
Constructors can be passed both simple and complex types (such as Container
, Directory
, Service
etc.) as arguments. Default values can be assigned in both cases.
Here is an example of a Dagger module with a default constructor argument of type Container
:
- Go
- Python
- TypeScript
package main
import (
"context"
"main/internal/dagger"
)
func New(
// +optional
ctr *dagger.Container,
) *MyModule {
if ctr == nil {
ctr = dag.Container().From("alpine:3.14.0")
}
return &MyModule{
Ctr: *ctr,
}
}
type MyModule struct {
Ctr dagger.Container
}
func (m *MyModule) Version(ctx context.Context) (string, error) {
c := m.Ctr
return c.
WithExec([]string{"/bin/sh", "-c", "cat /etc/os-release | grep VERSION_ID"}).
Stdout(ctx)
}
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
ctr: dagger.Container = dag.container().from_("alpine:3.14.0")
@function
async def version(self) -> str:
return await self.ctr.with_exec(
["/bin/sh", "-c", "cat /etc/os-release | grep VERSION_ID"]
).stdout()
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
ctr: Container
constructor(ctr?: Container) {
this.ctr = ctr ?? dag.container().from("alpine:3.14.0")
}
@func()
async version(): Promise<string> {
return await this.ctr
.withExec(["/bin/sh", "-c", "cat /etc/os-release | grep VERSION_ID"])
.stdout()
}
}
This default value can also be assigned directly in the field:
import { dag, Container, object, func } from "@dagger.io/dagger"
@object()
class MyModule {
ctr: Container = dag.container().from("alpine:3.14.0")
constructor(ctr?: Container) {
this.ctr = ctr ?? this.ctr
}
@func()
async version(): Promise<string> {
return await this.ctr
.withExec(["/bin/sh", "-c", "cat /etc/os-release | grep VERSION_ID"])
.stdout()
}
}
When assigning default values to complex types in TypeScript, it is necessary to use the ??
notation for this assignment. It is not possible to use the classic TypeScript notation for default arguments because the argument in this case is not a TypeScript primitive.
It is necessary to explicitly declare the type even when a default value is assigned, so that the Dagger SDK can extend the GraphQL schema correctly.
Here is an example call for this Dagger Function:
dagger call version
The result will be:
VERSION_ID=3.14.0
Exclude argument in constructor
The information in this section is only applicable to the Python SDK.
Same as any data class, attributes can be excluded from the
generated __init__()
function, using dataclasses.field(init=False):
"""A simple hello world example, using a constructor."""
import dataclasses
from typing import Annotated
from dagger import Doc, function, object_type
@object_type
class MyModule:
"""Functions for greeting the world"""
greeting: str = dataclasses.field(default="Hello", init=False)
name: Annotated[str, Doc("Who to greet")] = "World"
@function
def message(self) -> str:
"""Return the greeting message"""
return f"{self.greeting}, {self.name}!"
In this case, only the name
flag was added and is visible in the output:
FUNCTIONS
message Return the greeting message
ARGUMENTS
--name string Who to greet (default "World")
Constructor-only arguments
The information in this section is only applicable to the Python SDK.
The opposite is also possible. To define an argument that only exists in the constructor, but not as a class attribute, define it as an init-only variable:
"""An example module controlling constructor parameters."""
import dataclasses
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
base: dagger.Container = dataclasses.field(init=False)
variant: dataclasses.InitVar[str] = "alpine"
def __post_init__(self, variant: str):
self.base = dag.container().from_(f"python:{variant}")
@function
def container(self) -> dagger.Container:
return self.base
Complex or mutable defaults
The information in this section is only applicable to the Python SDK.
For default values that are more complex, dynamic or just mutable, use a factory function without arguments in dataclasses.field(default_factory=...):
"""An example module using default factory functions."""
import dataclasses
import dagger
from dagger import dag, function, object_type
@object_type
class MyModule:
base: dagger.Container = dataclasses.field(
default_factory=lambda: dag.container().from_("python:alpine"),
)
packages: list[str] = dataclasses.field(default_factory=list)
@function
def container(self) -> dagger.Container:
return self.base.with_exec(["apk", "add", "git", *self.packages])
Asynchronous constructor
The information in this section is only applicable to the Python SDK.
If a constructor argument needs an asynchronous call to set the default value, it's
possible to replace the default constructor function from __init__()
to
a factory class method named create
, as in the following code listing:
This factory class method must be named create
.
"""An example module using a factory constructor"""
from typing import Annotated
from dagger import Doc, dag, function, object_type
@object_type
class MyModule:
"""Functions for testing a project"""
parallelize: int
@classmethod
async def create(
cls,
parallelize: Annotated[
int | None, Doc("Number of parallel processes to run")
] = None,
):
if parallelize is None:
parallelize = int(
await dag.container().from_("alpine").with_exec(["nproc"]).stdout()
)
return cls(parallelize=parallelize)
@function
def debug(self) -> str:
"""Check the number of parallel processes"""
return f"Number of parallel processes: {self.parallelize}"