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
command directly.
Dagger modules have only one constructor. 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
- PHP
- Java
// A Dagger module for saying hello world!
package main
import (
"fmt"
)
func New(
// +default="Hello"
greeting string,
// +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}!"
Dagger @object_type
classes are Python data classes that become exposed to the Dagger API. This means that you can adjust the generation of the constructor using standard data class features, such as fields and post-init processing. Refer to the official dataclasses
documentation to learn more.
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}`
}
}
<?php
declare(strict_types=1);
namespace DaggerModule;
use Dagger\Attribute\{DaggerObject, DaggerFunction};
#[DaggerObject]
class MyModule
{
#[DaggerFunction]
public function __construct(
private readonly string $greeting = 'Hello',
private readonly string $name = 'World',
) {
}
#[DaggerFunction]
public function message(): string
{
return "{$this->greeting} {$this->message}";
}
}
In the PHP SDK the constructor must be the magic method __construct
.
As with any method, only public methods with the #[DaggerFunction]
attribute will be registered with Dagger.
package io.dagger.modules.mymodule;
import static io.dagger.client.Dagger.dag;
import io.dagger.client.Container;
import io.dagger.client.DaggerQueryException;
import io.dagger.client.Directory;
import io.dagger.module.annotation.Function;
import io.dagger.module.annotation.Ignore;
import io.dagger.module.annotation.Object;
import java.util.concurrent.ExecutionException;
@Object
public class MyModule {
@Function
public Container foo(@Ignore({"*", "!src/**/*.java", "!pom.xml"}) Directory source)
throws ExecutionException, DaggerQueryException, InterruptedException {
return dag().container().from("alpine:latest").withDirectory("/src", source).sync();
}
}
In the Java SDK, the constructor must be public. A public empty constructor is also required in order to create the object from the serialized data.
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c '. --name=Foo | message'
. --name=Foo | message
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.
Explicitly declare the type even when it can be inferred, so that the Dagger SDK can extend the GraphQL schema correctly.
Here is an example of a Dagger module with a default constructor argument of type Container
:
- Go
- Python
- TypeScript
- PHP
- Java
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)
}
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c version
version
dagger call version
The result will be:
VERSION_ID=3.14.0
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()
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c version
version
dagger call version
The result will be:
VERSION_ID=3.14.0
Dagger objects are Python data classes
Since @object_type
classes are Python data classes, you can fine tune the generation of the constructor in different ways.
Here is a more complex example:
from typing import Annotated
import dagger
from dagger import (
DefaultPath,
Doc,
dag,
field,
object_type,
)
@object_type
class Workspace:
source: Annotated[
dagger.Directory,
Doc("The context for the workspace"),
DefaultPath("/"),
]
token: Annotated[
dagger.Secret | None,
Doc("GitHub API token"),
] = None
container: Annotated[
dagger.Container,
Doc("The container for the workspace"),
] = field(init=False)
def __post_init__(self):
self.container = (
dag.container()
.from_("python:3.11")
.with_workdir("/app")
.with_directory("/app", self.source)
.with_mounted_cache("/root/.cache/pip", dag.cache_volume("python-pip"))
.with_exec(["pip", "install", "-r", "requirements.txt"])
)
A few important notes:
- Data class fields become constructor arguments by default unless specified as
field(init=False)
. So, even though thecontainer
field is not added as a constructor argument it's still a required field that must be initialized either with a default value or in post-init processing. - The
__post_init__
function is called at the end of the auto-generated constructor which can be used, for example, to initialize field values that depend on other field values. - Setting a default value like
None
in an attribute is the same as settingdataclasses.field(default=None)
.dataclasses.field()
is only required to specify other non-default values, likeinit
or to set the default using a factory function. - The
dagger.field
descriptor wrapsdataclasses.field
, adding two important differences:dagger.field()
is used to expose the attribute as a Dagger Function in the API (a simple getter), but it also becomes a part of the object's state.- Not all arguments from
dataclasses.field()
can be set withdagger.field()
which sets opinionated defaults, but you can specify both simple values and factory functions via the samedefault
argument while withdataclasses.field()
you need to use separatedefault
anddefault_factory
arguments.
Factory function
For default values that are more complex, dynamic or just mutable, use a factory function without arguments in
dataclasses.field(default_factory=...)
or dagger.field(default=...)
:
"""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 usage
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}"
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.
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c version
version
dagger call version
The result will be:
VERSION_ID=3.14.0
<?php
declare(strict_types=1);
namespace DaggerModule;
use Dagger\Attribute\{DaggerObject, DaggerFunction};
use Dagger\Container;
use function Dagger\dag;
#[DaggerObject]
class MyModule
{
private Container $ctr;
#[DaggerFunction]
public function __construct(
?Container $ctr,
) {
$this->ctr = $ctr ?? dag()->container()->from('alpine:3.14.0');
}
#[DaggerFunction]
public function version(): string
{
return $this->ctr
->withExec(['/bin/sh', '-c', 'cat /etc/os-release | grep VERSION_ID'])
->stdout();
}
}
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c version
version
dagger call version
The result will be:
VERSION_ID=3.14.0
package io.dagger.modules.mymodule;
import static io.dagger.client.Dagger.dag;
import io.dagger.client.Container;
import io.dagger.client.DaggerQueryException;
import io.dagger.module.annotation.Function;
import io.dagger.module.annotation.Object;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@Object
public class MyModule {
public Container ctr;
public MyModule() {}
public MyModule(Optional<Container> ctr) {
this.ctr = ctr.orElseGet(() -> dag().container().from("alpine:3.14.0"));
}
@Function
public String version() throws ExecutionException, DaggerQueryException, InterruptedException {
return ctr.withExec(List.of("/bin/sh", "-c", "cat /etc/os-release | grep VERSION_ID")).stdout();
}
}
Here is an example call for this Dagger Function:
- System shell
- Dagger Shell
- Dagger CLI
dagger -c version
version
dagger call version
The result will be:
VERSION_ID=3.14.0