SmartOptions Guide

Complete guide to using SmartOptions for intelligent option merging and configuration loading.

Overview

SmartOptions is a versatile namespace class for managing configuration with intelligent merging, multi-source loading, and hierarchical data support. Built on TreeDict, it uses path notation for accessing nested values.

Key Features:

  • Load config from files (YAML, JSON, TOML, INI)

  • Load config from environment variables

  • Extract defaults from function signatures

  • Merge multiple sources with + operator

  • Nested dicts become SmartOptions recursively

  • String lists become feature flags

  • List of dicts indexed by first key value

  • Path notation access: opts["server.host"]

Basic Usage

from genro_toolbox import SmartOptions

# Incoming options override defaults
opts = SmartOptions(
    incoming={'timeout': 5},
    defaults={'timeout': 1, 'retries': 3}
)

print(opts["timeout"])  # 5 (from incoming)
print(opts["retries"])  # 3 (from defaults)

Loading from Files

SmartOptions can load configuration directly from files:

# From YAML
opts = SmartOptions('config.yaml')

# From JSON
opts = SmartOptions('config.json')

# From TOML
opts = SmartOptions('config.toml')

# From INI (keys become section_key format)
opts = SmartOptions('config.ini')

# From Path object
from pathlib import Path
opts = SmartOptions(Path('config.yaml'))

Missing files return an empty SmartOptions (no error).

Loading from Environment Variables

Use the ENV:PREFIX syntax to load from environment variables:

# Given: MYAPP_HOST=localhost MYAPP_PORT=9000
opts = SmartOptions('ENV:MYAPP')

print(opts["host"])  # 'localhost'
print(opts["port"])  # '9000' (string from env)

The prefix is stripped and keys are lowercased.

Loading from Function Signatures

Extract defaults and parse argv from a callable:

def serve(host: str = '127.0.0.1', port: int = 8000, debug: bool = False):
    pass

# Just extract defaults
opts = SmartOptions(serve)
print(opts["host"])   # '127.0.0.1'
print(opts["port"])   # 8000
print(opts["debug"])  # False

Using env and argv Parameters

Load from environment and CLI with automatic type conversion:

def serve(host: str = '127.0.0.1', port: int = 8000, debug: bool = False):
    pass

# Given: MYAPP_HOST=0.0.0.0 MYAPP_PORT=9000
opts = SmartOptions(serve, env='MYAPP', argv=['--debug'])

print(opts["host"])   # '0.0.0.0' (from env)
print(opts["port"])   # 9000 (int, converted from env)
print(opts["debug"])  # True (from argv)

Priority: defaults < env < argv (rightmost wins)

Types are extracted from the function signature and applied to both env and argv values.

Boolean conversion from environment supports: true, 1, yes, on (case-insensitive) → True

Legacy argv Syntax

You can also pass argv as second positional argument:

import sys
opts = SmartOptions(serve, sys.argv[1:])
# ./app.py --port 9000 --debug
# opts["port"] = 9000, opts["debug"] = True

Annotated Types

Supports Annotated types for help strings:

from typing import Annotated

def serve(
    app_dir: Annotated[str, 'Path to application'],
    port: Annotated[int, 'Server port'] = 8000,
):
    pass

opts = SmartOptions(serve, argv=['/path/to/app', '--port', '9000'])
print(opts["app_dir"])  # '/path/to/app'
print(opts["port"])     # 9000

Composing with + Operator

The most powerful feature: compose multiple sources with priority:

def serve(host: str = '0.0.0.0', port: int = 8000, debug: bool = False):
    pass

# Priority: base < file < env < argv (rightmost wins)
opts = (
    SmartOptions(serve) +                    # defaults from signature
    SmartOptions('config.yaml') +            # file overrides
    SmartOptions('ENV:MYAPP') +              # env overrides
    SmartOptions(serve, sys.argv[1:])        # argv overrides (highest)
)

You can also add plain dicts:

opts = SmartOptions({'a': 1}) + {'b': 2, 'a': 10}
print(opts["a"])  # 10 (dict overrides)
print(opts["b"])  # 2

Nested Structures

Nested Dicts Become SmartOptions

opts = SmartOptions({
    'server': {
        'host': 'localhost',
        'port': 8080
    }
})

print(opts["server.host"])  # 'localhost'
print(opts["server.port"])  # 8080

String Lists Become Feature Flags

opts = SmartOptions({
    'middleware': ['cors', 'compression', 'logging']
})

print(opts["middleware.cors"])         # True
print(opts["middleware.compression"])  # True
print('cors' in opts["middleware"])    # True

List of Dicts Indexed by First Key

opts = SmartOptions({
    'apps': [
        {'name': 'shop', 'module': 'shop:ShopApp'},
        {'name': 'office', 'module': 'office:OfficeApp'},
    ]
})

print(opts["apps.shop.module"])    # 'shop:ShopApp'
print(opts["apps.office.module"])  # 'office:OfficeApp'
print('shop' in opts["apps"])      # True

Filtering Options

Ignore None Values

opts = SmartOptions(
    incoming={'timeout': None},
    defaults={'timeout': 10},
    ignore_none=True
)

print(opts["timeout"])  # 10 (default kept)

Ignore Empty Collections

opts = SmartOptions(
    incoming={'tags': [], 'name': ''},
    defaults={'tags': ['prod'], 'name': 'default'},
    ignore_empty=True
)

print(opts["tags.prod"])  # True (feature flag)
print(opts["name"])       # 'default'

Custom Filter Function

def only_positive(key, value):
    return isinstance(value, (int, float)) and value > 0

opts = SmartOptions(
    incoming={'timeout': -5, 'retries': 3},
    defaults={'timeout': 30, 'retries': 1},
    filter_fn=only_positive
)

print(opts["timeout"])  # 30 (negative filtered)
print(opts["retries"])  # 3 (positive, accepted)

Access Patterns

opts = SmartOptions({'a': 1, 'b': 2})

# Path notation (primary)
print(opts["a"])        # 1
print(opts["missing"])  # None (no error)

# Nested path access
print(opts["x.y.z"])    # None (missing path)

# Containment
print('a' in opts)   # True

# Iteration
for key in opts:
    print(key)       # 'a', 'b'

# Convert to dict
d = opts.as_dict()   # {'a': 1, 'b': 2}

Real-World Example

Complete CLI application configuration:

from typing import Annotated
from genro_toolbox import SmartOptions
import sys

def serve(
    app_dir: Annotated[str, 'Path to application directory'],
    host: Annotated[str, 'Server host'] = '127.0.0.1',
    port: Annotated[int, 'Server port'] = 8000,
    workers: Annotated[int, 'Number of workers'] = 4,
    debug: Annotated[bool, 'Enable debug mode'] = False,
):
    """Start the application server."""
    # Option 1: Single SmartOptions with env and argv (recommended)
    config = (
        SmartOptions('config.yaml') +              # 1. Config file
        SmartOptions('config.local.yaml') +        # 2. Local overrides
        SmartOptions(serve, env='MYAPP', argv=sys.argv[1:])  # 3. defaults < env < argv
    )

    # Option 2: Compose with + operator for full control
    # config = (
    #     SmartOptions(serve) +                    # 1. Function defaults
    #     SmartOptions('config.yaml') +            # 2. Config file
    #     SmartOptions('ENV:MYAPP') +              # 3. Environment (strings)
    #     SmartOptions(serve, sys.argv[1:])        # 4. CLI args (highest)
    # )

    print(f"Starting server at {config['host']}:{config['port']}")
    print(f"App: {config['app_dir']}, Workers: {config['workers']}")
    if config["debug"]:
        print("Debug mode enabled")

if __name__ == '__main__':
    serve()

Config file (config.yaml):

host: 0.0.0.0
workers: 8
middleware:
  - cors
  - compression
apps:
  - name: api
    module: api:app
  - name: admin
    module: admin:app

Usage:

# Use defaults + config file
./app.py /path/to/app

# Override port via CLI
./app.py /path/to/app --port 9000 --debug

# Override via environment
MYAPP_WORKERS=16 ./app.py /path/to/app

API Reference

class SmartOptions(TreeDict):
    """
    Convenience namespace for option management, built on TreeDict.

    Args:
        incoming: One of:
            - Mapping with runtime kwargs
            - str path to config file (YAML, JSON, TOML, INI)
            - str 'ENV:PREFIX' for environment variables
            - Path object to config file
            - Callable to extract defaults from signature
        defaults: One of:
            - Mapping with baseline options
            - list[str] as argv when incoming is callable (legacy)
            - None
        env: Environment variable prefix (e.g., "MYAPP" for MYAPP_HOST).
            Only used when incoming is a callable. Types from signature
            are used for conversion.
        argv: Command line arguments list. Only used when incoming is
            a callable. Types from signature are used for conversion.
        ignore_none: Skip incoming entries where value is None
        ignore_empty: Skip empty strings/collections from incoming
        filter_fn: Custom filter callable(key, value) -> bool

    When incoming is a callable with env/argv:
        - Defaults come from function signature
        - env values override defaults (with type conversion)
        - argv values override env (with type conversion)
        Priority: defaults < env < argv

    Operators:
        +: Merge two SmartOptions (right side wins)
        in: Check key existence
        []: Path notation access (returns None for missing)
    """

    def as_dict(self) -> dict[str, Any]:
        """Return a copy of current options as dict."""
        ...

    def __add__(self, other) -> SmartOptions:
        """Merge with another SmartOptions or dict."""
        ...

See Also