Defaults Decorator
The config_defaults decorator fills a function’s parameters from the configuration. This allows existing code to easily be converted to be configurable by the configuration system without changing how the function is called.
To use, add the decorator to a function and pass in a configuration path to use to override the default parameters for calls to that function. Each key underneath that configuration path is used to override the parameter with the same name in the function call.
For instance, if the decorator @config_defaults("client") is used on a function that takes port as a parameter, then its default value will be retrieved from client.port in the configuration.
The configuration to read from must be passed either to the decorator as the second parameter or passed to the function itself using a parameter named config.
Parameters passed to the function call override any defaults from the configuration, including when the value passed for a parameter is None. Parameters without keys in the configuration fall through to any defaults defined in the code.
For instance, given this decorated create_client function:
from strataconf import Config, config_defaults
config = Config()
config.load_config("configs/config.yaml")
@config_defaults("client", config)
def create_client(host: str, port: int, timeout: int = 30):
...and this configuration:
client:
host: api.example.com
port: 443the decorator resolves the arguments as follows:
create_client() # host="api.example.com", port=443, timeout=30
create_client(port=8443) # host="api.example.com", port=8443, timeout=30
create_client(port=None) # host="api.example.com", port=None, timeout=30In this example, host and port are read from the config, while timeout is not configured and so falls back to the function’s own default of 30. Passing port=8443 overrides the configured value, and passing port=None explicitly opts out of it, leaving port as None.
The Config instance is bound to the function when the config parameter is passed into the config_defaults decorator (i.e., when the function is defined). This means the configuration must be fully loaded before the function is called. To work with a configuration that is built later or that varies per run, pass a callable that returns the active configuration instead:
@config_defaults("client", get_config)
def create_client(host: str, port: int, timeout: int = 30):
...Reading Config From a Parameter
If you would rather resolve the configuration entirely at call time, omit the config argument and give the function a config parameter. The decorator reads the configuration from that argument on each call, and does nothing when it is None:
@config_defaults("client")
def create_client(config: Config, host: str, port: int, timeout: int = 30):
...
create_client(config=config) # host and port filled from configThis composes with Object Instantiation’s config injection: a component built via _target_ receives the active Config in its config parameter, and config_defaults reads its defaults from that same configuration.
Complex Types
When a parameter is annotated with a class and its config value is a mapping, the mapping is instantiated into that type using the same coercion as Object Instantiation, including nested _target_ construction.
@config_defaults("server", config)
def start_server(tls: TlsSettings):
...server:
tls:
cert: /etc/certs/app.pem
key: /etc/certs/app.keyCalling start_server() builds a TlsSettings instance from the tls mapping and passes it in.
Transformers
To convert a config value before it reaches the function, pass a transformers mapping from parameter name to a function that takes the config value and returns the transformed value. A transformer takes precedence over type-based instantiation.
from datetime import timedelta
@config_defaults(
"cache",
config,
transformers={"ttl": lambda seconds: timedelta(seconds=seconds)},
)
def build_cache(ttl: timedelta):
...cache:
ttl: 3600Here ttl is stored as a plain number of seconds in the configuration and converted to a timedelta before build_cache receives it.