On this page

Aether config two doors๏ƒ

๐ŸŸ  Advanced ยท Lesson 07

TWO DOORS TO ONE CONFIG, AND THE LADDER BEHIND THEM.

Aether hands you two ways to build its root configuration: aether.create_configuration() -> AetherConfiguration aether.create_configuration_builder() -> AetherConfigurationBuilder

They are NOT redundant, and they do NOT end in the same place by accident - they end in the same place ON PURPOSE:

config.with_(...).finalize() -> FROZEN builder.with_(...).build() -> FROZEN

finalize() freezes and returns THE SAME OBJECT. build() freezes and HANDS OVER OWNERSHIP - it is one-shot and consuming, so the builder is spent afterwards. Reach for the builder when construction happens somewhere that should not keep a handle on the result.

THE PART EVERYONE GETS WRONG: FROZEN IS NOT READY.

Freezing and activating are TWO SEPARATE STATE BITS, and this is the single most confusing thing about melder's configuration model until you see it written down:

frozen - "no more edits" (finalize / build / freeze) activated - "in force" (activate)

A config can sit frozen for a long time before anything turns it on. So the ladder is three rungs, in this order, and the order is a RULE rather than a convention:

  1. finalize() or build() -> frozen 2. configuration.activate() -> activated 3. aether.activate(config) -> Aether itself comes up

Skipping rung 2 raises. Aether's own contract says so in capitals: "THE CONFIGURATION MUST BE ACTIVATED BEFORE AETHER CAN BE." The two failure modes stay distinct on purpose - "not configured" and "configuration not activated" are different sentences because they are different bugs.

RUNG 3 IS NOT PERFORMED HERE, DELIBERATELY. Aether is a process-wide singleton, and aether.activate(cfg) installs the config BEFORE it checks the activated bit - so even the refusing call mutates the world. A lesson that shares an interpreter with every other lesson must not do that. The refusal is pinned in pytest_examples/test_advanced_probes.py, where the reset fixture owns a clean singleton.

Before you run๏ƒ

Use the Advanced guide for prerequisite concepts. Run from a checkout with Melder installed and Python 3.14 free-threading selected. The collection download includes the level's local helper modules.

Run the saved script๏ƒ

python UX_and_AIX_experiences/03_advanced/07_aether_config_two_doors.py
py -3.14t UX_and_AIX_experiences/03_advanced/07_aether_config_two_doors.py

Download this collection ยท Source on GitHub

Public surface๏ƒ

md.Aether().create_configuration, create_configuration_builder, md.AetherConfiguration, md.AetherConfigurationBuilder, the freeze/activate split

Code๏ƒ

  1"""
  2TIER: advanced (07)
  3GOAL: TWO DOORS TO ONE CONFIG, AND THE LADDER BEHIND THEM.
  4
  5      Aether hands you two ways to build its root configuration:
  6        aether.create_configuration() -> AetherConfiguration
  7        aether.create_configuration_builder() -> AetherConfigurationBuilder
  8
  9      They are NOT redundant, and they do NOT end in the same place by
 10      accident - they end in the same place ON PURPOSE:
 11
 12        config.with_*(...).finalize() -> FROZEN
 13        builder.with_*(...).build() -> FROZEN
 14
 15      finalize() freezes and returns THE SAME OBJECT. build() freezes and
 16      HANDS OVER OWNERSHIP - it is one-shot and consuming, so the builder
 17      is spent afterwards. Reach for the builder when construction happens
 18      somewhere that should not keep a handle on the result.
 19
 20      THE PART EVERYONE GETS WRONG: FROZEN IS NOT READY.
 21
 22      Freezing and activating are TWO SEPARATE STATE BITS, and this is the
 23      single most confusing thing about melder's configuration model until
 24      you see it written down:
 25
 26        frozen     - "no more edits" (finalize / build / freeze)
 27        activated - "in force" (activate)
 28
 29      A config can sit frozen for a long time before anything turns it on.
 30      So the ladder is three rungs, in this order, and the order is a RULE
 31      rather than a convention:
 32
 33        1. finalize() or build() -> frozen
 34        2. configuration.activate() -> activated
 35        3. aether.activate(config) -> Aether itself comes up
 36
 37      Skipping rung 2 raises. Aether's own contract says so in capitals:
 38      "THE CONFIGURATION MUST BE ACTIVATED BEFORE AETHER CAN BE." The two
 39      failure modes stay distinct on purpose - "not configured" and
 40      "configuration not activated" are different sentences because they
 41      are different bugs.
 42
 43      RUNG 3 IS NOT PERFORMED HERE, DELIBERATELY. Aether is a process-wide
 44      singleton, and `aether.activate(cfg)` installs the config BEFORE it
 45      checks the activated bit - so even the refusing call mutates the
 46      world. A lesson that shares an interpreter with every other lesson
 47      must not do that. The refusal is pinned in
 48      pytest_examples/test_advanced_probes.py, where the reset fixture
 49      owns a clean singleton.
 50SURFACE EXERCISED: md.Aether().create_configuration,
 51                   create_configuration_builder, md.AetherConfiguration,
 52                   md.AetherConfigurationBuilder, the freeze/activate split
 53VERIFY: rides the owner's 3.14t run; asserts are the contract.
 54"""
 55import melder as md
 56
 57
 58def main() -> None:
 59    aether = md.Aether()
 60
 61    # DOOR 1 - the configuration object, driven fluently.
 62    # create_configuration() is a FACTORY: it hands back a fresh, UNATTACHED
 63    # config. Making one does not install it anywhere, which is exactly why
 64    # this lesson can build several without disturbing the running world.
 65    config = aether.create_configuration()
 66    assert isinstance(config, md.AetherConfiguration)
 67    assert config.frozen is False
 68    assert config.activated is False
 69    print("door 1: fresh config - frozen:", config.frozen,
 70          "activated:", config.activated)
 71
 72    # finalize() is the fluent terminator: freeze, return THIS object.
 73    sealed = config.with_defaults().finalize()
 74    assert sealed is config, "finalize must not clone the configuration"
 75    assert config.frozen is True
 76    print("after finalize  - frozen:", config.frozen,
 77          "activated:", config.activated)
 78
 79    # THE POINT OF THE LESSON. Two bits, and freezing set only one of them.
 80    assert config.frozen is True and config.activated is False
 81    print("frozen is NOT ready - activated is still False")
 82
 83    # Rung 2. Now it is in force.
 84    config.activate()
 85    assert config.activated is True
 86    print("after activate  - frozen:", config.frozen,
 87          "activated:", config.activated)
 88
 89    # DOOR 2 - the builder. Same destination, different ownership story.
 90    builder = aether.create_configuration_builder()
 91    assert isinstance(builder, md.AetherConfigurationBuilder)
 92    built = builder.with_defaults().build()
 93    assert isinstance(built, md.AetherConfiguration)
 94
 95    # build() lands on EXACTLY the rung finalize() did: frozen, not
 96    # activated. The builder is spent; the config belongs to the caller now.
 97    assert built.frozen is True
 98    assert built.activated is False
 99    print("door 2: built config - frozen:", built.frozen,
100          "activated:", built.activated)
101    print("both doors land on the same rung: frozen, not yet in force")
102
103    # The two configs are separate objects with separate bits - proof that
104    # neither door reaches into shared state to do its work.
105    assert built is not config
106    assert config.activated is True and built.activated is False
107    print("independent objects, independent bits")
108
109    print()
110    print("finalize/build = frozen. activate = in force. two bits, not one.")
111    print("the config activates BEFORE aether does - that ordering is a rule")
112
113
114if __name__ == "__main__":
115    main()

Check the outcome๏ƒ

The script contains its own assertions or demonstrated refusal paths. Run it to evaluate those checks against your installed version. The code above is taken directly from the saved file; no run output is invented here.

More advanced examples ยท Level guide

API contracts๏ƒ