Skip to content

Field Groups#

Ensure related fields are always overridden together:

"""Field groups — ensure related fields change together."""

from dataclasses import dataclass
from pathlib import Path

import dature

SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    debug: bool
    user: str
    password: str


config = dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_field_groups_defaults.yaml"),
    dature.Yaml12Source(file=SHARED_DIR / "common_field_groups_overrides.yaml"),
    schema=Config,
    field_groups=((dature.F[Config].host, dature.F[Config].port),),
)

assert config.host == "production.example.com"
assert config.port == 8080
assert config.user == "admin"
assert config.password == "secret"
host: "localhost"
port: 3000
debug: false
user: "admin"
password: "secret"
host: "production.example.com"
port: 8080

If overrides.yaml changes host and port together, the group constraint is satisfied. If a source partially overrides a group, FieldGroupError is raised:

"""Field groups — error on partial override (basic)."""

from dataclasses import dataclass
from pathlib import Path

import dature

SOURCES_DIR = Path(__file__).parent / "sources"
SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    debug: bool
    user: str
    password: str


dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_field_groups_defaults.yaml"),
    dature.Yaml12Source(
        file=SOURCES_DIR / "field_groups_partial_overrides.yaml",
    ),
    schema=Config,
    field_groups=(
        (dature.F[Config].host, dature.F[Config].port),
        (dature.F[Config].user, dature.F[Config].password),
    ),
)
host: "localhost"
port: 3000
debug: false
user: "admin"
password: "secret"
host: "production.example.com"
  | dature.errors.exceptions.FieldGroupError: Config field group errors (1)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.FieldGroupViolationError:   Field group (host, port) partially overridden in source 1
    |     changed:   host (from source yaml1.2 '{SOURCES_DIR}field_groups_partial_overrides.yaml')
    |     unchanged: port (from source yaml1.2 '{SHARED_DIR}common_field_groups_defaults.yaml')
    +------------------------------------

Nested Dataclass Expansion#

Passing a dataclass field expands it into all its leaf fields:

"""Field groups — error on partial override with nested dataclass expansion."""

from dataclasses import dataclass
from pathlib import Path

import dature

SOURCES_DIR = Path(__file__).parent / "sources"


@dataclass
class Database:
    host: str
    port: int


@dataclass
class Config:
    host: str
    port: int
    database: Database


# (dature.F[Config].database, dature.F[Config].port)
# expands to (database.host, database.port, port)
dature.load(
    dature.Yaml12Source(file=SOURCES_DIR / "field_groups_nested_defaults.yaml"),
    dature.Yaml12Source(
        file=SOURCES_DIR
        / "advanced_field_groups_expansion_error_overrides.yaml",
    ),
    schema=Config,
    field_groups=((dature.F[Config].database, dature.F[Config].port),),
)
host: "localhost"
port: 3000
database:
  host: "db.local"
  port: 5432
host: "production.example.com"
port: 8080
database:
  host: "db.prod"
  | dature.errors.exceptions.FieldGroupError: Config field group errors (1)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.FieldGroupViolationError:   Field group (database.host, database.port, port) partially overridden in source 1
    |     changed:   database.host (from source yaml1.2 '{SOURCES_DIR}advanced_field_groups_expansion_error_overrides.yaml'), port (from source yaml1.2 '{SOURCES_DIR}advanced_field_groups_expansion_error_overrides.yaml')
    |     unchanged: database.port (from source yaml1.2 '{SOURCES_DIR}field_groups_nested_defaults.yaml')
    +------------------------------------

Multiple Groups#

If a source partially overrides multiple groups, all violations are reported:

"""Multiple field groups — error when both groups partially overridden."""

from dataclasses import dataclass
from pathlib import Path

import dature

SOURCES_DIR = Path(__file__).parent / "sources"
SHARED_DIR = Path(__file__).parents[2] / "shared"


@dataclass
class Config:
    host: str
    port: int
    debug: bool
    user: str
    password: str


OVERRIDES_FILE = (
    SOURCES_DIR / "advanced_field_groups_multiple_error_overrides.yaml"
)

dature.load(
    dature.Yaml12Source(file=SHARED_DIR / "common_field_groups_defaults.yaml"),
    dature.Yaml12Source(file=OVERRIDES_FILE),
    schema=Config,
    field_groups=(
        (dature.F[Config].host, dature.F[Config].port),
        (dature.F[Config].user, dature.F[Config].password),
    ),
)
host: "localhost"
port: 3000
debug: false
user: "admin"
password: "secret"
host: "production.example.com"
user: "deploy"
  | dature.errors.exceptions.FieldGroupError: Config field group errors (2)
  +-+---------------- 1 ----------------
    | dature.errors.exceptions.FieldGroupViolationError:   Field group (host, port) partially overridden in source 1
    |     changed:   host (from source yaml1.2 '{SOURCES_DIR}advanced_field_groups_multiple_error_overrides.yaml')
    |     unchanged: port (from source yaml1.2 '{SHARED_DIR}common_field_groups_defaults.yaml')
    +---------------- 2 ----------------
    | dature.errors.exceptions.FieldGroupViolationError:   Field group (user, password) partially overridden in source 1
    |     changed:   user (from source yaml1.2 '{SOURCES_DIR}advanced_field_groups_multiple_error_overrides.yaml')
    |     unchanged: password (from source yaml1.2 '{SHARED_DIR}common_field_groups_defaults.yaml')
    +------------------------------------

Field groups work with all merge strategies and can be combined with field_merges.