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
PriorityPolicyfor per-key priority rulesCreate 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:
PriorityPolicyallows per-key priority rulesLater 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
Sourcebase classImplement
nameproperty andload()methodReturn 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()returningTrueImplement
watch()method that yieldsChangeEventobjectsWatch 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¶
Keep it simple: Only use advanced features when necessary
Normalize keys: Always use lowercase and dot notation
Handle errors: Custom sources should be fail-safe
Document custom sources: Explain what they do and how to use them
Test thoroughly: Advanced features need more testing
Summary¶
You’ve now learned:
How to use
PriorityPolicyfor per-key priority rulesHow 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.