Skip to content

Commit 3602b13

Browse files
committed
ready to ship 0.2.0
1 parent 93a668f commit 3602b13

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2926
-1261
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.0] - 2025-01-02
9+
10+
### Added
11+
- **Explicit Required/Optional Configuration**: All fields must explicitly specify `required=True` or `optional=True` in metadata. No inference allowed.
12+
- **Automatic Model Defaults**: Model defaults are automatically applied as base layer. No need to explicitly include `sources.Defaults` in sources list.
13+
- **Model-Driven Source Filtering**: All sources (Env, CLI, DotEnv, Etcd) now filter variables/arguments based on model fields. Only model-defined fields are loaded.
14+
- **Required Field Validation**: New `Config.validate()` method to validate required fields independently. `Config.load()` now has optional `validate` parameter.
15+
- **Comprehensive Error Messages**: When required fields are missing, error messages include:
16+
- List of missing fields with descriptions
17+
- Source mapping rules and examples for each active source
18+
- Actionable guidance on how to provide missing parameters
19+
- **Field Metadata Support**: Support for `description` and `help` in field metadata for better documentation and CLI help text.
20+
- **New Modules**:
21+
- `varlord.metadata`: Field information extraction and utilities
22+
- `varlord.validation`: Model definition validation and configuration validation
23+
- `varlord.source_help`: Source mapping examples and error message formatting
24+
25+
### Changed
26+
- **BREAKING**: `Env` source no longer accepts `prefix` parameter. All environment variables are filtered by model fields.
27+
- **BREAKING**: All fields must have explicit `required` or `optional` metadata. `ModelDefinitionError` is raised if missing.
28+
- **BREAKING**: `Config.from_model()` no longer accepts `env_prefix` parameter.
29+
- **BREAKING**: `Defaults` source is now internal. Model defaults are automatically applied, no need to include in sources list.
30+
- **BREAKING**: Empty strings and empty collections are now considered valid values for required fields (only presence is checked, not emptiness).
31+
32+
### Fixed
33+
- Improved error messages for missing required fields
34+
- Better CLI help text with field descriptions
35+
- Consistent model filtering across all sources
36+
837
## [0.1.0] - 2025-12-31
938

1039
### Added

LICENSE

Lines changed: 201 additions & 204 deletions
Large diffs are not rendered by default.

docs/source/api_reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ Complete API documentation for all Varlord classes and functions.
1111
sources
1212
policy
1313
validators
14+
model_validation
1415
logging
1516

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
Model Validation
2+
================
3+
4+
This module provides validation functions for model definitions and configuration structure.
5+
6+
.. automodule:: varlord.model_validation
7+
:members:
8+
:undoc-members:
9+
:show-inheritance:
10+
11+
Functions
12+
---------
13+
14+
**validate_model_definition**
15+
Validates that all fields in a dataclass model have explicit required/optional metadata.
16+
17+
Example:
18+
19+
.. code-block:: python
20+
21+
from varlord.model_validation import validate_model_definition
22+
from dataclasses import dataclass, field
23+
24+
@dataclass
25+
class Config:
26+
api_key: str = field(metadata={"required": True}) # OK
27+
host: str = field() # Missing metadata - will raise ModelDefinitionError
28+
29+
validate_model_definition(Config) # Raises ModelDefinitionError
30+
31+
**validate_config**
32+
Validates that all required fields exist in a configuration dictionary.
33+
34+
Example:
35+
36+
.. code-block:: python
37+
38+
from varlord.model_validation import validate_config, RequiredFieldError
39+
from dataclasses import dataclass, field
40+
41+
@dataclass
42+
class Config:
43+
api_key: str = field(metadata={"required": True})
44+
45+
config_dict = {} # Missing api_key
46+
try:
47+
validate_config(Config, config_dict, [])
48+
except RequiredFieldError as e:
49+
print(e) # Shows missing fields and source help
50+
51+
Exceptions
52+
----------
53+
54+
**VarlordError**
55+
Base exception for all varlord errors.
56+
57+
**ModelDefinitionError**
58+
Raised when a field is missing required/optional metadata in the model definition.
59+
60+
**RequiredFieldError**
61+
Raised when required fields are missing from the configuration dictionary.
62+
Includes comprehensive error messages with source mapping help.
63+

docs/source/api_reference/validators.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,4 @@ The validators are organized into the following categories:
5555

5656
**Custom Validators**
5757
- :func:`validate_custom` - Validate using custom function
58-
- :func:`validate_config` - Validate configuration object
58+
- :func:`apply_validators` - Apply validators to configuration object

docs/source/quickstart.rst

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ Basic Usage
1818

1919
.. code-block:: python
2020
21-
from dataclasses import dataclass
21+
from dataclasses import dataclass, field
2222
from varlord import Config, sources
2323
2424
@dataclass(frozen=True)
2525
class AppConfig:
26-
host: str = "127.0.0.1"
27-
port: int = 8000
28-
debug: bool = False
26+
host: str = field(default="127.0.0.1", metadata={"optional": True})
27+
port: int = field(default=8000, metadata={"optional": True})
28+
debug: bool = field(default=False, metadata={"optional": True})
2929
3030
**Step 2: Create and load configuration**
3131

@@ -34,8 +34,7 @@ Basic Usage
3434
cfg = Config(
3535
model=AppConfig,
3636
sources=[
37-
sources.Defaults(model=AppConfig),
38-
sources.Env(prefix="APP_"),
37+
sources.Env(), # Model defaults applied automatically
3938
sources.CLI(), # Model auto-injected
4039
],
4140
)
@@ -49,8 +48,7 @@ Basic Usage
4948
5049
cfg = Config.from_model(
5150
AppConfig,
52-
env_prefix="APP_",
53-
cli=True,
51+
cli=True, # env_prefix removed - all env vars filtered by model
5452
)
5553
5654
app = cfg.load()
@@ -65,8 +63,7 @@ Priority is determined by sources order (later sources override earlier ones):
6563
cfg = Config(
6664
model=AppConfig,
6765
sources=[
68-
sources.Defaults(model=AppConfig), # Lowest priority
69-
sources.Env(prefix="APP_"),
66+
sources.Env(), # Model defaults applied first, then env
7067
sources.CLI(), # Highest priority (last)
7168
],
7269
)
@@ -134,7 +131,6 @@ Use ConfigStore for dynamic configuration updates:
134131
cfg = Config(
135132
model=AppConfig,
136133
sources=[
137-
sources.Defaults(model=AppConfig),
138134
sources.Etcd(..., watch=True), # Enable watch here
139135
],
140136
)

docs/source/tutorial/advanced_features.rst

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ Sometimes you need different priority orders for different keys. Use
2323
:linenos:
2424
2525
import os
26-
from dataclasses import dataclass
26+
from dataclasses import dataclass, field
2727
from varlord import Config, sources, PriorityPolicy
2828
2929
@dataclass(frozen=True)
3030
class AppConfig:
31-
host: str = "0.0.0.0"
32-
port: int = 8000
33-
api_key: str = "default-key"
31+
host: str = field(default="0.0.0.0", metadata={"optional": True})
32+
port: int = field(default=8000, metadata={"optional": True})
33+
api_key: str = field(default="default-key", metadata={"optional": True})
3434
35-
# Set environment variables
36-
os.environ["APP_HOST"] = "env-host"
37-
os.environ["APP_PORT"] = "9000"
38-
os.environ["APP_API_KEY"] = "env-key"
35+
# Set environment variables (no prefix needed)
36+
os.environ["HOST"] = "env-host"
37+
os.environ["PORT"] = "9000"
38+
os.environ["API_KEY"] = "env-key"
3939
4040
# Define priority policy
4141
policy = PriorityPolicy(
@@ -49,8 +49,7 @@ Sometimes you need different priority orders for different keys. Use
4949
cfg = Config(
5050
model=AppConfig,
5151
sources=[
52-
sources.Defaults(model=AppConfig),
53-
sources.Env(prefix="APP_"),
52+
sources.Env(), # Defaults applied automatically
5453
],
5554
policy=policy,
5655
)
@@ -112,8 +111,8 @@ You can create custom sources by extending the ``Source`` base class:
112111
113112
@dataclass(frozen=True)
114113
class AppConfig:
115-
host: str = "0.0.0.0"
116-
port: int = 8000
114+
host: str = field(default="0.0.0.0", metadata={"optional": True})
115+
port: int = field(default=8000, metadata={"optional": True})
117116
118117
# Create JSON file
119118
import tempfile
@@ -125,8 +124,7 @@ You can create custom sources by extending the ``Source`` base class:
125124
cfg = Config(
126125
model=AppConfig,
127126
sources=[
128-
sources.Defaults(model=AppConfig),
129-
JSONFileSource(json_path),
127+
JSONFileSource(json_path), # Defaults applied automatically
130128
],
131129
)
132130
@@ -222,18 +220,18 @@ Here are some best practices for complex scenarios:
222220
223221
@dataclass(frozen=True)
224222
class DatabaseConfig:
225-
host: str = "localhost"
226-
port: int = 5432
223+
host: str = field(default="localhost", metadata={"optional": True})
224+
port: int = field(default=5432, metadata={"optional": True})
227225
228226
@dataclass(frozen=True)
229227
class CacheConfig:
230-
host: str = "localhost"
231-
port: int = 6379
228+
host: str = field(default="localhost", metadata={"optional": True})
229+
port: int = field(default=6379, metadata={"optional": True})
232230
233231
@dataclass(frozen=True)
234232
class AppConfig:
235-
db: DatabaseConfig = field(default_factory=lambda: DatabaseConfig())
236-
cache: CacheConfig = field(default_factory=lambda: CacheConfig())
233+
db: DatabaseConfig = field(default_factory=lambda: DatabaseConfig(), metadata={"optional": True})
234+
cache: CacheConfig = field(default_factory=lambda: CacheConfig(), metadata={"optional": True})
237235
238236
**2. Use Environment-Specific Defaults**
239237

@@ -244,19 +242,20 @@ Here are some best practices for complex scenarios:
244242
245243
@dataclass(frozen=True)
246244
class AppConfig:
247-
debug: bool = os.getenv("ENV") != "production"
248-
log_level: str = "DEBUG" if os.getenv("ENV") != "production" else "INFO"
245+
debug: bool = field(default=os.getenv("ENV") != "production", metadata={"optional": True})
246+
log_level: str = field(default="DEBUG" if os.getenv("ENV") != "production" else "INFO", metadata={"optional": True})
249247
250248
**3. Validate Critical Fields**
251249

252250
.. code-block:: python
253251
:linenos:
254252
253+
from dataclasses import field
255254
from varlord.validators import validate_not_empty, validate_port
256255
257256
@dataclass(frozen=True)
258257
class AppConfig:
259-
api_key: str = ""
258+
api_key: str = field(default="", metadata={"optional": True})
260259
261260
def __post_init__(self):
262261
validate_not_empty(self.api_key) # Fail fast if missing
@@ -277,28 +276,28 @@ Here's a complete example combining multiple advanced features:
277276
278277
@dataclass(frozen=True)
279278
class DBConfig:
280-
host: str = "localhost"
281-
port: int = 5432
279+
host: str = field(default="localhost", metadata={"optional": True})
280+
port: int = field(default=5432, metadata={"optional": True})
282281
283282
def __post_init__(self):
284283
validate_not_empty(self.host)
285284
validate_port(self.port)
286285
287286
@dataclass(frozen=True)
288287
class AppConfig:
289-
host: str = "0.0.0.0"
290-
port: int = 8000
291-
db: DBConfig = field(default_factory=lambda: DBConfig())
288+
host: str = field(default="0.0.0.0", metadata={"optional": True})
289+
port: int = field(default=8000, metadata={"optional": True})
290+
db: DBConfig = field(default_factory=lambda: DBConfig(), metadata={"optional": True})
292291
293292
def __post_init__(self):
294293
validate_port(self.port)
295294
296295
def main():
297-
# Set environment variables
298-
os.environ["APP_HOST"] = "0.0.0.0"
299-
os.environ["APP_PORT"] = "9000"
300-
os.environ["APP_DB__HOST"] = "db.example.com"
301-
os.environ["APP_DB__PORT"] = "3306"
296+
# Set environment variables (no prefix needed)
297+
os.environ["HOST"] = "0.0.0.0"
298+
os.environ["PORT"] = "9000"
299+
os.environ["DB__HOST"] = "db.example.com"
300+
os.environ["DB__PORT"] = "3306"
302301
303302
# Use PriorityPolicy for fine-grained control
304303
policy = PriorityPolicy(
@@ -311,8 +310,7 @@ Here's a complete example combining multiple advanced features:
311310
cfg = Config(
312311
model=AppConfig,
313312
sources=[
314-
sources.Defaults(model=AppConfig),
315-
sources.Env(prefix="APP_", separator="__"),
313+
sources.Env(), # Defaults applied automatically
316314
],
317315
policy=policy,
318316
)

0 commit comments

Comments
 (0)