cot.config.ingest
Declare configuration once, as annotated classes, and ingest it from CLI arguments, environment variables and config files — with the merge order, the name mapping and the provenance all falling out of the declaration.
Experimental. This is an early experiment that may or may not work out. The API moves between releases, and there is no deprecation policy yet.
Installing this package patches pytest. See pytest integration before installing it anywhere you care about.
-p no:cot_configturns it off.
The problem
pytest declares every logging option twice — once as an ini option, once as a CLI option — and reconciles the two by hand at read time:
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("logging")
def add_option_ini(option, dest, default=None, type=None, **kwargs):
parser.addini(dest, default=default, type=type,
help="Default value for " + option)
group.addoption(option, dest=dest, **kwargs)
add_option_ini("--log-level", dest="log_level", default=None, metavar="LEVEL",
help="Level of messages to catch/display.")
add_option_ini("--log-format", dest="log_format", default=DEFAULT_LOG_FORMAT,
help="Log format used by the logging module")
# ... 11 more, plus a bare parser.addini for log_cli, plus a bare
# group.addoption for --log-disable
Then, at use:
log_cli_format = get_option_ini(config, "log_cli_format", "log_format")
Thirteen options, ~90 lines, a local helper, and a fallback chain spelled out by
hand at every read site. Adding pyproject.toml support on top of that is
daunting, which is the itch this library exists to scratch.
The same thing, declared once
This is the real declaration from
testing/test_pytest_logging.py, the
project's acceptance test — all 13 ini options plus the CLI-only
--log-disable, reachable by their real pytest names from ini, TOML, env and
CLI:
from typing import Annotated
from cot.config import ConfigPart, SubConfig, from_parent, help, named, no_cli
class LogOutputConfig(SubConfig):
"""Settings shared by every log output.
`from_parent` is what makes `log_cli_level` fall back to `log_level`.
"""
level: Annotated[str | None, from_parent, help("level of messages to catch")] = None
format: Annotated[str, from_parent, help("log format")] = DEFAULT_LOG_FORMAT
date_format: Annotated[str, from_parent, help("log date format")] = (
DEFAULT_LOG_DATE_FORMAT
)
class LogCliConfig(LogOutputConfig):
# pytest calls this `log_cli`, and it is ini-only: live logging is switched
# on from the command line with --log-cli-level instead.
enabled: Annotated[bool, named("log_cli"), no_cli, help("enable live logs")] = False
class LogFileConfig(LogOutputConfig):
# Structurally `file.path`, but pytest calls it `log_file`.
path: Annotated[str | None, named("log_file"), help("path to log file")] = None
mode: Annotated[str, named("log_file_mode"), help("log file open mode")] = "w"
class LoggingConfig(LogOutputConfig, ConfigPart, prefix="pytest", name_prefix="log"):
auto_indent: Annotated[str | None, help("auto-indent multiline messages")] = None
logger_disable: Annotated[
list[str], named("log_disable"), help("disable a logger by name")
] = []
cli: LogCliConfig
file: LogFileConfig
The fallback pytest hand-rolls with get_option_ini(config, "log_cli_format", "log_format") is the from_parent cascade. Reading a value is attribute
access:
config.cli.format # falls back to config.format when unset
config.file.path # the --log-file / log_file value
Using it
The usage walkthrough, names, precedence, provenance and help live in
the documentation; it
is executed by testing/test_docs_index.py the same way this README is by
testing/test_readme.py. In short:
import sys
from cot.config import CLISource, ConfigManager, EnvSource
manager = ConfigManager(sources=[EnvSource(), CLISource(sys.argv[1:])])
manager.declare(LoggingConfig)
config = manager.get(LoggingConfig)
declare() registers a type, get() resolves every declared type once and
returns the built instance, and manager.explain(LoggingConfig) says where each
value came from.
pytest integration (a hack, on purpose)
cot.config.pytest_plugin monkeypatches pytest. It adds three methods
pytest does not have:
Parser.add_config(part_type)Config.get_config(part_type)Config.explain_config(part_type)
It is registered as a pytest11 entry point, so installing the package is
enough to activate it — importing the plugin module patches
_pytest.config.Parser and _pytest.config.Config at import time, in every
environment the package is installed into, including as a transitive
dependency.
That is deliberate for now. The point of the proof of concept is to show the
library driving real pytest options, and a conftest-level
pytest_plugins = [...] would be too late: that conftest's own
pytest_addoption runs before its plugin list is processed. It is not how a
stable release should behave, and it will change: the plan is to replace it with
importable add_config(parser, T) / get_config(config, T) functions. See
docs/design/pytest/evolution.md.
What you should know:
- The patch is additive only. No option, ini key, hook or behaviour of pytest's is replaced.
- Turn it off with
-p no:cot_config. - A type checker cannot see the patched methods. Use
manager_for_config(config).get(T)for the statically-typed equivalent. - The collision behaviour is pinned by tests: an ini key pytest already
declares (
log_level) is adopted rather than clobbered, and a colliding CLI option raisesConfigLifecycleErrornaming the field instead of argparse's bare "conflicting option string".
def pytest_addoption(parser):
parser.add_config(LoggingConfig) # declare
def pytest_configure(config):
log = config.get_config(LoggingConfig) # resolve + get, typed
config.explain_config(LoggingConfig) # provenance table
The intended end state is the reverse of this: pytest using the library directly, with no patching at all.
Installing
pip install cot-config
Requires Python 3.10+. Read the pytest section above first — installing is activating.
Where the code and the design differ
docs/design/ is normative. The code is being rebuilt to it
(D20) in
the build order; until that lands, the
code in src/ is the pre-rebuild shape and the design describes the target.
Plugin discovery, list merge semantics, YAML and hot reload are design intent
with no code behind them either way.
Development
uv run pytest -q
uv run mypy src
pre-commit run -a
License
MPL-2.0. See LICENSE.
Metadata
Release files for cot-config 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cot_config-0.1.0.tar.gz | 206.6 kB | With provenance | Details for cot_config-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cot_config-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for cot_config-0.1.0-py3-none-any.whl |
Total release size: 257.6 kB
Release files / cot_config-0.1.0.tar.gz
| Download URL | cot_config-0.1.0.tar.gz |
|---|---|
| Size | 206.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3188418742f17c62245a460b843eef6a4162cd6e00c43bd7b0557d69d13757e6
|
|
BLAKE2b-256 checksum How to use checksums |
3863e362f1ec831d313d327ed9e7226279f77174429c275f02e0caa0de1b5f51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / cot_config-0.1.0-py3-none-any.whl
| Download URL | cot_config-0.1.0-py3-none-any.whl |
|---|---|
| Size | 51.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8b9ba747232f24fb8703ecc0ea65d3ebe78fbcb080f9181c73b2dab448bb3855
|
|
BLAKE2b-256 checksum How to use checksums |
3a7d855b1bc37327a2b7c4199a0b606c75b62ce1835e159987d86c19ff0641c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logSecurity for cot-config 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for cot_config-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.