RootConfig & CLI Integration
nshconfig
provides RootConfig
, a specialized configuration class that integrates seamlessly with pydantic-settings
to load settings from various sources, including environment variables, .env
files, secrets directories, and command-line arguments. It also offers the CLI
utility to easily turn your configuration models into command-line applications.
RootConfig
RootConfig
inherits from both nshconfig.Config
and pydantic_settings.BaseSettings
, combining the features of both. It’s the recommended base class for your top-level application configuration.
from nshconfig import RootConfig
class AppSettings(RootConfig):
"""My application settings."""
debug_mode: bool = False
"""Enable debug mode"""
api_key: str
"""API key for external service"""
database_url: str = "sqlite:///./default.db"
"""Database connection URL"""
# You can configure pydantic-settings behavior via model_config
model_config = {
"env_prefix": "MYAPP_", # Environment variables should start with MYAPP_
"env_file": ".env", # Load settings from .env file
"secrets_dir": "/run/secrets", # Load secrets from files in this directory
"cli_parse_args": True, # Enable parsing from CLI arguments
"use_attribute_docstrings": True, # Ensure docstrings are used for descriptions
}
# Initialize settings using auto_init
# This will load from .env, environment variables (MYAPP_DEBUG_MODE, MYAPP_API_KEY, ...),
# secrets (/run/secrets/api_key), and CLI arguments (--debug-mode, --api-key, ...)
settings = AppSettings.auto_init()
print(settings.model_dump_json(indent=2))
auto_init
Instead of relying on the potentially confusing magic __init__
behavior of pydantic-settings.BaseSettings
(which tries to load settings automatically upon instantiation), nshconfig.RootConfig
provides the explicit auto_init
class method.
auto_init
performs the core logic of pydantic-settings
: it discovers, loads, and validates configuration values from all configured sources (environment, files, CLI, etc.) and returns a fully validated instance of your RootConfig
class.
You can pass keyword arguments to auto_init
to provide initial values or override specific settings source parameters (like _env_file
, _secrets_dir
, _cli_parse_args
, etc.).
Controlling __init__
Behavior
By default (unset_magic_init_method=True
in RootConfigDict
), RootConfig
disables the automatic settings loading within the standard __init__
method. This makes __init__
behave like a standard Pydantic BaseModel
constructor, improving clarity and compatibility with type checkers and IDEs.
If you need the original pydantic-settings
behavior where __init__
also loads settings, you can configure it:
class LegacySettings(RootConfig):
# ... fields ...
model_config = {
"unset_magic_init_method": False, # Re-enable magic __init__
# ... other settings ...
}
# Now, __init__ will also load settings (though auto_init is still recommended)
settings = LegacySettings(api_key="provided_at_init")
CLI
Utility (CliApp
)
The nshconfig.CLI
class (also available as nshconfig.CliApp
) provides static methods to run your Config
or RootConfig
classes as command-line applications.
Basic Usage with cli_cmd
To make a configuration class runnable, define a cli_cmd
method within it. This method will be executed after the configuration is loaded and validated via CLI.run
.
import asyncio
from nshconfig import RootConfig, CLI
class ToolConfig(RootConfig):
input_file: str
"""Path to the input file"""
output_file: str = "output.txt"
"""Path to the output file"""
force: bool = False
"""Overwrite output file if it exists"""
model_config = {
"cli_parse_args": True,
"use_attribute_docstrings": True,
}
def cli_cmd(self) -> None:
"""The main logic of the command-line tool."""
print(f"Processing {self.input_file}...")
# ... actual processing logic ...
print(f"Writing output to {self.output_file} (Force: {self.force})")
print("Done.")
# Example of an async command
# async def cli_cmd(self) -> None:
# print(f"Processing {self.input_file} asynchronously...")
# await asyncio.sleep(1)
# print(f"Writing output to {self.output_file} (Force: {self.force})")
# print("Done.")
if __name__ == "__main__":
# Run the config as a CLI app.
# CLI.run() will:
# 1. Instantiate ToolConfig using auto_init (loading from CLI args).
# 2. Call the cli_cmd() method on the instance.
config_instance = CLI.run(ToolConfig)
# config_instance holds the validated configuration used
print(f"CLI run finished. Final config: {config_instance.input_file=}")
You can then run this script from your terminal:
python your_script.py --input-file data.csv --force
# Output:
# Processing data.csv...
# Writing output to output.txt (Force: True)
# Done.
# CLI run finished. Final config: config_instance.input_file='data.csv'
CLI.run
handles parsing arguments (using the underlying pydantic-settings
and argparse
integration), instantiating the model via auto_init
, and then executing the cli_cmd
method. It also correctly handles async def cli_cmd
methods.
Subcommands
pydantic-settings
(and therefore nshconfig
) supports defining subcommands using nested models annotated with nshconfig.CliSubCommand
.
from nshconfig import RootConfig, Config, CLI, CliSubCommand
from typing import Annotated
from collections.abc import Sequence
class UserConfig(Config):
name: str
"""User name"""
email: str | None = None
"""User email"""
model_config = {"use_attribute_docstrings": True}
def cli_cmd(self) -> None:
print(f"Running user command for {self.name}")
if self.email:
print(f"Email: {self.email}")
class GroupConfig(Config):
group_name: str
"""Group name"""
members: list[str] = []
"""List of members"""
model_config = {"use_attribute_docstrings": True}
def cli_cmd(self) -> None:
print(f"Running group command for {self.group_name}")
print(f"Members: {', '.join(self.members) or 'None'}")
class AdminTool(RootConfig):
"""Admin tool with user and group subcommands."""
verbose: bool = False
"""Enable verbose output"""
user: Annotated[UserConfig | None, CliSubCommand()] = None
group: Annotated[GroupConfig | None, CliSubCommand()] = None
model_config = {
"cli_parse_args": True,
"use_attribute_docstrings": True,
}
# Optional: Define a top-level command if no subcommand is given
# def cli_cmd(self) -> None:
# print("Admin tool main command. Use 'user' or 'group' subcommand.")
if __name__ == "__main__":
# CLI.run will parse args, identify the subcommand, instantiate it,
# and run its cli_cmd.
config_instance = CLI.run(AdminTool)
# You can also manually run the subcommand if needed
# active_subcommand_name = config_instance.cli_active_subcommand()
# if active_subcommand_name:
# print(f"Active subcommand: {active_subcommand_name}")
# # CLI.run_subcommand(config_instance) # Runs the active subcommand's cli_cmd
Running this script:
# Get help
python admin_tool.py --help
# Run the user subcommand
python admin_tool.py user --name Alice --email [email protected]
# Output:
# Running user command for Alice
# Email: [email protected]
# Run the group subcommand
python admin_tool.py group --group-name admins --members bob carol
# Output:
# Running group command for admins
# Members: bob, carol
CLI.run
automatically detects and executes the cli_cmd
of the chosen subcommand. You can also use CLI.run_subcommand(instance)
to explicitly run the active subcommand’s cli_cmd
on an already instantiated model.
CLI Methods on Config
The base nshconfig.Config
class also gains a few CLI-related helper methods:
config.cli_run_subcommand() -> Config
: Runs thecli_cmd
of the active subcommand, if any.Config.cli_available_subcommands() -> list[str]
: Returns a list of field names that are defined as subcommands.config.cli_active_subcommand() -> str | None
: Returns the name of the subcommand field that was activated via CLI arguments, orNone
.
For more advanced CLI customization options (like argument groups, positional arguments, custom parsers), refer to the pydantic-settings Command Line Support documentation. nshconfig
exposes the necessary types like CliPositionalArg
, CliExplicitFlag
, etc., via from nshconfig import ...
.