Advanced Features

In this tutorial, you’ll learn about advanced features like custom priority policies, custom sources, and best practices for complex scenarios.

Learning Objectives

By the end of this tutorial, you’ll be able to:

  • Use PriorityPolicy for per-key priority rules

  • Create custom configuration sources

  • Understand advanced patterns and best practices

Step 1: Custom Priority with PriorityPolicy

Sometimes you need different priority orders for different keys. Use PriorityPolicy:

 1import os
 2from dataclasses import dataclass, field
 3from varlord import Config, sources, PriorityPolicy
 4
 5@dataclass(frozen=True)
 6class AppConfig:
 7    host: str = field(default="0.0.0.0")
 8    port: int = field(default=8000)
 9    api_key: str = field(default="default-key")
10
11# Set environment variables (no prefix needed)
12os.environ["HOST"] = "env-host"
13os.environ["PORT"] = "9000"
14os.environ["API_KEY"] = "env-key"
15
16# Define priority policy
17policy = PriorityPolicy(
18    rules={
19        "host": ["defaults", "env"],  # Env overrides defaults
20        "port": ["defaults", "env"],  # Env overrides defaults
21        "api_key": ["env", "defaults"],  # Defaults override env (unusual!)
22    }
23)
24
25cfg = Config(
26    model=AppConfig,
27    sources=[
28        sources.Env(),  # Defaults applied automatically
29    ],
30    policy=policy,
31)
32
33app = cfg.load()
34print(f"Host: {app.host}")      # From env: env-host
35print(f"Port: {app.port}")      # From env: 9000
36print(f"API Key: {app.api_key}")  # From defaults: default-key (env overridden!)

Expected Output:

Host: env-host
Port: 9000
API Key: default-key

Key Points:

  • PriorityPolicy allows per-key priority rules

  • Later sources in the list override earlier ones

  • Useful when you need different override behavior for different keys

Step 2: Creating Custom Sources

You can create custom sources by extending the Source base class:

 1import json
 2from typing import Mapping, Any
 3from dataclasses import dataclass
 4from varlord import Config
 5from varlord.sources.base import Source
 6
 7class JSONFileSource(Source):
 8    """Source that loads configuration from a JSON file."""
 9
10    def __init__(self, file_path: str):
11        self._file_path = file_path
12
13    @property
14    def name(self) -> str:
15        return "json_file"
16
17    def load(self) -> Mapping[str, Any]:
18        """Load configuration from JSON file."""
19        try:
20            with open(self._file_path, "r") as f:
21                data = json.load(f)
22                # Normalize keys to lowercase for consistency
23                return {k.lower(): v for k, v in data.items()}
24        except FileNotFoundError:
25            return {}  # Return empty if file doesn't exist
26        except json.JSONDecodeError:
27            return {}  # Return empty if JSON is invalid
28
29@dataclass(frozen=True)
30class AppConfig:
31    host: str = field(default="0.0.0.0")
32    port: int = field(default=8000)
33
34# Create JSON file
35import tempfile
36with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
37    json.dump({"host": "json-host", "port": 7000}, f)
38    json_path = f.name
39
40# Use custom source
41cfg = Config(
42    model=AppConfig,
43    sources=[
44        JSONFileSource(json_path),  # Defaults applied automatically
45    ],
46)
47
48app = cfg.load()
49print(f"Host: {app.host}")  # From JSON: json-host
50print(f"Port: {app.port}")  # From JSON: 7000
51
52# Cleanup
53import os
54os.unlink(json_path)

Expected Output:

Host: json-host
Port: 7000

Key Points:

  • Extend Source base class

  • Implement name property and load() method

  • Return a dictionary with normalized keys (lowercase, dot notation)

  • Handle errors gracefully (return empty dict on failure)

Step 3: Custom Source with Watch Support

For sources that support watching, implement supports_watch() and watch():

 1import time
 2from typing import Iterator
 3from varlord.sources.base import Source, ChangeEvent
 4
 5class PollingFileSource(Source):
 6    """Source that polls a file for changes."""
 7
 8    def __init__(self, file_path: str, poll_interval: float = 1.0):
 9        self._file_path = file_path
10        self._poll_interval = poll_interval
11        self._last_mtime = 0
12
13    @property
14    def name(self) -> str:
15        return "polling_file"
16
17    def supports_watch(self) -> bool:
18        """Declare that this source supports watching."""
19        return True
20
21    def load(self) -> Mapping[str, Any]:
22        """Load current configuration from file."""
23        # Implementation similar to JSONFileSource
24        return {}
25
26    def watch(self) -> Iterator[ChangeEvent]:
27        """Watch for file changes by polling."""
28        import os
29        while True:
30            try:
31                current_mtime = os.path.getmtime(self._file_path)
32                if current_mtime > self._last_mtime:
33                    self._last_mtime = current_mtime
34                    # Load new configuration
35                    new_config = self.load()
36                    # Yield change events for all keys
37                    for key, value in new_config.items():
38                        yield ChangeEvent(key=key, value=value, source=self.name)
39            except FileNotFoundError:
40                pass  # File doesn't exist yet
41            time.sleep(self._poll_interval)

Key Points:

  • Implement supports_watch() returning True

  • Implement watch() method that yields ChangeEvent objects

  • Watch is automatically enabled when using load_store()

Step 4: Best Practices for Complex Configurations

Here are some best practices for complex scenarios:

1. Organize Configuration by Domain

 1from dataclasses import dataclass, field
 2
 3@dataclass(frozen=True)
 4class DatabaseConfig:
 5    host: str = field(default="localhost")
 6    port: int = field(default=5432)
 7
 8@dataclass(frozen=True)
 9class CacheConfig:
10    host: str = field(default="localhost")
11    port: int = field(default=6379)
12
13@dataclass(frozen=True)
14class AppConfig:
15    db: DatabaseConfig = field(default_factory=lambda: DatabaseConfig())
16    cache: CacheConfig = field(default_factory=lambda: CacheConfig())

2. Use Environment-Specific Defaults

1import os
2
3@dataclass(frozen=True)
4class AppConfig:
5    debug: bool = field(default=os.getenv("ENV") != "production")
6    log_level: str = field(default="DEBUG" if os.getenv("ENV") != "production" else "INFO")

3. Validate Critical Fields

1from dataclasses import field
2from varlord.validators import validate_not_empty, validate_port
3
4@dataclass(frozen=True)
5class AppConfig:
6    api_key: str = field(default="")
7
8    def __post_init__(self):
9        validate_not_empty(self.api_key)  # Fail fast if missing

Step 5: Complete Advanced Example

Here’s a complete example combining multiple advanced features:

 1import os
 2from dataclasses import dataclass, field
 3from varlord import Config, sources, PriorityPolicy
 4from varlord.validators import validate_port, validate_not_empty
 5
 6@dataclass(frozen=True)
 7class DBConfig:
 8    host: str = field(default="localhost")
 9    port: int = field(default=5432)
10
11    def __post_init__(self):
12        validate_not_empty(self.host)
13        validate_port(self.port)
14
15@dataclass(frozen=True)
16class AppConfig:
17    host: str = field(default="0.0.0.0")
18    port: int = field(default=8000)
19    db: DBConfig = field(default_factory=lambda: DBConfig())
20
21    def __post_init__(self):
22        validate_port(self.port)
23
24def main():
25    # Set environment variables (no prefix needed)
26    os.environ["HOST"] = "0.0.0.0"
27    os.environ["PORT"] = "9000"
28    os.environ["DB__HOST"] = "db.example.com"
29    os.environ["DB__PORT"] = "3306"
30
31    # Use PriorityPolicy for fine-grained control
32    policy = PriorityPolicy(
33        rules={
34            "port": ["defaults", "env"],  # Env overrides defaults
35            "db.port": ["env", "defaults"],  # Defaults override env (example)
36        }
37    )
38
39    cfg = Config(
40        model=AppConfig,
41        sources=[
42            sources.Env(),  # Model defaults applied automatically
43        ],
44        policy=policy,
45    )
46
47    app = cfg.load()
48
49    print("Advanced configuration loaded:")
50    print(f"  App: {app.host}:{app.port}")
51    print(f"  DB: {app.db.host}:{app.db.port}")
52
53if __name__ == "__main__":
54    main()

Expected Output:

Advanced configuration loaded:
  App: 0.0.0.0:9000
  DB: db.example.com:3306

Common Pitfalls

Pitfall 1: Overusing PriorityPolicy

# Don't do this for simple cases
policy = PriorityPolicy(
    rules={
        "host": ["defaults", "env"],
        "port": ["defaults", "env"],
        # ... same rule for every key
    }
)

Solution: Only use PriorityPolicy when you need different priority rules for different keys. For uniform priority, just order your sources correctly.

Pitfall 2: Not normalizing keys in custom sources

def load(self) -> Mapping[str, Any]:
    data = json.load(f)
    return data  # Keys might be uppercase or inconsistent!

Solution: Always normalize keys to lowercase and use dot notation for consistency with other sources.

Pitfall 3: Not handling errors in custom sources

def load(self) -> Mapping[str, Any]:
    with open(self._file_path) as f:
        return json.load(f)  # Raises exception if file doesn't exist!

Solution: Always handle errors gracefully and return empty dict on failure.

Best Practices

  1. Keep it simple: Only use advanced features when necessary

  2. Normalize keys: Always use lowercase and dot notation

  3. Handle errors: Custom sources should be fail-safe

  4. Document custom sources: Explain what they do and how to use them

  5. Test thoroughly: Advanced features need more testing

Summary

You’ve now learned:

  • How to use PriorityPolicy for per-key priority rules

  • How to create custom configuration sources

  • How to implement watch support in custom sources

  • Best practices for complex configurations

You’re now ready to use Varlord in production! For more details, see the User Guide and API Reference.