Generating Config Files¶
argclass can WRITE config files for a parser — the symmetric inverse of the config-file reading covered elsewhere. The expected end-user workflow is “run the app with a flag and get a config file out”; everything else (programmatic dumps, custom formats) builds on that.
Add --generate-config to your CLI¶
Most users only need this:
import argclass
class CLI(argclass.Parser):
host: str = "localhost"
port: int = 8080
generate_config = argclass.Argument(
action=argclass.GenerateConfigAction,
generator=argclass.INIConfigGenerator,
metavar="FILE",
)
The attribute name generate_config auto-derives --generate-config;
end users then run:
myapp --generate-config /etc/myapp.ini # write a file
myapp --generate-config - # print to stdout
The action writes the file (or stdout) and exits with status 0.
If you want to ship multiple formats, declare one attribute per generator — the flag names follow the attribute names:
class CLI(argclass.Parser):
host: str = "localhost"
generate_ini = argclass.Argument(
action=argclass.GenerateConfigAction,
generator=argclass.INIConfigGenerator,
metavar="FILE",
)
generate_toml = argclass.Argument(
action=argclass.GenerateConfigAction,
generator=argclass.TOMLConfigGenerator,
metavar="FILE",
)
generate_env = argclass.Argument(
action=argclass.GenerateConfigAction,
generator=argclass.EnvConfigGenerator,
metavar="FILE",
)
This is exactly the pattern the interactive demo uses; try it with:
python -m argclass genconfig --generate-ini -
python -m argclass genconfig --generate-toml -
DEMO_HOST=prod python -m argclass genconfig --generate-env -
Picking a format¶
Generator |
Output |
Help comments |
Pick when… |
|---|---|---|---|
|
INI |
|
legacy ecosystems, stdlib-only stack |
|
TOML |
|
comments + nested sections, modern default |
|
JSON |
(dropped) |
machine consumption / pipelines |
|
|
|
Docker, systemd, CI, secret managers |
All four are interchangeable from the user’s perspective — switch
generator=... and rerun.
What lands in the dump¶
The dump reflects the parser’s CURRENT resolved state at the moment
--generate-config fires. argclass’s usual priority applies
(defaults < config files < env vars < CLI args), so all four
sources can shape the output.
CLI flags¶
CLI args parsed BEFORE --generate-config end up in the dump
(argparse processes flags left-to-right; the action exits before
later flags are seen):
myapp --host=10.0.0.1 --port=9090 --generate-config -
produces a config with host = 10.0.0.1 and port = 9090. Putting
--generate-config last is the safe convention.
Environment variables¶
When the parser uses auto_env_var_prefix= (or arguments declare
explicit env_var=), values from os.environ reach the dump too:
APP_HOST=prod.example.com APP_PORT=9999 myapp --generate-config -
writes host = prod.example.com, port = 9999.
Config-file defaults¶
A parser instantiated with config_files=[…] loads those values
during parse_args. Whatever the file contained ends up in the
dump alongside any CLI overrides. This is the building block for
format conversion (next section).
Converting between config formats¶
Format conversion happens through your own parser class — load
through reader X, dump through generator Y. There’s no built-in
--config flag in argclass that loads a config file, so this is
best expressed as a small script (or a __main__ entry point):
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile
class Database(argclass.Group):
host: str = "localhost"
port: int = 5432
class App(argclass.Parser):
debug: bool = False
name: str = "myapp"
db: Database = Database()
# Existing INI (shipped with the app in real life).
with NamedTemporaryFile(mode="w", suffix=".ini", delete=False) as f:
f.write(
"[DEFAULT]\n"
"debug = true\n"
"name = prod\n"
"\n"
"[db]\n"
"host = db.prod.example.com\n"
"port = 6432\n",
)
ini_path = f.name
# Load through the same parser class.
parser = App(config_files=[ini_path])
parser.parse_args([])
# Dump as TOML.
toml_path = Path(ini_path).with_suffix(".toml")
argclass.TOMLConfigGenerator().dump(parser, toml_path)
# Round-trip check: reload the TOML, confirm the same state.
reloaded = App(
config_files=[toml_path],
config_parser_class=argclass.TOMLDefaultsParser,
)
reloaded.parse_args([])
assert reloaded.debug is True
assert reloaded.name == "prod"
assert reloaded.db.host == "db.prod.example.com"
assert reloaded.db.port == 6432
Path(ini_path).unlink()
toml_path.unlink()
Bulk conversion is the same in a loop:
import argclass
from pathlib import Path
for src in Path("configs").glob("*.ini"):
parser = App(config_files=[src])
parser.parse_args([])
dst = src.with_suffix(".toml")
argclass.TOMLConfigGenerator().dump(parser, dst)
The conversion is schema-validated: keys in the source that have no corresponding argument in the parser are silently dropped from the output, and missing keys fall back to argument defaults. That’s usually what you want for a migration script — anything weird in the source surfaces as a missing field in the dump.
Generating env-var listings¶
EnvConfigGenerator emits one KEY=value line per argument, using
the env var name argclass would read (explicit env_var= or computed
from auto_env_var_prefix=). Arguments without a resolvable env var
are skipped — set auto_env_var_prefix= on the parser to cover
everything.
import argclass
class Database(argclass.Group):
host: str = "localhost"
port: int = 5432
class CLI(argclass.Parser):
debug: bool = False
db: Database = Database()
parser = CLI(auto_env_var_prefix="APP_")
text = argclass.EnvConfigGenerator().dump_to_string(parser)
assert "APP_DEBUG=false" in text
assert "APP_DB_HOST=localhost" in text
assert "APP_DB_PORT=5432" in text
Lists serialise to Python literal syntax so argclass can
ast.literal_eval them on read:
APP_TAGS=['alpha', 'beta', 'gamma']
Strings get quoted only when they contain whitespace, =, #,
control chars, or other shell-significant characters; newlines and
tabs are escaped (\n, \t) so each entry stays on one line.
Excluding arguments from dumps¶
Some arguments make no sense in a config file — --version,
--generate-config itself, --check-updates, anything else that
“fires and exits”. argclass needs to know about them so they stay
out of generated configs.
argparse’s built-in --help and --version actions are recognised
and skipped automatically. For your own custom argparse.Action
subclasses, pick one of two equivalent opt-outs:
Option 1 — inherit from argclass.NonConfigAction¶
The cleanest choice for a new action. NonConfigAction sets
__emit_config__ = False for you and keeps intent visible at the
class declaration:
import argparse
import argclass
class PingAction(argclass.NonConfigAction):
def __init__(self, option_strings, dest, **kw):
kw.setdefault("nargs", 0)
kw.setdefault("default", argparse.SUPPRESS)
super().__init__(option_strings, dest, **kw)
def __call__(self, parser, namespace, values, option_string=None):
parser.exit(0, "pong\n")
class CLI(argclass.Parser):
host: str = "localhost"
ping = argclass.Argument(action=PingAction)
text = argclass.INIConfigGenerator().dump_to_string(CLI())
assert "host = localhost" in text
assert "ping" not in text
Option 2 — set __emit_config__ = False on an existing action¶
Useful when you already inherit from a third-party argparse.Action
and would rather not add another base class:
import argparse
import argclass
class PingAction(argparse.Action):
__emit_config__ = False # opt out, equivalent to NonConfigAction
def __init__(self, option_strings, dest, **kw):
kw.setdefault("nargs", 0)
kw.setdefault("default", argparse.SUPPRESS)
super().__init__(option_strings, dest, **kw)
def __call__(self, parser, namespace, values, option_string=None):
parser.exit(0, "pong\n")
class CLI(argclass.Parser):
host: str = "localhost"
ping = argclass.Argument(action=PingAction)
text = argclass.INIConfigGenerator().dump_to_string(CLI())
assert "ping" not in text
Both forms are honoured the same way. If you forget both, the action shows up in dumps as an empty value — that’s the smell test that tells you to opt out.
Dumping from code¶
The CLI-level --generate-config flag is the right entry point for
end users. If you’re writing tests, a migration script, or a hook
that uses the generators directly, use dump_to_string(parser) or
dump(parser, dest):
import argclass
class Database(argclass.Group):
host: str = "localhost"
port: int = 5432
class CLI(argclass.Parser):
debug: bool = False
name: str = argclass.Argument(default="app", help="App name")
db: Database = Database()
parser = CLI()
ini_text = argclass.INIConfigGenerator().dump_to_string(parser)
assert "[DEFAULT]" in ini_text
assert "name = app" in ini_text
assert "[db]" in ini_text
assert "host = localhost" in ini_text
dump(parser, dest) accepts a path (str or pathlib.Path), a
file-like object, or the string "-" for stdout:
import argclass
from pathlib import Path
from tempfile import NamedTemporaryFile
class CLI(argclass.Parser):
host: str = "localhost"
port: int = 8080
with NamedTemporaryFile(mode="w", suffix=".toml", delete=False) as f:
config_path = f.name
argclass.TOMLConfigGenerator().dump(CLI(), config_path)
loaded = CLI(
config_files=[config_path],
config_parser_class=argclass.TOMLDefaultsParser,
)
loaded.parse_args([])
assert loaded.host == "localhost"
assert loaded.port == 8080
Path(config_path).unlink()
You can also pass a generator INSTANCE instead of a class when you
need to construct it with extra arguments (e.g.
generator=argclass.JSONConfigGenerator()).
Custom formats¶
A ConfigGenerator walks the parser tree once and yields
ConfigField records (one per leaf argument) containing the
current value, attribute path, help text, and env var metadata.
Subclasses consume that stream and produce text — that’s the only
thing you need to override:
import argclass
from typing import Sequence
class KeyValueGenerator(argclass.ConfigGenerator):
"""Flat KEY=VALUE format with dotted paths for nested groups."""
extension = ".kv"
def render(self, fields: Sequence[argclass.ConfigField]) -> str:
lines = []
for field in fields:
if field.value is None:
continue
key = ".".join(field.attr_path)
lines.append(f"{key}={field.value}")
return "\n".join(lines) + "\n"
class CLI(argclass.Parser):
host: str = "localhost"
port: int = 8080
text = KeyValueGenerator().dump_to_string(CLI())
assert "host=localhost" in text
assert "port=8080" in text
Field records already carry env var names, so even .env-style
formats are typically a one-method override. See
argclass.ConfigField in the API reference for the full
record shape.
Security note¶
Generators emit values as-is, including those marked
Secret(). A dumped config can therefore contain
credentials. Treat the output file like any credential-bearing file:
Set restrictive permissions when writing to disk.
Avoid dumping to shared locations or to stdout in contexts where logs may be captured.
Prefer
EnvConfigGeneratorwhen you want to record config in a way that’s easy to load via a secret-manager wrapper.
Limitations¶
Subparsers are skipped. They represent runtime branches, not config-time state. Dump each subparser separately by passing its instance to the generator.
JSON has no comments. Help text is dropped in JSON output; INI, TOML, and
.envformats include it.Mid-parse ordering. CLI flags appearing AFTER
--generate-configare not reflected in the dump — argparse invokes the action synchronously and the action exits. Put overrides before the generation flag.TOML is emitted by a minimal hand-rolled writer. It covers the types argclass supports (
str,int,float,bool,list,None). Exotic values fall back tostr().