Configuration files

A .lclcfg file holds trusted LCL definitions outside Python. Loading preserves the defining file and source positions, then creates the same Module and Frame values used by Python applications. Ordinary results stay lazy; loading evaluates only the definitions needed to choose dynamic file targets.

Start file introduction targets with f"{__dir__}/...", including in child files. For example, using f"{__dir__}/shared.lclcfg" names a sibling, and import f"{__dir__}/../pricing.lclcfg" as pricing names a file in the parent directory. This convention makes each defining file's path base explicit.

Parse text that another system owns

Version 1 is the default. An optional version declaration must be the first meaningful declaration. Definitions use a colon, and a backslash is the only physical-line continuation marker. Opening a bracket alone does not continue the declaration.

pricing.lclcfg:

__LCL_VERSION__: 1
# Expressions may refer to inputs supplied later.
subtotal: unit_price * quantity
total: subtotal + \
  shipping

Read the text and parse it without asking the parser to access a file:

from pathlib import Path

from lclang.config import parse_config

text = Path("pricing.lclcfg").read_text(encoding="utf-8")
document = parse_config(text, source_name="generated pricing")
assert str(document.origin.name) == "generated pricing"
print(document.version, len(document.declarations))

This prints version 1 and two definitions. Metadata and comments do not contribute definitions, and continuation keeps total as one expression.

1 2

Put definitions from one scope on consecutive rows. Separate scopes or functional groups with a blank line. Use an inline comment to explain an individual value and a standalone comment to name a group. Qualified leaves infer their prefixes.

# scope: service endpoint
service.host: "api.example.com" # Deployment DNS name
service.port: 8443 # TLS listener

# Retry policy
retry.count: 3 # Maximum attempts
retry.delay: 0.5 # Seconds between attempts

FRAME_PROXY can declare a prefix explicitly, but ordinary files can rely on inference. See configuration composition for cases where empty imported namespaces matter.

Expand shared files in source order

using inserts another file's definitions at the declaration position. Start its target with f"{__dir__}/..." to identify that file's directory explicitly. Later same-name declarations replace values while retaining every occurrence in Config.history.

shared.lclcfg:

discount_rate: 0.05 # Base discount
shipping: 8 # Flat shipping charge
currency: "USD"

application.lclcfg:

using f"{__dir__}/shared.lclcfg"
discount_rate: 0.10 # Application discount

subtotal: unit_price * quantity
total: subtotal * (1 - discount_rate) + shipping
label: f"{currency} {total:.2f}"

Run this Python program beside the files:

import asyncio

from lclang.config import load_config


async def main() -> None:
    config = await load_config("application.lclcfg")
    assert len(config.history["discount_rate"]) == 2
    async with config.to_frame(preset={"unit_price": 25, "quantity": 4}) as frame:
        print(await frame.get("label"))
        assert await frame.get("total") == 98.0


asyncio.run(main())

The subtotal is 100. The final discount rate is 0.10, so the discounted subtotal is 90; shipping brings it to 98. The program prints:

USD 98.00

The second get reuses the cached total. config.to_frame(preset=...) creates a fresh caller-owned Frame synchronously; async with closes its owned state. Configuration definitions normally take precedence over preset values.

Allow an absent local file

Use using? when a deployment may omit a local override file. The question mark must immediately follow the keyword. Only a directly missing target is skipped. An existing file's read, decoding, syntax, nested dependency, or cycle error continues to fail loading.

The local override file is absent in this example.

application.lclcfg:

discount_rate: 0.05
using? f"{__dir__}/local.lclcfg"
total: 100 * (1 - discount_rate)
import asyncio

from lclang.config import load_config


async def main() -> None:
    config = await load_config("application.lclcfg")
    assert len(config.history["discount_rate"]) == 1
    async with config.to_frame() as frame:
        print(await frame.get("total"))


asyncio.run(main())

The absent file contributes no definition or history. The base rate remains 0.05, and the program prints:

95.0

If local.lclcfg contains discount_rate: 0.10, it contributes a second history entry and changes the total to 90.0. A reused loader retries absent targets; successful files keep their cached snapshots.

Choose a file from preceding values

A dynamic target must be an f-string producing a nonempty .lclcfg path. It reads definitions available before that introduction. A later declaration does not change a target already selected.

profiles/production.lclcfg:

service_name: "production"

profiles/development.lclcfg:

service_name: "development"

application.lclcfg:

profile: "production"
using f"{__dir__}/profiles/{profile}.lclcfg"
profile: "development"
import asyncio

from lclang.config import load_config


async def main() -> None:
    config = await load_config("application.lclcfg")
    async with config.to_frame() as frame:
        print(await frame.get("service_name"), await frame.get("profile"))


asyncio.run(main())

Loading selects the production file. Final runtime winners still include the later development profile, so the two values differ:

production development

Use explicit environment overrides

env.NAME reads the live process environment, and ?? provides an absent-value fallback. Scoped loading overrides can select another file without changing the process environment.

application.lclcfg:

using f"{__dir__}/{env.LCLANG_TUTORIAL_PROFILE ?? 'local'}.lclcfg"

local.lclcfg:

profile_name: "local"

blue.lclcfg:

profile_name: "blue"

green.lclcfg:

profile_name: "green"
import asyncio
import os
from unittest.mock import patch

from lclang.config import load_config


async def main() -> None:
    with patch.dict(os.environ, {}, clear=False):
        os.environ.pop("LCLANG_TUTORIAL_PROFILE", None)
        for overrides in (None, None, {"env.LCLANG_TUTORIAL_PROFILE": "green"}):
            config = await load_config("application.lclcfg", overrides=overrides)
            async with config.to_frame() as frame:
                print(await frame.get("profile_name"))
            os.environ["LCLANG_TUTORIAL_PROFILE"] = "blue"
        assert os.environ["LCLANG_TUTORIAL_PROFILE"] == "blue"


asyncio.run(main())

The fallback chooses local, the process value chooses blue, and the explicit loading override chooses green. Loading overrides do not become final runtime configuration values. The program prints:

local
blue
green

Reuse construction policy with fresh caches

Use Config.frame_factory() when several independent runs share one loaded configuration. Each created Frame receives its own result and failure snapshots.

policy.lclcfg:

total: unit_price * quantity
origin: __file__
import asyncio
from pathlib import Path

from lclang.config import load_config
from lclang.lang.runtime import Preset


async def main() -> None:
    config = await load_config("policy.lclcfg")
    factory = config.frame_factory(preset=Preset("pricing inputs", {"unit_price": 12, "quantity": 4}))
    async with factory.create() as first, factory.create(values={"quantity": 5}) as second:
        print(await first.get("total"), await second.get("total"))
        assert Path(str(await first.get("origin"))).name == "policy.lclcfg"


asyncio.run(main())

The first run uses four units and the second uses five. File magic retains the defining physical file, and separate Frames prevent shared result caches:

48 60

Files decode as UTF-8, with a BOM accepted at byte zero. Loading performs no globbing or network access. Use ConfigLoadLimits and an allowed-root resolver when the application needs bounded loading. Treat configuration and supplied Python objects as trusted code.

Previous: The LCL language | Next: Async Python integration | Return to the series introduction