Metadata-Version: 2.4
Name: confumo
Version: 0.2.0
Summary: Platform-aware configuration management (CLI flags + env + YAML/JSON deep-merge)
Author: Aaron Colichia
License-Expression: 0BSD
Project-URL: Homepage, https://latebrum.com/git/actx/confumo/
Project-URL: Repository, https://latebrum.com/git/actx/confumo/
Keywords: config,argparse,yaml,env,confuse
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: confuse>=2.0
Requires-Dist: platformdirs>=3.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: typing-extensions>=4.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=6.0; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Dynamic: license-file

# Confumo

**Confumo** derives from "Confuse more". It's a thin Python module that merges
command-line flags, environment variables, and YAML/JSON files into a single,
deep-merged configuration object. The object is always a singleton per
application name and can be exposed at module scope so existing code keeps
working.

*One line to register your flags, one line to expose the config to the rest of
your project.*

## Highlights

| ✨ | Description |
|---|-------------|
| **Merged sources** | argparse • ENV vars • YAML/JSON • defaults |
| **Singleton registry** | Only *one* `ArgumentParser` run per `app_name`; mismatched config classes raise instead of silently reusing the wrong singleton |
| **Auto flag inheritance** | Base class + mix-ins + leaf class all show up in `--help` |
| **Lazy “print-once” help** | Libraries defer `--help`; root application prints merged help |
| **Module-level proxy** | `from my_app.config import log_level` still works; alias collisions raise unless explicitly replaced |
| **Deep-merge** | Powered by&nbsp;[confuse](https://github.com/beetbox/confuse) |
| **Cross-platform paths** | Uses&nbsp;[platformdirs](https://github.com/platformdirs/platformdirs) |

## Installation

```bash
pip install confumo
```

## 1 · Define a config class

```python
# ui_layer/config.py
import argparse
from confumo import Confumo

class UIConfig(Confumo):
    def add_args(self, p: argparse.ArgumentParser):
        p.add_argument('--theme', choices=['light', 'dark', 'auto'], default='auto')
        p.add_argument('--font', default='Roboto')

    def _init_subclass(self):
        self.theme = self.args.theme
        self.font = self.cfg['font'].get(str)

    def to_dict(self):
        return {'theme': self.theme, 'font': self.font}
```

## 2 · Expose it (library mode)

`UIConfig` is meant to be reused by many apps, so the library defers `--help`
printing:

```python
# ui_layer/config.py  (bottom of file)
config = Confumo.expose(
    'ui_layer',
    UIConfig,
    globals(),
    root=False,
)
```

Now any package can do:

```python
from ui_layer import config
print(config.theme)
```

and the singleton is shared process-wide.

## 3 · Extend it in an application

```python
# cool_app/config.py
import argparse
from confumo import Confumo
from ui_layer.config import UIConfig

class CoolAppConfig(UIConfig):
    def add_args(self, p: argparse.ArgumentParser):
        p.add_argument('--log_level', default='INFO',
                       choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
        p.add_argument('--library_path', default='~/Pictures')

    def _init_subclass(self):
        super()._init_subclass()
        self.log_level = self.cfg['log_level'].get(str)
        self.library_path = self.cfg['library_path'].get(str)

    def to_dict(self):
        base = super().to_dict()
        base.update({'log_level': self.log_level,
                     'library_path': self.library_path})
        return base

config = Confumo.expose(
    'cool_app',
    CoolAppConfig,
    globals(),
)
```

Running the application:

```bash
python cool_app/main.py --help
```

prints all flags exactly once.

`Confumo` collects `add_args()` methods across the config class MRO, so a
subclass does not have to call `super().add_args(p)` to inherit parent flags.
Calling `super().add_args(p)` is also safe: exact duplicate inherited argument
registrations are ignored, while real conflicting option definitions still raise
argparse errors.

## 4 · Access the config

```python
from cool_app import config
print(config.library_path)

from confumo import Confumo
cfg = Confumo.get('cool_app', CoolAppConfig)
```

Both lines refer to the same singleton.

## Safety checks

Confumo keys singleton instances by `app_name`, but it also validates the
requested concrete config class on registry hits. If `Confumo.get('demo',
DemoConfig)` created the singleton first, a later `Confumo.get('demo',
OtherConfig)` raises `ConfumoSingletonTypeError` instead of returning a config
object with the wrong shape. Requesting a base class remains valid when the
existing singleton is an instance of that base class.

`Confumo.expose(..., publish_to=(...))` also refuses to replace an existing
Confumo-managed module proxy by default. Publish targets that are already
imported are preflighted before the requested config singleton is constructed,
so an unsafe cross-package alias is rejected before the importing package can
run its own config bootstrap. This prevents one package from silently
repointing another package's `config`, `__getattr__`, and `__dir__` at an
unrelated singleton. Use `replace_existing=True` only for an intentional
compatibility shim where that replacement has been audited.

## Precedence order

1. **CLI flags** (`--foo bar`)
2. **Environment variables** (`COOL_APP__FOO=bar`)
3. **YAML file** (`-c settings.yml`) or default path
4. Defaults hard-coded in your subclass

`confuse` handles deep-merging dicts and lists.

## Persist the merged config

```python
path = config.save_yaml()  # ~/.config/cool_app/cool_app_config.yaml (Linux)
```

The file contains the fully merged state for later inspection.

## Testing helpers

`config.copy()` returns a detached clone whose built-in mutable containers and
argparse namespace state do not alias the singleton. Runtime objects that cannot
be copied, such as GUI widgets stored as leaf values, are preserved by reference.

## License

This repository ships under 0BSD; see `LICENSE`.

## Licensing / provenance notice

This repository may include AI-generated material and minimal human-authored
material. To the extent any portion of this repository is not copyrightable, it
is made available without restriction.

No third-party source or license texts are currently bundled in-tree. Any
third-party material added later remains subject to its original terms.
