> built 2026-09-22 14:46 UTC from 10e9005 (master) · config2py 0.1.54. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# config2py

Simplified reading and writing configurations from various sources and formats.

To install:	`pip install config2py`

[Documentation](https://i2mint.github.io/config2py/)

<!-- epythet:agentic-readme:start -->

## For AI agents

`config2py` ships no skills or subagents of its own — it’s the thing that reads
*your* agent’s config, not the other way around. If you’re an agent that needs to
fetch a value from an environment variable, a local file, or a user prompt without
three different codecs and a `configparser` incantation, this is your package.

**The documentation, machine-readable**: [`llms.txt`](https://i2mint.github.io/config2py/llms.txt) indexes every page; [`config2py.md`](https://i2mint.github.io/config2py/config2py.md) is the whole documentation in one file; every page has a `.md` twin; [`objects.inv`](https://i2mint.github.io/config2py/objects.inv) maps symbols to URLs.

If you are a control freak (human or otherwise), the rest of this README is written for you, starting at [The cherry on top: config_getter]().

<!-- epythet:agentic-readme:end -->

# The cherry on top: config_getter

```python
from config2py import config_getter
```

Let’s start with an extremely convenient, no questions asked, object.
Later, we’ll look under the hood to show the many tools that support it, and can be shaped to fit many desired behaviors.

What `config2py.config_getter(key)` will do is:

* search for `key` in your environment variables, and if not found…
* … search for it in a default local directory (more on that later), and if not found…
* … ask the user to enter the value that key should have, and then put it in the local directory mentioned above.

![image](https://github.com/i2mint/config2py/assets/1906276/09f287a8-05f9-4590-8664-10feda9ad617)

*Note: The “… ask the user to enter the value…” will be activated only when in an interactive environment (python console, jupyter notebook, etc.), as decided by the `config2py.is_repl()` function*

```python
config_getter("HOME")  # if you are using Linux/MacOS
# config_getter('USERPROFILE')  # if you are using Windows
```

```none
'/Users/thorwhalen'
```

Now, normally all systems come with a `HOME` environment variable (or a `USERPROFILE` on windows), so the above should always work fine.
But see what happens if you ask for a key that is not an environment variable:

```python
my_config_val = config_getter("_TEST_NON_EXISTING_KEY_")  # triggers a user input dialog
# ... I enter 'my config value' in the dialog, and then...
```

```python
my_config_val
```

```none
'my config value'
```

But if I do that again (even on a different day, somewhere else (on my same computer), in a different session), it will get me the value I entered in the user input dialog.

```python
my_config_val = config_getter(
    "_TEST_NON_EXISTING_KEY_"
)  # does not trigger input dialog
my_config_val
```

```none
'my config value'
```

And of course, we give you a means to delete that value, since `config_getter` has a `local_configs` mapping (think `dict`) to the local files where it has been stored.
You can do all the usual stuff you do with a `dict` (except the effects will be on local files),
like list the keys (with `list(.)`), get values for a key (with `.[key]`), ask for the number of keys (`len(.)`), and, well, delete stuff:

```python
if "_TEST_NON_EXISTING_KEY_" in config_getter.configs:
    del config_getter.configs["_TEST_NON_EXISTING_KEY_"]
```

This tool allows you to:

* not have to set up any special configs stuff (unless you want/need to)
* enables you to share your notebooks (CLIs etc.) with others without having to polute the code with configs-setup gunk…
* … including when you put local file/folder paths (or worse, secrets) in your notebook or code, which others then have to edit (instead, here, just enter a probably-unique name for the needed resource, then enter your filepath in the user input dialog instead)

This is very convenient situation where user input (via things like `__builtins__.input` or `getpass.getpass` etc) is available. But **you should not use this to manage configurations/resources anywhere were there’s not a user to see and respond to the builtin user input dialog**

Don’t fret though, this `config_getter` is just our no-BS entry point to much more.
Let’s have a slight look under its hood to see what else we can do with it.

And of course, if you’re that type, you can already have a look at [the documentation](https://i2mint.github.io/config2py/)

## `simple_config_getter`: Controlling your config_getter a bit more

If you look up for the definition of the `config_getter` function you imported above, you’ll find this: `config_getter = simple_config_getter()`.
That is, it was created by `simple_config_getter` with its default arguments.
Let’s have a look at what these are.

In fact, `simple_config_getter` is a function to make configuration getters that ressemble the one we’ve seen above:

![image](https://github.com/i2mint/config2py/assets/1906276/09f287a8-05f9-4590-8664-10feda9ad617)

But where you can control what the central store (by default a local configuration files store) is, and whether to first search in environment variables or not, and whether to ask the user for the value, if not found before, or not.

```python
from config2py import simple_config_getter, get_configs_local_store
from i2 import Sig

print(*str(Sig(simple_config_getter)).split(","), sep="\n")
```

```none
(configs_src: str = '.../.config/config2py/configs'
*
first_look_in_env_vars: bool = True
ask_user_if_key_not_found: bool = None
config_store_factory: Callable = <function get_configs_local_store at 0x10a457370>)
```

`first_look_in_env_vars` specifies whether to look into environment variables first, or not.

`ask_user_if_key_not_found` specifies whether to ask the user if a configuration key is not found. The default is `None`, which will result in checking if you’re running in an interactive environment or not.
When you use `config2py` in production though, you should definitely specify `ask_user_if_key_not_found=False` to make that choice explicit.

The `configs_src` default is automatically set to be the `config2py/configs` folder of your system’s config directory (following XDG standards on Unix/Linux/macOS). You can override this with environment variables like `CONFIG2PY_CONFIG_DIR`, `CONFIG2PY_DATA_DIR`, etc., or the standard XDG variables.

Your central store will be `config_store_factory(configs_src)`, and since you can also specify `config_store_factory`, you have total control over the store.

The default `config_store_factory` is `get_configs_local_store` which will give you a locally persisted store where if `configs_src`:

* is a directory, it’s assumed to be a folder of text files.
* is a file, it’s assumed to be an ini or cfg file.
* is a string, it’s assumed to be an app name, from which to create a config folder for with the default method

# Setting the config key search path

If you check out the code for `simple_config_getter`, you’ll find that all it it is simply setting the `sources` argument for the `get_config` function.
Something more or less like:

```python
configs = config_store_factory(configs_src)
source = [
    os.environ,  # search in environment variables first
    configs,  # then search in configs
    user_gettable(configs),  # if not found, ask the user and store in
]
config_getter = get_config(sources=source)
```

So you see that you can easily define your own sources for configs, and in what order they should be searched. If you don’t want that “ask the user for the value” thing, you can just remove the `user_gettable(local_configs)` part. If you wanted instead to add a place to look before the environment variables – say, you want to look in to local variables of the scope the config getter is **defined** (not called), you can stick `locals()` in front of the `os.environ`.

So you see that you can easily define your own sources for configs, and in what order they should be searched. If you don’t want that “ask the user for the value” thing, you can just remove the `user_gettable(local_configs)` part. If you wanted instead to add a place to look before the environment variables – say, you want to look in to local variables of the scope the config getter is **defined** (not called), you can stick `locals()` in front of the `os.environ`.

Let’s work through a custom-made `config_getter`.

```python
from config2py import get_config, user_gettable
from dol import TextFiles
import os

my_configs = TextFiles(
    "~/.my_configs/"
)  # Note, to run this, you'd need to have such a directory!
# (But you can also use my_configs = dict() if you want.)
config_getter = get_config(
    sources=[locals(), os.environ, my_configs, user_gettable(my_configs)]
)
```

Now let’s see what happens when we do:

```python
config_getter("SOME_CONFIG_KEY")
```

Well, it will first look in `locals()`, which is a dictionary containing local variables
where the `config_getter` was **defined** (careful – not called!!).
This is desirable sometimes when you define your `config_getter` in a module that has other python variables you’d like to use.

Assuming it doesn’t find such a key in `locals()` it goes on to try to find it in
`os.environ`, which is a dict containing system environment variables.

Assuming it doesn’t find it there either (that is, doesn’t find a file with that name in
the directory `~/.my_configs/`), it will prompt the user to enter the value of that key.
The function finally returns with the value that the user entered.

But there’s more!

Now look at what’s in `my_configs`!
If you’ve used `TextFiles`, look in the folder to see that there’s a new file.
Either way, if you do:

```python
my_configs["SOME_CONFIG_KEY"]
```

You’ll now see the value the user entered.

This means what? This means that the next time you try to get the config:

```python
config_getter("SOME_CONFIG_KEY")
```

It will return the value that the user entered last time, without prompting the
user again.

## SyncStore: Auto-Syncing Key-Value Stores

### Overview

`SyncStore` provides MutableMapping interfaces that automatically persist changes to backing storage. Changes sync immediately by default, or can be deferred using a context manager for efficient batch operations.

### Basic Usage

```python
from config2py.sync_store import FileStore, JsonStore

# Auto-detected from .json extension
config = FileStore("config.json")
config["api_key"] = "secret"  # Syncs immediately

# Batch operations (deferred sync)
with config:
    config["a"] = 1
    config["b"] = 2
    config["c"] = 3
    # Syncs once on exit
```

### Nested Sections

```python
# Work with specific section via key_path
db_config = FileStore("config.json", key_path="database")
db_config["host"] = "localhost"  # Only affects database section

# Dotted notation for deep nesting
items = FileStore("config.json", key_path="app.settings.items")
items["item1"] = "value"
```

### Supported Formats

Auto-detected by extension:

- `.json` - JSON (stdlib)
- `.ini`, `.cfg` - INI files (stdlib)
- `.yaml`, `.yml` - YAML (if PyYAML installed)
- `.toml` - TOML (if tomli/tomllib installed)

Register custom formats:

```python
from sync_store import register_extension

register_extension(".custom", my_loader, my_dumper)
store = FileStore("data.custom")
```

### Custom Backing Storage

```python
from config2py.sync_store import SyncStore


# Any backing storage via loader/dumper
def my_loader():
    return fetch_from_database()


def my_dumper(data):
    save_to_database(data)


store = SyncStore(my_loader, my_dumper)
store["key"] = "value"  # Calls my_dumper
```

### Key Classes

- **`SyncStore`** - Base class with loader/dumper functions
- **`FileStore`** - File-based with extension detection and key_path
- **`JsonStore`** - Explicit JSON with sensible defaults

# A few notable tools you can import from config2py

* `get_config`: Get a config value from a list of sources. See more below.
* `user_gettable`: Create a `GettableContainer` that asks the user for a value, optionally saving it.
* `ask_user_for_input`: Ask the user for input, optionally masking, validating and transforming the input.
* `get_app_folder`: Returns the full path of a directory suitable for storing application-specific data for a given app name and folder kind (config, data, cache, state, runtime).
* `get_app_config_folder`: Specialized version of `get_app_folder` for configuration files.
* `get_app_data_folder`: Specialized version of `get_app_folder` for application data.
* `get_configs_local_store`: Get a local store (mapping interface of local files) of configs for a given app or package name
* `configs`: A default store instance for configs, defaulting to a local store under a default configuration local directory.

## get_config

Get a config value from a list of sources.

This function acts as a mini-framework to construct config accessors including defining
multiple sources of where to find these configs,

A source can be a function or a `GettableContainer`.
(A `GettableContainer` is anything that can be indexed with brackets: `obj[k]`,
like `dict`, `list`, `str`, etc..).

Let’s take two sources: a `dict` and a `Callable`.

```none
>>> def func(k):
...     if k == 'foo':
...         return 'quux'
...     elif k == 'green':
...         return 'eggs'
...     else:
...         raise RuntimeError(f"I don't handle that: {k}")
>>> dict_ = {'foo': 'bar', 'baz': 'qux'}
>>> sources = [func, dict_]
```

See that `get_config` go through the sources in the order they were listed,
and returns the first value it finds (or manages to compute) for the key:

`get_config` finds `'foo'` in the very first source (`func`):

```none
>>> get_config('foo', sources)
'quux'
```

But `baz` makes `func` raise an error, so it goes to the next source: `dict_`.
There, it finds `'baz'` and returns its value:

```none
>>> get_config('baz', sources)
'qux'
```

On the other hand, no one manages to find a config value for `'no_a_key'`, so
`get_config` raises an error:

```none
>>> get_config('no_a_key', sources)
Traceback (most recent call last):
...
config2py.errors.ConfigNotFound: Could not find config for key: no_a_key
```

But if you provide a default value, it will return that instead:

```none
>>> get_config('no_a_key', sources, default='default')
'default'
```

You can also provide a function that will be called on the value before it is
returned. This is useful if you want to do some post-processing on the value,
or if you want to make sure that the value is of a certain type:

This “search the next source if the previous one fails” behavior may not be what
you want in some situations, since you’d be hiding some errors that you might
want to be aware of. This is why allow you to specify what exceptions should
actually be considered as “config not found” exceptions, through the
`config_not_found_exceptions` argument, which defaults to `Exception`.

Further, your sources may return a value, but not one that you consider valid:
For example, a sentinel like `None`. In this case you may want the search to
continue. This is what the `val_is_valid` argument is for. It is a function
that takes a value and returns a boolean. If it returns `False`, the search
will continue. If it returns `True`, the search will stop and the value will
be returned.

Finally, we have `egress : Callable[[KT, TT], VT]`.
This is a function that takes a key and a value, and
returns a value. It is called after the value has been found, and its return
value is the one that is returned by `get_config`. This is useful if you want
to do some post-processing on the value, or before you return the value, or if you
want to do some caching.

```none
>>> config_store = dict()
>>> def store_before_returning(k, v):
...    config_store[k] = v
...    return v
>>> get_config('foo', sources, egress=store_before_returning)
'quux'
>>> config_store
{'foo': 'quux'}

Note that a source can be a callable or a ``GettableContainer`` (most of the
time, a ``Mapping`` (e.g. ``dict``)).
Here, you should be compelled to use the resources of ``dol``
(https://pypi.org/project/dol/) which will allow you to make ``Mapping``s for all
sorts of data sources.
```

For more info, see: https://github.com/i2mint/config2py/issues/4

# user_gettable

So, what’s that `user_gettable`?

It’s a way for you to specify that the system should ask the user for a key, and optionally save it somewhere, plus many other parameters (like what to ask the user, etc.)

```python
from config2py.base import user_gettable

s = user_gettable()
s["SOME_KEY"]
# will trigger a prompt for the user to enter the value of SOME_KEY
# ... and when they do (say they entered 'SOME_VAL') it will return that value

# And if you specify a save_to store (usually a persistent MutableMapping made with the dol package)
# then it will save the value to that store for future use
d = dict(some="store")
s = user_gettable(save_to=d)
s["SOME_KEY"]
```

More on that another day…

<p class="epythet-aggregates">This documentation as a single file: <a href="config2py.md">config2py.md</a> (Markdown, for agents).</p>


# _autosummary/config2py.base.html.md

# config2py.base

Base for getting configs from various sources and formats

### Functions

| [`ask_user_for_key`](_autosummary/config2py.base.html.md#config2py.base.ask_user_for_key)([key, prompt_template, ...])     | Ask the user for the value of `key`, optionally saving it.                         |
|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|
| [`get_config`](_autosummary/config2py.base.html.md#config2py.base.get_config)([key, sources, default, egress, ...])  | Get a config value from a list of sources                                          |
| [`gettable_containers`](_autosummary/config2py.base.html.md#config2py.base.gettable_containers)(sources[, val_is_valid, ...]) | Convert an iterable of sources into `GettableContainers`                           |
| [`is_not_empty`](_autosummary/config2py.base.html.md#config2py.base.is_not_empty)(val)                                 | True unless `val` is `None` or the empty string.                                   |
| [`is_not_none_nor_empty`](_autosummary/config2py.base.html.md#config2py.base.is_not_none_nor_empty)(x)                          | True unless `x` is `None` or the empty string.                                     |
| [`sources_chainmap`](_autosummary/config2py.base.html.md#config2py.base.sources_chainmap)(sources[, val_is_valid, ...])    | Create a `ChainMap` from a list of sources                                         |
| [`user_gettable`](_autosummary/config2py.base.html.md#config2py.base.user_gettable)([save_to, prompt_template, ...])    | Create a `GettableContainer` that asks the user for a value, optionally saving it. |

### Classes

| [`FuncBasedGettableContainer`](_autosummary/config2py.base.html.md#config2py.base.FuncBasedGettableContainer)(getter[, ...])   | A class that wraps a `Callable[[KT], VT]` function so it has a (partial) Mapping[KT, TT] interface.   |
|----------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|
| [`GettableContainer`](_autosummary/config2py.base.html.md#config2py.base.GettableContainer)(\*args, \*\*kwargs)       | `Containers` that are "gettable"".                                                                    |

### *class* config2py.base.FuncBasedGettableContainer(getter, val_is_valid=<function always_true>, config_not_found_exceptions=(<class 'Exception'>, ))

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A class that wraps a `Callable[[KT], VT]` function so it has a (partial)
Mapping[KT, TT] interface. It is “partial” in the sense that it only implements
`__getitem__`, raise a `KeyError` when a key can’t be computed.
This is the standard for `Mapping` types, which enables us to use the
`FuncBasedGettable` in a `collections.ChainMap` to catch the error and move on
to the next source.

```pycon
>>> def getter(k):
...     if k == 'foo':
...         return 'quux'
...     elif k == 'green':
...         return 'eggs'
...     else:
...         raise RuntimeError(f"I don't handle that: {k}")
>>> gc = FuncBasedGettableContainer(getter)
>>> gc['foo']
'quux'
>>> gc['green']
'eggs'
```

Observe below that though the `getter` function raises a `RuntimeError`, the
`FuncBasedGettableContainer` raises a `KeyError`, to conform to the
`Mapping` protocol.

```pycon
>>> gc['no_a_key']
Traceback (most recent call last):
...
KeyError: 'no_a_key'
```

The `KeyError` message is just the key: neither the upstream exception text nor
the rejected value is interpolated into it, since getters commonly wrap credential
checks and these errors commonly end up in logs. The upstream exception is still
available, through the standard exception chain:

```pycon
>>> try:
...     gc['no_a_key']
... except KeyError as e:
...     print(type(e.__cause__).__name__, e.__cause__, sep=': ')
RuntimeError: I don't handle that: no_a_key
```

Note that by default, `FuncBasedGettableContainer` will catch all `Exception`
exceptions, but you can specify a different set of exceptions to catch.

Note as well that you can specify a `val_is_valid` function that will be used to
check the value returned by the `getter` function. If the value is not valid, a
`KeyError` will also be raised.
This is useful, for example, when you have a function that returns a sentinel like
`None` instead of raising an exception, but you want to treat that as a
`KeyError`.

```pycon
>>> def getter(k):
...     if k == 'foo':
...         return 'quux'
...     elif k == 'green':
...         return 'eggs'
...     else:
...         return None
>>> gc = FuncBasedGettableContainer(getter, val_is_valid=lambda x: x is not None)
>>> gc['foo']
'quux'
>>> gc['no_a_key']
Traceback (most recent call last):
...
KeyError: 'no_a_key'
```

#### val_is_valid()

Function that just returns True.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### *class* config2py.base.GettableContainer(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

`Containers` that are “gettable””.

By “gettable”, we mean that we can fetch an element from `obj` with brackets:
`obj[k]`. That is, `obj` has a `__getitem__` method.
A `Container` means that `obj` has a `__contains__` method, i.e. the
expression `k in obj` is valid.

```pycon
>>> isinstance(3, GettableContainer)  # 3 is not Gettable (can't do 3[...])
False
```

But `dict`, `list`, and `str` are GettableContainer:

```pycon
>>> isinstance([1, 2, 3], GettableContainer)
True
>>> isinstance({'foo': 'bar'}, GettableContainer)
True
>>> isinstance('foo', GettableContainer)
True
```

Note that so are their types:

```pycon
>>> all(isinstance(c, GettableContainer) for c in (list, dict, str))
True
```

But `set` is not a `GettableContainer`.

```pycon
>>> myset = {1, 2, 3}
>>> isinstance(myset, GettableContainer)
False
```

This is because a `set` is a `Container`, but it is not gettable:

```pycon
>>> 4 in myset  # set is a container
False
>>> myset[4]  # ... but not gettable
Traceback (most recent call last):
...
TypeError: 'set' object is not subscriptable
```

### config2py.base.ask_user_for_key(key=None, \*, prompt_template='Enter a value for {}: ', save_to=None, save_condition=<function is_not_empty>, user_asker=<function ask_user_for_input>, egress=None)

Ask the user for the value of `key`, optionally saving it.

* **Parameters:**
  * **key** – The key to ask the user for. If `None`, a “curried” version of
    `ask_user_for_key` is returned, so you can specify the key later.
  * **prompt_template** – A template string to prompt the user with. It should
    contain a placeholder for the key, e.g. `"Enter a value for {}: "`.
  * **save_to** (`Union`[[`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Where to save the user’s response: a `MutableMapping` (or
    anything with a `__setitem__`), or a `(key, value)` saver function.
    If `None`, the response is not saved. See `_resolve_saver`.
  * **save_condition** – A function of the value, deciding whether to save it.
  * **user_asker** – A function that takes a prompt string and returns the user’s
    response.
  * **egress** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A `(key, value)` function to apply to the user’s response before
    returning (and saving) it.

The value can be saved to any `MutableMapping`:

```pycon
>>> store = {}
>>> ask_user_for_key('some_key', save_to=store, user_asker=lambda prompt: 'val')
'val'
>>> store
{'some_key': 'val'}
```

… or to a `(key, value)` function, when saving isn’t a simple write:

```pycon
>>> saved = []
>>> ask_user_for_key(
...     'some_key',
...     save_to=lambda k, v: saved.append((k, v)),
...     user_asker=lambda prompt: 'val',
... )
'val'
>>> saved
[('some_key', 'val')]
```

### config2py.base.get_config(key=None, sources=None, \*, default=Sentinel('no_default'), egress=None, val_is_valid=<function always_true>, config_not_found_exceptions=(<class 'Exception'>, ))

Get a config value from a list of sources

A source can be a function or a `GettableContainer`.
(A `GettableContainer` is anything that can be indexed with brackets: `obj[k]`,
like `dict`, `list`, `str`, etc..).

Let’s take two sources: a `dict` and a `Callable`.

```pycon
>>> def func(k):
...     if k == 'foo':
...         return 'quux'
...     elif k == 'green':
...         return 'eggs'
...     else:
...         raise RuntimeError(f"I don't handle that: {k}")
>>> dict_ = {'foo': 'bar', 'baz': 'qux'}
>>> sources = [func, dict_]
```

See that `get_config` go through the sources in the order they were listed,
and returns the first value it finds (or manages to compute) for the key:

`get_config` finds `'foo'` in the very first source (`func`):

```pycon
>>> get_config('foo', sources)
'quux'
```

But `baz` makes `func` raise an error, so it goes to the next source: `dict_`.
There, it finds `'baz'` and returns its value:

```pycon
>>> get_config('baz', sources)
'qux'
```

On the other hand, no one manages to find a config value for `'no_a_key'`, so
`get_config` raises an error:

```pycon
>>> get_config('no_a_key', sources)
Traceback (most recent call last):
...
config2py.errors.ConfigNotFound: Could not find config for key: no_a_key
```

But if you provide a default value, it will return that instead:

```pycon
>>> get_config('no_a_key', sources, default='default')
'default'
```

You can also provide a function that will be called on the value before it is
returned. This is useful if you want to do some post-processing on the value,
or if you want to make sure that the value is of a certain type:

This “search the next source if the previous one fails” behavior may not be what
you want in some situations, since you’d be hiding some errors that you might
want to be aware of. This is why allow you to specify what exceptions should
actually be considered as “config not found” exceptions, through the
`config_not_found_exceptions` argument, which defaults to `Exception`.

Further, your sources may return a value, but not one that you consider valid:
For example, a sentinel like `None`. In this case you may want the search to
continue. This is what the `val_is_valid` argument is for. It is a function
that takes a value and returns a boolean. If it returns `False`, the search
will continue. If it returns `True`, the search will stop and the value will
be returned.

Finally, we have `egress : Callable[[KT, TT], VT]`.
This is a function that takes a key and a value, and
returns a value. It is called after the value has been found, and its return
value is the one that is returned by `get_config`. This is useful if you want
to do some post-processing on the value, or before you return the value, or if you
want to do some caching.

```pycon
>>> config_store = dict()
>>> def store_before_returning(k, v):
...    config_store[k] = v
...    return v
>>> get_config('foo', sources, egress=store_before_returning)
'quux'
>>> config_store
{'foo': 'quux'}
```

Note that a source can be a callable or a `GettableContainer` (most of the
time, a `Mapping` (e.g. `dict`)).
Here, you should be compelled to use the resources of `dol`
([https://pypi.org/project/dol/](https://pypi.org/project/dol/)) which will allow you to make `Mapping` objects
for all sorts of data sources.

For more info, see: [https://github.com/i2mint/config2py/issues/4](https://github.com/i2mint/config2py/issues/4)

### config2py.base.gettable_containers(sources, val_is_valid=<function always_true>, config_not_found_exceptions=(<class 'Exception'>, ))

Convert an iterable of sources into `GettableContainers`

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable)[[`GettableContainer`](_autosummary/config2py.base.html.md#config2py.base.GettableContainer)]

### config2py.base.is_not_empty(val)

True unless `val` is `None` or the empty string.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> is_not_empty(None)
False
>>> is_not_empty('')
False
>>> is_not_empty('a')
True
>>> is_not_empty(0)
True
```

### config2py.base.is_not_none_nor_empty(x)

True unless `x` is `None` or the empty string.

```pycon
>>> is_not_none_nor_empty(None)
False
>>> is_not_none_nor_empty('')
False
>>> is_not_none_nor_empty('a')
True
>>> is_not_none_nor_empty(0)
True
```

### config2py.base.sources_chainmap(sources, val_is_valid=<function always_true>, config_not_found_exceptions=(<class 'Exception'>, ))

Create a `ChainMap` from a list of sources

* **Return type:**
  [`ChainMap`](https://docs.python.org/3/library/collections.html#collections.ChainMap)

### config2py.base.user_gettable(save_to=None, \*, prompt_template='Enter a value for {}: ', egress=None, user_asker=<function ask_user_for_input>, val_is_valid=<function is_not_empty>, config_not_found_exceptions=(<class 'Exception'>, ))

Create a `GettableContainer` that asks the user for a value, optionally saving it.

* **Parameters:**
  * **save_to** (`Union`[[`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping), [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`KT`), [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Where to save the user’s response: a `MutableMapping` (or
    anything with a `__setitem__`), or a `(key, value)` saver function.
    If `None`, the user’s response is not saved.
  * **prompt_template** – A template string to prompt the user with. It should
    contain a placeholder for the key, e.g. `"Enter a value for {}: "`.
  * **egress** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – A function to apply to the user’s response before returning it.
    This can be used to validate the response, for example.
  * **user_asker** – A function that asks the user for input. It should take a
    prompt string and return the user’s response.
  * **val_is_valid** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`VT`)], [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]) – A function that takes a value and returns a boolean. If it
    returns `False`, the user will be asked for a new value.
  * **config_not_found_exceptions** ([`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`type`](https://docs.python.org/3/builtins/functions.html#type)[[`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)], [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]) – An iterable of exceptions that should be
    considered as “config not found” exceptions. If the user’s response
    raises one of these exceptions, the user will be asked for a new value.
* **Returns:**
  A `GettableContainer` that asks the user for a value, optionally saving it.

### Example

```pycon
>>> s = user_gettable()
>>> v = s['SOME_KEY']
'SOME_VAL'
```

This will trigger a prompt for the user to enter the value of `SOME_KEY`.
When they do (say they entered ‘SOME_VAL’) it will return that value.

And if you specify a save_to store (usually a persistent MutableMapping made
with the `dol` package) then it will save the value to that store for
future use.

```pycon
>>> d = dict(some='store')
>>> s = user_gettable(save_to=d)
>>> s['SOME_KEY']
'SOME_VAL'
>>> d
{'some': 'store', 'SOME_KEY': 'SOME_VAL'}
```

When saving isn’t a simple write (say you need to encrypt, or write to two
places), `save_to` can be a `(key, value)` function instead:

```pycon
>>> saved = []
>>> s = user_gettable(
...     save_to=lambda k, v: saved.append((k, v)),
...     user_asker=lambda prompt: 'SOME_VAL',
... )
>>> s['SOME_KEY']
'SOME_VAL'
>>> saved
[('SOME_KEY', 'SOME_VAL')]
```


# _autosummary/config2py.codecs.html.md

# config2py.codecs

Extension-based codec registries for configuration file parsing.

This module provides a flexible pattern for encoding and decoding configuration files
based on their file extensions. It includes codecs for bytes <-> JSON-friendly Python types.

### Examples

```pycon
>>> # Basic usage
>>> data = {'name': 'config2py', 'version': '1.0'}
>>>
>>> # Encode to bytes
>>> encoded = encode_by_extension('config.json', data)
>>> assert isinstance(encoded, bytes)
>>>
>>> # Decode from bytes
>>> decoded = decode_by_extension('config.json', encoded)
>>> assert decoded == data
>>>
>>> # Register custom codec
>>> @register_decoder('.custom')
... def decode_custom(data: bytes) -> dict:
...     return {'custom': data.decode()}
>>>
>>> @register_encoder('.custom')
... def encode_custom(obj: dict) -> bytes:
...     return obj.get('custom', '').encode()
```

The module automatically registers codecs for standard formats (json, toml, ini, etc.)
and conditionally registers codecs that require third-party libraries (yaml, json5, etc.).

### Functions

| [`decode_by_extension`](_autosummary/config2py.codecs.html.md#config2py.codecs.decode_by_extension)(key, data)                | Decode data based on key's extension.                |
|------------------------------------------------------------------------------------------------|------------------------------------------------------|
| [`encode_by_extension`](_autosummary/config2py.codecs.html.md#config2py.codecs.encode_by_extension)(key, obj)                 | Encode object based on key's extension.              |
| [`get_extension`](_autosummary/config2py.codecs.html.md#config2py.codecs.get_extension)(key)                            | Extract extension from a key (filename, path, etc.). |
| [`register_codec`](_autosummary/config2py.codecs.html.md#config2py.codecs.register_codec)(extension, \*[, encoder, ...]) | Register encoder and/or decoder for an extension.    |
| [`register_decoder`](_autosummary/config2py.codecs.html.md#config2py.codecs.register_decoder)(extension, \*[, overwrite])  | Decorator to register a decoder function.            |
| [`register_encoder`](_autosummary/config2py.codecs.html.md#config2py.codecs.register_encoder)(extension, \*[, overwrite])  | Decorator to register an encoder function.           |
| [`list_registered_extensions`](_autosummary/config2py.codecs.html.md#config2py.codecs.list_registered_extensions)()                  | List all registered extensions.                      |
| [`is_extension_registered`](_autosummary/config2py.codecs.html.md#config2py.codecs.is_extension_registered)(extension)            | Check if an extension has any codec registered.      |
| [`get_codec_info`](_autosummary/config2py.codecs.html.md#config2py.codecs.get_codec_info)(extension)                     | Get information about a registered codec.            |

### config2py.codecs.decode_by_extension(key, data)

Decode data based on key’s extension.

* **Parameters:**
  * **key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Key or filename with extension
  * **data** ([`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)) – Bytes to decode
* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)
* **Returns:**
  Decoded Python object
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If no decoder registered for extension

### Examples

```pycon
>>> data = b'{"key": "value"}'
>>> decode_by_extension('config.json', data)
{'key': 'value'}
```

### config2py.codecs.encode_by_extension(key, obj)

Encode object based on key’s extension.

* **Parameters:**
  * **key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Key or filename with extension
  * **obj** ([`Any`](https://docs.python.org/3/library/typing.html#typing.Any)) – Python object to encode
* **Return type:**
  [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)
* **Returns:**
  Encoded bytes
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If no encoder registered for extension

### Examples

```pycon
>>> obj = {'key': 'value'}
>>> encoded = encode_by_extension('config.json', obj)
>>> assert b'"key"' in encoded
```

### config2py.codecs.get_codec_info(extension)

Get information about a registered codec.

* **Parameters:**
  **extension** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File extension (with or without leading dot)
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]
* **Returns:**
  Dictionary with codec information

### Examples

```pycon
>>> info = get_codec_info('.json')
>>> info['has_encoder']
True
>>> info['has_decoder']
True
```

### config2py.codecs.get_extension(key)

Extract extension from a key (filename, path, etc.).

* **Parameters:**
  **key** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – A string that may contain a file extension
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  Extension without the dot, or empty string if no extension found

### Examples

```pycon
>>> get_extension('config.json')
'json'
>>> get_extension('/path/to/data.yaml')
'yaml'
>>> get_extension('no_extension')
''
>>> get_extension('.env')
'env'
>>> get_extension('/path/to/.env')
'env'
```

### config2py.codecs.is_extension_registered(extension)

Check if an extension has any codec registered.

* **Parameters:**
  **extension** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File extension (with or without leading dot)
* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)
* **Returns:**
  True if decoder or encoder is registered

### Examples

```pycon
>>> is_extension_registered('.json')
True
>>> is_extension_registered('.nonexistent')
False
```

### config2py.codecs.list_registered_extensions()

List all registered extensions.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]
* **Returns:**
  Sorted list of registered extensions

### Examples

```pycon
>>> extensions = list_registered_extensions()
>>> '.json' in extensions
True
```

### config2py.codecs.register_codec(extension, , encoder=None, decoder=None, overwrite=False, dependency=None)

Register encoder and/or decoder for an extension.

* **Parameters:**
  * **extension** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File extension (with or without leading dot)
  * **encoder** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`Any`](https://docs.python.org/3/library/typing.html#typing.Any)], [`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)]]) – Function to encode objects to bytes
  * **decoder** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`bytes`](https://docs.python.org/3/builtins/stdtypes.html#bytes)], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Function to decode bytes to objects
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to overwrite existing codec
  * **dependency** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – Optional package name required for this codec
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If codec already registered and overwrite=False

### Examples

```pycon
>>> def my_encoder(obj): return str(obj).encode()
>>> def my_decoder(data): return eval(data.decode())
>>> register_codec('.custom', encoder=my_encoder, decoder=my_decoder, overwrite=True)
```

### config2py.codecs.register_decoder(extension, , overwrite=False)

Decorator to register a decoder function.

* **Parameters:**
  * **extension** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File extension (with or without leading dot)
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to overwrite existing decoder
* **Returns:**
  Decorator function

### Examples

```pycon
>>> @register_decoder('.custom', overwrite=True)
... def decode_custom(data: bytes) -> dict:
...     return {'data': data.decode()}
```

### config2py.codecs.register_encoder(extension, , overwrite=False)

Decorator to register an encoder function.

* **Parameters:**
  * **extension** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File extension (with or without leading dot)
  * **overwrite** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to overwrite existing encoder
* **Returns:**
  Decorator function

### Examples

```pycon
>>> @register_encoder('.custom', overwrite=True)
... def encode_custom(obj: dict) -> bytes:
...     return obj.get('data', '').encode()
```


# _autosummary/config2py.errors.html.md

# config2py.errors

Error classes for config2py.

### Exceptions

| [`Config2PyError`](_autosummary/config2py.errors.html.md#config2py.errors.Config2PyError)   | Base class for config2py errors.        |
|-------------------------------------------------------------------|-----------------------------------------|
| [`ConfigNotFound`](_autosummary/config2py.errors.html.md#config2py.errors.ConfigNotFound)   | Raised when a config file is not found. |

### *exception* config2py.errors.Config2PyError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Base class for config2py errors.

### *exception* config2py.errors.ConfigNotFound

Bases: [`Config2PyError`](_autosummary/config2py.errors.html.md#config2py.errors.Config2PyError)

Raised when a config file is not found.


# _autosummary/config2py.html.md

# config2py

Tools to read and write configurations from various sources and formats

### Modules

| [`base`](_autosummary/config2py.base.html.md#module-config2py.base)                     | Base for getting configs from various sources and formats        |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------|
| [`codecs`](_autosummary/config2py.codecs.html.md#module-config2py.codecs)                 | Extension-based codec registries for configuration file parsing. |
| [`errors`](_autosummary/config2py.errors.html.md#module-config2py.errors)                 | Error classes for config2py.                                     |
| [`s_configparser`](_autosummary/config2py.s_configparser.html.md#module-config2py.s_configparser) | Data Object Layer for configparser standard lib.                 |
| [`sync_store`](_autosummary/config2py.sync_store.html.md#module-config2py.sync_store)         | Synchronized key-value stores with automatic persistence.        |
| [`tools`](_autosummary/config2py.tools.html.md#module-config2py.tools)                   | Various tools                                                    |
| [`util`](_autosummary/config2py.util.html.md#module-config2py.util)                     | Utility functions for config2py.                                 |


# _autosummary/config2py.s_configparser.html.md

# config2py.s_configparser

Data Object Layer for configparser standard lib.

### Functions

| [`persist_after_operation`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.persist_after_operation)(method_func)      | Wrap a mutating method so it calls `self.persist()` after running.                                            |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| [`postprocess_ini_section_items`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.postprocess_ini_section_items)(items)      | Transform newline-separated string values into actual list of strings (assuming that intent)                  |
| [`preprocess_ini_section_items`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.preprocess_ini_section_items)(items)       | Transform list values into newline-separated strings, in view of writing the value to a ini formatted section |
| [`super_and_persist`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.super_and_persist)(super_cls, method_name) | To be able to do this:                                                                                        |

### Classes

| [`ConfigReader`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.ConfigReader)([defaults, dict_type, ...])   | A KvReader to read config files                  |
|---------------------------------------------------------------------------------------------|--------------------------------------------------|
| [`ConfigStore`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.ConfigStore)([defaults, dict_type, ...])    | Persister (read, write, delete) for ini configs. |

### *class* config2py.s_configparser.ConfigReader(defaults=None, dict_type=<class 'dict'>, allow_no_value=False, \*, delimiters=('=', ': '), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section='DEFAULT', interpolation=<object object>, converters=<object object>)

Bases: [`ConfigStore`](_autosummary/config2py.s_configparser.html.md#config2py.s_configparser.ConfigStore)

A KvReader to read config files

```pycon
>>> from config2py.s_configparser import ConfigReader
>>>
>>> # from a (pretend) file
>>> from io import BytesIO, StringIO
>>> file_content_bytes = b'''
... [Paths]
... home_dir: /Users
... my_dir: %(home_dir)s/lumberjack
... my_pictures: %(my_dir)s/Pictures
...
... [Escape]
... gain: 80%%  # use a %% to escape the % sign (% is the only character that needs to be escaped)'''
>>> c = ConfigReader(file_content_bytes)  # get configs from the bytes
>>> list(c)
['DEFAULT', 'Paths', 'Escape']
>>> ######## From a (pretend) file (pointer) ########
>>> # Usually, you write your configs in a file and give ConfigReader the filepath, or open file pointer...
>>> pretend_file_pointer = StringIO(file_content_bytes.decode())
>>> c = ConfigReader(pretend_file_pointer)
>>> list(c)
['DEFAULT', 'Paths', 'Escape']
>>> c['Paths']  # gives you a configparser.Section object
<Section: Paths>
>>> # A configparser.Section is a mapping. Let's see the keys
>>> list(c['Paths'])
['home_dir', 'my_dir', 'my_pictures']
```

# >>> # here’s a quick way to see both keys and values. Note how the home_dir interpolation was performed!
# >>> dict(c[‘Paths’])
# {‘home_dir’: ‘/Users’, ‘my_dir’: ‘/Users/lumberjack’, ‘my_pictures’: ‘/Users/lumberjack/Pictures’}

```pycon
>>>
>>> ######## Get configs from a dict ########
>>> config_dict = {'section1': {'key1': 'value1'},
...                'section2': {'keyA': 'valueA', 'keyB': 'valueB'},
...                'section3': {'foo': 'x', 'bar': 'y','baz': 'z'}}
>>> c = ConfigReader(config_dict)
>>>
>>> assert list(c) == ['DEFAULT', 'section1', 'section2', 'section3']
>>> assert list(c['section3']) == ['foo', 'bar', 'baz']
>>>
>>> ######## Get configs from a string ########
>>> from config2py.s_configparser import _test_config_str
>>> c = ConfigReader(_test_config_str, allow_no_value=True)
>>> list(c)
['DEFAULT', 'Simple Values', 'All Values Are Strings', 'Multiline Values', 'No Values', 'You can use comments', 'Sections Can Be Indented']
>>> list(c['Simple Values'])
['key', 'spaces in keys', 'spaces in values', 'spaces around the delimiter', 'you can also use']
```

#### persist()

Disabled: `ConfigReader` is read-only.

### *class* config2py.s_configparser.ConfigStore(defaults=None, dict_type=<class 'dict'>, allow_no_value=False, \*, delimiters=('=', ': '), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section='DEFAULT', interpolation=<object object>, converters=<object object>)

Bases: `Store`

Persister (read, write, delete) for ini configs.

You can read ini formated configurations with ConfigStore (though if you want to
just read, you should use ConfigReader instead – since ConfigReader disables
write and delete operations.

See ConfigReader for more examples of how to use ConfigStore.
We’ll mainly focus on write and delete operations here.

```pycon
>>> import os
>>> from config2py.s_configparser import ConfigStore, ConfigReader
>>> import tempfile
>>> temp_dir_path = tempfile.TemporaryDirectory().name
>>> if not os.path.exists(temp_dir_path):
...     os.makedirs(temp_dir_path)
>>> ini_filepath = os.path.join(temp_dir_path, 'config_store_test.ini')
>>> if os.path.isfile(ini_filepath):
...     os.remove(ini_filepath)
>>>
>>> os.path.isfile(ini_filepath)  # File doesn't exist
False
>>>
>>> s = ConfigStore(ini_filepath)
>>> list(s)  # There's always a default (by default empty)
['DEFAULT']
>>>
>>> os.path.isfile(ini_filepath)  # But the file still doesn't exist (the DEFAULT is virtual)
False
>>>
>>> # Now let's make a config
>>> s['nothing'] = {'special': 'about', 'number': 42}
>>> list(s)
['DEFAULT', 'nothing']
>>>
>>> os.path.isfile(ini_filepath)  # But NOW the file exists (ConfigStore will automatically write to file)
True
>>> s['add'] = {'more': 'sections'}
>>> list(s)
['DEFAULT', 'nothing', 'add']
```

```pycon
>>> # and yes, that config can now be read
>>> config_reader = ConfigReader(ini_filepath)
>>> list(config_reader)
['DEFAULT', 'nothing', 'add']
>>>
>>> config_reader['nothing']
<Section: nothing>
>>>
>>> dict(config_reader['nothing'])  # note that 42 is now a string (that's the ini format for you!)
{'special': 'about', 'number': '42'}
>>> dict(config_reader['DEFAULT'])  # and DEFAULT is empty
{}
```

You can delete sections

```pycon
>>> del s['add']
```

But you’ll need to refresh your reader to see the effect.

```pycon
>>> list(config_reader)
['DEFAULT', 'nothing', 'add']
>>> config_reader = ConfigReader(ini_filepath)
>>> list(config_reader)
['DEFAULT', 'nothing']
```

You can use `update` to write several sections at the same time.
Note that existing sections will be completely overwritten.

```pycon
>>> s.update({'nothing': {'like': 'you'}, 'new_section': {'a': 'b', 'c': 'd'}})
>>> ConfigReader(ini_filepath).to_dict()
{'DEFAULT': {}, 'nothing': {'like': 'you'}, 'new_section': {'a': 'b', 'c': 'd'}}
```

**Warning: On the other hand, updating a section will not persist the updates**

Updates are automatically persisted at the top level, as shown in the example above.
This means you can change a section entirely, but partial updates of a section
will not be persisted.

You’ll see the updated section in the store.

```pycon
>>> s['nothing'].update({'something': 'else'})
>>> dict(s['nothing'])
{'like': 'you', 'something': 'else'}
```

But it’s not automatically persisted

```pycon
>>> dict(ConfigReader(ini_filepath)['nothing'])
{'like': 'you'}
```

… unless you ask for it explicitly

```pycon
>>> s.persist()
>>> dict(ConfigReader(ini_filepath)['nothing'])
{'like': 'you', 'something': 'else'}
```

# TODO: Could make section updates auto-persistent by wrapping configparser.SectionProxy

For your convenience, the ConfigStore is also a context manager, that will,
you guessed, persist stuff when (and only when) you exit it.

```pycon
>>> ConfigReader(ini_filepath).to_dict()
{'DEFAULT': {}, 'nothing': {'like': 'you', 'something': 'else'}, 'new_section': {'a': 'b', 'c': 'd'}}
>>> with ConfigStore(ini_filepath) as s:
...     del s['new_section']  # that's usually immediately persisted. This time, it'll wait to be
...     del s['nothing']['something']  # delete the something field of nothing section
...     s['nothing'].update({'like': 'that', 'ever': 'happened'})  # update 'like' config and add an 'ever' one
>>> ConfigReader(ini_filepath).to_dict()
{'DEFAULT': {}, 'nothing': {'like': 'that', 'ever': 'happened'}}
```

#### *class* BasicInterpolation

Bases: `Interpolation`

Interpolation as implemented in the classic ConfigParser.

The option values can contain format strings which refer to other values in
the same section, or values in the special default section.

For example:

> something: %(dir)s/whatever

would resolve the “%(dir)s” to the value of dir.  All reference
expansions are done late, on demand. If a user needs to use a bare % in
a configuration file, she can escape it by writing %%. Other % usage
is considered a user error and raises `InterpolationSyntaxError`.

#### *class* ExtendedInterpolation

Bases: `Interpolation`

Advanced variant of interpolation, supports the syntax used by
`zc.buildout`. Enables interpolation between sections.

#### get(k, default=None)

Needed because the Store.get didn’t catch the NoSectionError

#### persist()

Persists the data (if not in a context manager).
Persists means to call

#### to_dict()

Return the whole config as a `{section: {key: value}}` dict.

### config2py.s_configparser.persist_after_operation(method_func)

Wrap a mutating method so it calls `self.persist()` after running.

Used to make `ConfigStore` methods like `__setitem__` and
`__delitem__` persist their change to the store’s target immediately –
which writes to disk only when `target_kind` is `'filepath'`; for
`'string'`, `'bytes'` and `'dict'` targets, `persist()` just returns
the serialized data without touching disk.

### config2py.s_configparser.postprocess_ini_section_items(items)

Transform newline-separated string values into actual list of strings (assuming that intent)

* **Return type:**
  [`Generator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Generator)

```pycon
>>> section_from_ini = {
...     'name': 'aspyre',
...     'keywords': '\n\tdocumentation\n\tpackaging\n\tpublishing'
... }
>>> section_for_python = dict(postprocess_ini_section_items(section_from_ini))
>>> section_for_python
{'name': 'aspyre', 'keywords': ['documentation', 'packaging', 'publishing']}
```

### config2py.s_configparser.preprocess_ini_section_items(items)

Transform list values into newline-separated strings, in view of writing the value to a ini formatted section

* **Return type:**
  [`Generator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Generator)

```pycon
>>> section = {
...     'name': 'aspyre',
...     'keywords': ['documentation', 'packaging', 'publishing']
... }
>>> for_ini = dict(preprocess_ini_section_items(section))
>>> print('keywords =' + for_ini['keywords'])
keywords =
    documentation
    packaging
    publishing
```

### config2py.s_configparser.super_and_persist(super_cls, method_name)

To be able to do this:

```text
__setitem__ = super_and_persist(ConfigParser, '__setitem__')
__delitem__ = super_and_persist(ConfigParser, '__delitem__')
```

in your class definition block.

I thought I needed to wrap more method this way, but as it turns out, I might not,
so I prefer open code.


# _autosummary/config2py.sync_store.html.md

# config2py.sync_store

Synchronized key-value stores with automatic persistence.

Provides MutableMapping interfaces that automatically sync changes to their backing
storage. Supports deferred sync via context manager for batch operations.

```pycon
>>> import tempfile
>>> import json
>>>
>>> # Basic usage
>>> with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
...     _ = f.write('{"key": "value"}')
...     temp_file = f.name
>>>
>>> store = FileStore(temp_file)
>>> store['new_key'] = 'new_value'  # Auto-syncs immediately
>>> assert 'new_key' in store
>>>
>>> # Batch operations with context manager
>>> with store:
...     store['a'] = 1
...     store['b'] = 2
...     store['c'] = 3
...     # No sync until context exit
>>>
>>> import os
>>> os.unlink(temp_file)
```

### Functions

| [`register_extension`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.register_extension)(ext, loader, dumper)   | Register loader/dumper for a file extension.     |
|--------------------------------------------------------------------------------------------|--------------------------------------------------|
| [`get_format_handlers`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.get_format_handlers)(filepath)             | Get loader/dumper for a file based on extension. |

### Classes

| [`SyncStore`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.SyncStore)(loader, dumper)                        | A MutableMapping that automatically syncs changes to backing storage.   |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [`FileStore`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.FileStore)(filepath, \*[, key_path, loader, ...]) | A SyncStore backed by a file with automatic format detection.           |
| [`JsonStore`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.JsonStore)(filepath, \*[, key_path, indent, ...]) | A FileStore specialized for JSON files.                                 |

### *class* config2py.sync_store.FileStore(filepath, , key_path=None, loader=None, dumper=None, mode='r', dump_kwargs=None, create_file_content=None, create_key_path_content=None)

Bases: [`SyncStore`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.SyncStore)

A SyncStore backed by a file with automatic format detection.

Supports nested key paths for working with specific sections.

* **Parameters:**
  * **filepath** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Path to file (supports ~ expansion)
  * **key_path** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Optional nested path to operate on
  * **loader** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]]) – Optional custom loader (auto-detected from extension if not provided)
  * **dumper** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)], [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]) – Optional custom dumper (auto-detected from extension if not provided)
  * **mode** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – File read mode (‘r’ for text, ‘rb’ for binary)
  * **dump_kwargs** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Additional kwargs for dumper
  * **create_file_content** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]]) – Optional factory callable that returns initial dict content
    for missing files. If None, FileNotFoundError is raised for missing files.
  * **create_key_path_content** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]]) – Optional factory callable that returns initial content
    for missing key_path. If None, KeyError is raised for missing key paths.

### Example

```pycon
>>> import tempfile
>>> import os
>>>
>>> # Basic usage with existing file
>>> with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
...     _ = f.write('{"section": {"key": "value"}}')
...     temp_file = f.name
>>>
>>> section = FileStore(temp_file, key_path='section')
>>> section['key']
'value'
>>> section['new'] = 'data'
>>> os.unlink(temp_file)
>>>
>>> # Auto-create missing file and key_path
>>> with tempfile.TemporaryDirectory() as tmpdir:
...     new_file = os.path.join(tmpdir, 'config.json')
...     store = FileStore(
...         new_file,
...         key_path='servers',
...         create_file_content=lambda: {},
...         create_key_path_content=lambda: {}
...     )
...     store['myserver'] = {'command': 'python'}
...     'myserver' in store
True
```

### *class* config2py.sync_store.JsonStore(filepath, , key_path=None, indent=2, ensure_ascii=False, \*\*dump_kwargs)

Bases: [`FileStore`](_autosummary/config2py.sync_store.html.md#config2py.sync_store.FileStore)

A FileStore specialized for JSON files.

Pre-configured with json.loads/dumps and sensible defaults.

* **Parameters:**
  * **filepath** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Path to JSON file
  * **key_path** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Optional nested path to operate on
  * **indent** ([`int`](https://docs.python.org/3/builtins/functions.html#int)) – JSON indentation (default: 2)
  * **ensure_ascii** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to escape non-ASCII (default: False)
  * **\*\*dump_kwargs** – Additional kwargs for json.dumps

### *class* config2py.sync_store.SyncStore(loader, dumper)

Bases: [`MutableMapping`](https://docs.python.org/3/library/collections.abc.html#collections.abc.MutableMapping)

A MutableMapping that automatically syncs changes to backing storage.

Supports deferred sync via context manager for efficient batch operations.

* **Parameters:**
  * **loader** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[], [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – Function that returns the current data as a dict
  * **dumper** ([`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – Function that persists the data dict to storage

### Example

```pycon
>>> def my_loader():
...     return {'x': 1}
>>>
>>> data_holder = []
>>> def my_dumper(data):
...     data_holder.clear()
...     data_holder.append(data.copy())
>>>
>>> store = SyncStore(my_loader, my_dumper)
>>> store['y'] = 2  # Auto-syncs
>>> data_holder[0]
{'x': 1, 'y': 2}
>>>
>>> # Batch with context manager
>>> with store:
...     store['a'] = 1
...     store['b'] = 2
...     # Not synced yet
>>> data_holder[0]  # Now synced
{'x': 1, 'y': 2, 'a': 1, 'b': 2}
```

#### flush()

Sync data to backing storage if changes exist.

### config2py.sync_store.get_format_handlers(filepath)

Get loader/dumper for a file based on extension.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`Tuple`](https://docs.python.org/3/library/typing.html#typing.Tuple)[[`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable), [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)]]

### config2py.sync_store.register_extension(ext, loader, dumper)

Register loader/dumper for a file extension.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/config2py.tools.html.md

# config2py.tools

Various tools

### Functions

| [`extract_exports`](_autosummary/config2py.tools.html.md#config2py.tools.extract_exports)(exports)                   | Get a dict of `{name: value}` pairs from the `name="value" pairs of unix export lines (that is, lines of the ``export NAME="VALUE"` format        |
|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| [`get_configs_local_store`](_autosummary/config2py.tools.html.md#config2py.tools.get_configs_local_store)([config_src, ...]) | Get the local store of configs.                                                                                                                   |
| [`simple_config_getter`](_autosummary/config2py.tools.html.md#config2py.tools.simple_config_getter)([configs_src, ...])   | Make a simple config getter from a "central" config source specification.                                                                         |
| [`source_config_params`](_autosummary/config2py.tools.html.md#config2py.tools.source_config_params)(\*config_params)      | A decorator factory that sources config params, based on their names, to a config getter that will be provided when calling the wrapped function. |

### config2py.tools.extract_exports(exports)

Get a dict of `{name: value}` pairs from the `name="value" pairs of unix
export lines (that is, lines of the ``export NAME="VALUE"` format

* **Parameters:**
  **exports** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Filepath or string contents thereof
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  A dict of extracted `{name: value}` pairs

```pycon
>>> extract_exports('export KEY="secret"\nexport TOKEN="arbitrary"')
{'KEY': 'secret', 'TOKEN': 'arbitrary'}
```

## Use case:

You have access to environment variables through `os.environ`, but
if you want to extract exports from only a specific file (env vars are often
placed in different linked files), or the exports are defined in a string you hold,
then this simple parser can be useful.

### config2py.tools.get_configs_local_store(config_src='/home/runner/.config/config2py/configs', , configs_name='configs')

Get the local store of configs.

* **Parameters:**
  **config_src** – A specification of the local config store. By default:
  If it’s a directory, it’s assumed to be a folder of text files.
  If it’s a file, it’s assumed to be an ini or cfg file.
  If it’s a string, it’s assumed to be an app name, from which to create a folder

### config2py.tools.simple_config_getter(configs_src='/home/runner/.config/config2py/configs', \*, first_look_in_env_vars=True, ask_user_if_key_not_found=None, config_store_factory=<function get_configs_local_store>)

Make a simple config getter from a “central” config source specification.

The purpose of this function is to implement a common pattern of getting configs:
One that, by default (but optionally), looks in environment variables first,
then in a central config store, created via a simple `configs_src` specification
and then, if the key is not found in this “central” store, optionally (but not by
default) asks the user for the value and stores it in the central config store.

* **Parameters:**
  * **configs_src** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – A specification of the central config store. By default:
    If it’s a directory (with at least a slash), it’s assumed to be a folder of text files.
    If it’s a file, it’s assumed to be an ini or cfg file.
    If it’s a string, it’s assumed to be an app name, from which to create a folder
  * **first_look_in_env_vars** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to look in environment variables first
  * **ask_user_if_key_not_found** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to ask the user if the key is not found
    (and subsequently store the key in the central config store)
  * **config_store_factory** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)) – A function that takes a config source specification
    and returns the central config store

### config2py.tools.source_config_params(\*config_params)

A decorator factory that sources config params, based on their names, to a config
getter that will be provided when calling the wrapped function.

* **Parameters:**
  **config_params** – The names of the config params to source
* **Returns:**
  A decorator that sources the config params to the config getter

```pycon
>>> @source_config_params('a', 'b')
... def foo(a, b, c):
...     return a, b, c
>>> config = {'a': 1, 'b': 2, 'c': 3}
>>> foo(a='a', b='b', c=3, _config_getter=config.get)
(1, 2, 3)
```

A common use case is when you need to partialize a function with configs but the
config source is not defined yet.

```pycon
>>> from functools import partial
>>> bar = partial(foo, a='a')
```

`a` is set, but you’ll be able to call `bar` with different config sources,

```pycon
>>> bar(b='b', c=3, _config_getter=config.get)
(1, 2, 3)
>>> other_config = {'a': 11, 'b': 22, 'c': 33}
>>> bar(b='b', c=3, _config_getter=other_config.get)
(11, 22, 3)
```

What if the function as kwargs? No problem, the decorator will handle it. Just
make sure to use the same names for the kwargs as the config params.

```pycon
>>> @source_config_params('a', 'b', 'd')
... def foo(a, b, c, **kwargs):
...     return a, b, c, kwargs
>>> config = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> foo(a='a', b='b', c=3, d='d', _config_getter=config.get)
(1, 2, 3, {'d': 4})
```

As you can see, `d` is sourced as well.


# _autosummary/config2py.util.html.md

# config2py.util

Utility functions for config2py.

### Module Attributes

| [`FolderSpec`](_autosummary/config2py.util.html.md#config2py.util.FolderSpec)(env_var, default_path, subpath)   | Declarative description of where a given folder kind lives on a platform.   |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|

### Functions

| [`always_true`](_autosummary/config2py.util.html.md#config2py.util.always_true)(x)                                    | Function that just returns True.                                                                                                                                                        |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`app_folder_standards`](_autosummary/config2py.util.html.md#config2py.util.app_folder_standards)([os_name])                   | Return the `{folder_kind: FolderSpec}` table for the given `os.name`.                                                                                                                   |
| [`ask_user_for_input`](_autosummary/config2py.util.html.md#config2py.util.ask_user_for_input)(prompt[, default, ...])        | Ask the user for input, optionally masking, validating and transforming the input.                                                                                                      |
| [`create_directories`](_autosummary/config2py.util.html.md#config2py.util.create_directories)(dirpath[, max_dirs_to_make])   | Create directories up to a specified limit.                                                                                                                                             |
| [`ensure_seeded`](_autosummary/config2py.util.html.md#config2py.util.ensure_seeded)(target, package_name, ...[, ...])   | Copy a bundled seed file to *target* if it does not already exist.                                                                                                                      |
| [`extract_variable_declarations`](_autosummary/config2py.util.html.md#config2py.util.extract_variable_declarations)(string[, expand])   | Reads the contents of a config file, extracting Unix-style environment variable declarations of the form `export {NAME}={value}`, returning a dictionary of `{NAME: value, ...}` pairs. |
| [`get_app_folder`](_autosummary/config2py.util.html.md#config2py.util.get_app_folder)([app_name, setup_callback, ...])   | Retrieve or create the app directory specific to the given app name and folder kind.                                                                                                    |
| [`get_app_rootdir`](_autosummary/config2py.util.html.md#config2py.util.get_app_rootdir)([folder_kind, ensure_exists])     | Returns the root directory for a specific folder kind.                                                                                                                                  |
| [`get_configs_directory_for_app`](_autosummary/config2py.util.html.md#config2py.util.get_configs_directory_for_app)([app_name, ...])    | Retrieve or create the configs directory specific to the given app name.                                                                                                                |
| [`get_configs_folder_for_app`](_autosummary/config2py.util.html.md#config2py.util.get_configs_folder_for_app)([app_name, ...])       | Retrieve or create the configs directory specific to the given app name.                                                                                                                |
| [`identity`](_autosummary/config2py.util.html.md#config2py.util.identity)(x)                                       | Function that just returns its argument.                                                                                                                                                |
| [`is_not_empty`](_autosummary/config2py.util.html.md#config2py.util.is_not_empty)(x)                                   | Function that returns True if x is not empty.                                                                                                                                           |
| [`is_repl`](_autosummary/config2py.util.html.md#config2py.util.is_repl)()                                         | Determines if the Python interpreter is running in REPL.                                                                                                                                |
| [`parse_assignments_from_py_source`](_autosummary/config2py.util.html.md#config2py.util.parse_assignments_from_py_source)(source_code, \*) | Parse assignments from python source code.                                                                                                                                              |
| [`secure_makedirs`](_autosummary/config2py.util.html.md#config2py.util.secure_makedirs)(dirpath, \*[, exist_ok])          | `os.makedirs(dirpath, mode=0o700)`, re-tightening the mode if it already exists.                                                                                                        |
| [`secure_open`](_autosummary/config2py.util.html.md#config2py.util.secure_open)(path[, mode])                         | Open `path` for writing with owner-only (`0o600`) permissions.                                                                                                                          |
| [`system_default_for_app_data_folder`](_autosummary/config2py.util.html.md#config2py.util.system_default_for_app_data_folder)([...])         | Get the system default folder for `folder_kind`.                                                                                                                                        |

### Classes

| [`AppData`](_autosummary/config2py.util.html.md#config2py.util.AppData)(app_name, \*[, package_name, ...])   | Per-user data directory facade for a Python application.                     |
|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`EnvironmentVariables`](_autosummary/config2py.util.html.md#config2py.util.EnvironmentVariables)()                       | Class to wrap environment variables, hiding values from `repr`/`print` only. |
| [`FolderSpec`](_autosummary/config2py.util.html.md#config2py.util.FolderSpec)(env_var, default_path, subpath)   | Declarative description of where a given folder kind lives on a platform.    |

### *class* config2py.util.AppData(app_name, , package_name=None, seed_data_dir='_seed_data')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Per-user data directory facade for a Python application.

Binds an application name (and optional Python package name) once and
provides convenient access to:

* **resources** — editable reference data seeded from the package on
  first access (`~/.local/share/<app>/resources/`).
* **config** — user preference files, also seeded on first access
  (`~/.config/<app>/`).
* **artifact directories** — runtime-generated data organised by kind
  (`~/.local/share/<app>/artifacts/<kind>/`).

Seed files are read via `importlib.resources` from
`<package_name>._seed_data.{resources,config}/`.

* **Parameters:**
  * **app_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – The application name used for the directory under the
    XDG root (e.g. `"my_app"` → `~/.local/share/my_app`).
  * **package_name** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]) – The top-level Python package that contains the
    `_seed_data` directory.  Defaults to *app_name*.
  * **seed_data_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the seed-data sub-package inside the
    Python package (default `"_seed_data"`).

### Example

```pycon
>>> app = AppData("myapp", package_name="myapp")
>>> app.app_folder()
PosixPath('/Users/.../.local/share/myapp')
```

#### app_folder(, folder_kind='data')

Return the app directory for *folder_kind*, creating it if needed.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

#### get_artifact_dir(kind)

Return (and create) an artifact sub-directory for *kind*.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

#### get_config(name)

Return a config file path, seeding from package data if missing.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

#### get_resource(name)

Return a user resource path, seeding from package data if missing.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### *class* config2py.util.EnvironmentVariables

Bases: [`ChainMap`](https://docs.python.org/3/library/collections.html#collections.ChainMap)

Class to wrap environment variables, hiding values from `repr`/`print` only.

`__repr__` is overridden to avoid printing secrets to a REPL or log, but values
are still reachable through normal `Mapping` operations – `dict(envvar)`,
`envvar.items()`/`.values()`, `pickle.dumps(envvar)`, or a structured logger
that walks the mapping. Treat this as UI-level redaction, not access control (see
i2mint/config2py#16).

### *class* config2py.util.FolderSpec(env_var, default_path, subpath)

Bases: [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)

Declarative description of where a given folder kind lives on a platform.

`env_var` is the platform-standard environment variable that, when set,
names the *root* folder.  `default_path` is the root to use when that
variable is absent (`~` is expanded).  `subpath` is a relative path
appended to the root; it exists because some platform standards place a
folder kind *inside* another kind’s root rather than under its own variable
(e.g. Windows cache lives at `%LOCALAPPDATA%\\Temp`).

#### default_path

Alias for field number 1

#### env_var

Alias for field number 0

#### subpath

Alias for field number 2

### config2py.util.always_true(x)

Function that just returns True.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### config2py.util.app_folder_standards(os_name='posix')

Return the `{folder_kind: FolderSpec}` table for the given `os.name`.

This is the *single* place where config2py branches on the operating
system: everything else consumes the returned table.  Exposing it as a
function (rather than an `if` at import time) keeps the branch testable
on any platform – callers can ask for the table of an OS they are not
running on.

* **Parameters:**
  **os_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – An `os.name` value; `"nt"` selects the Windows standards,
  anything else selects the XDG Base Directory standards.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

```pycon
>>> app_folder_standards("nt")["cache"]
FolderSpec(env_var='LOCALAPPDATA', default_path='~\\AppData\\Local', subpath='Temp')
>>> app_folder_standards("posix")["cache"]
FolderSpec(env_var='XDG_CACHE_HOME', default_path='~/.cache', subpath='')
```

### config2py.util.ask_user_for_input(prompt, default='', \*, mask_input=False, masking_toggle_str=None, egress=<function identity>)

Ask the user for input, optionally masking, validating and transforming the input.

* **Parameters:**
  * **prompt** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Prompt to display to the user
  * **default** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Default value to return if the user enters nothing
  * **mask_input** – Whether to mask the user’s input
  * **masking_toggle_str** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – String to toggle input masking. If `None`, no toggle
    is available. If not `None` (a common choice is the empty string)
    the user can enter this string to toggle input masking.
  * **egress** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)) – Function to apply to the user’s response before returning it.
    This can be used to validate the response, for example.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)
* **Returns:**
  The user’s response (or the default value if the user entered nothing)

### config2py.util.create_directories(dirpath, max_dirs_to_make=None)

Create directories up to a specified limit.

* **Parameters:**
  * **dirpath** ([*str*](https://docs.python.org/3/builtins/stdtypes.html#str)) – The directory path to create.
  * **max_dirs_to_make** ([*int*](https://docs.python.org/3/builtins/functions.html#int) *,* *optional*) – The maximum number of directories to
    create. If None, there’s no limit.
* **Returns:**
  True if the directory was created successfully, False otherwise.
* **Return type:**
  [*bool*](https://docs.python.org/3/builtins/functions.html#bool)
* **Raises:**
  [**ValueError**](https://docs.python.org/3/builtins/exceptions.html#ValueError) – If max_dirs_to_make is negative.

### Examples

```pycon
>>> import tempfile, shutil
>>> temp_dir = tempfile.mkdtemp()
>>> target_dir = os.path.join(temp_dir, 'a', 'b', 'c')
>>> create_directories(target_dir, max_dirs_to_make=2)
False
>>> create_directories(target_dir, max_dirs_to_make=3)
True
>>> os.path.isdir(target_dir)
True
>>> shutil.rmtree(temp_dir)  # Cleanup
```

```pycon
>>> temp_dir = tempfile.mkdtemp()
>>> target_dir = os.path.join(temp_dir, 'a', 'b', 'c', 'd')
>>> create_directories(target_dir)
True
>>> os.path.isdir(target_dir)
True
>>> shutil.rmtree(temp_dir)  # Cleanup
```

### config2py.util.ensure_seeded(target, package_name, seed_subpackage, filename, , seed_data_dir='_seed_data')

Copy a bundled seed file to *target* if it does not already exist.

Reads the seed from `importlib.resources.files(
"{package_name}.{seed_data_dir}.{seed_subpackage}") / filename`
and writes its bytes to *target*.  If *target* already exists, this is
a no-op (user edits are preserved).

* **Parameters:**
  * **target** (`Union`[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)]) – Destination path for the seeded file.
  * **package_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Top-level Python package that ships the seed data.
  * **seed_subpackage** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Subdirectory inside `_seed_data` (e.g. `"resources"`
    or `"config"`).
  * **filename** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the seed file.
  * **seed_data_dir** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the seed-data directory inside *package_name*
    (default `"_seed_data"`).
* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
* **Returns:**
  The resolved *target* as a `Path`.

### Example

```pycon
>>> from config2py import ensure_seeded
>>> # ensure_seeded("/tmp/myfile.txt", "mypkg", "resources", "myfile.txt")
```

### config2py.util.extract_variable_declarations(string, expand=None)

Reads the contents of a config file, extracting Unix-style environment variable
declarations of the form
`export {NAME}={value}`, returning a dictionary of `{NAME: value, ...}` pairs.

See issue for more info and applications:
[https://github.com/i2mint/config2py/issues/2](https://github.com/i2mint/config2py/issues/2)

* **Parameters:**
  * **string** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – String to extract variable declarations from
  * **expand** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict) | [`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – An optional dictionary of variable names and values to use to
    expand variables that are referenced (i.e. `$NAME` is a reference to `NAME`
    variable) in the values of config variables.
    If `True`, `expand` is replaced with an empty dictionary, which means we
    want to expand variables recursively, but we have no references to seed the
    expansion with. If `False`, `expand` is replaced with `None`, indicating
    that we don’t want to expand any variables.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)
* **Returns:**
  A dictionary of variable names and values.

```pycon
>>> config = 'export ENVIRONMENT="dev"\nexport PORT=8080\nexport DEBUG=true'
>>> extract_variable_declarations(config)
{'ENVIRONMENT': 'dev', 'PORT': '8080', 'DEBUG': 'true'}
```

```pycon
>>> config = 'export PATH="$PATH:/usr/local/bin"\nexport EDITOR="nano"'
>>> extract_variable_declarations(config)
{'PATH': '$PATH:/usr/local/bin', 'EDITOR': 'nano'}
```

The `expand` argument can be used to expand variables in the values of other.

Let’s add a reference to the `PATH` variable in the `EDITOR` variable:

```pycon
>>> config = 'export PATH="$PATH:/usr/local/bin"\nexport EDITOR="nano $PATH"'
```

If you specify a value for `PATH` in the `expand` argument, you’ll see it
reflected in the `PATH` variable (self reference) and the `EDITOR` variable.
(Note if you changed the order of `PATH` and `EDITOR` in the `config`,
you wouldn’t get the same thing though.)

```pycon
>>> extract_variable_declarations(config, expand={'PATH': '/root'})
{'PATH': '/root:/usr/local/bin', 'EDITOR': 'nano /root:/usr/local/bin'}
```

If you specify `expand={}`, the first `PATH` variable will not be expanded,
since PATH is not in the expand dictionary. But the second `PATH` variable,
referenced in the definition of `EDITOR` will be expanded, since it is in the
expand dictionary.

```pycon
>>> extract_variable_declarations(config, expand={})
{'PATH': '$PATH:/usr/local/bin', 'EDITOR': 'nano $PATH:/usr/local/bin'}
```

### config2py.util.get_app_config_folder(app_name='config2py', \*, setup_callback=<function \_default_folder_setup>, ensure_exists=False, folder_kind='config')

Retrieve or create the app directory specific to the given app name and folder kind.

The folder kind determines where the app’s files are stored. Here are concise
explanations for each folder kind:

- **config**: User preferences and settings files (e.g., API keys, theme
  preferences, editor settings). Files users might edit manually or that
  define how the app behaves.
- **data**: Essential user-created content and application state (e.g.,
  databases, saved games, user documents, session files). Data that should
  be backed up and persists across updates.
- **cache**: Temporary, regeneratable files (e.g., downloaded images,
  compiled assets, web cache). Can be safely deleted to free space without
  losing user work.
- **state**: Application state and logs that persist between sessions but
  aren’t critical user data (e.g., command history, undo history, recently
  opened files, log files). Unlike cache, shouldn’t be auto-deleted.
- **runtime**: Temporary runtime files that only exist while the app runs
  (e.g., PID files, Unix sockets, lock files, named pipes). Typically
  cleared on logout/reboot.
- **TL;DR**: config = settings, data = user files, cache = disposable,
  state = logs/history, runtime = process files.

* **Parameters:**
  * **app_name** – Name of the app for which the directory is needed.
  * **setup_callback** – A callback function to initialize the directory.
    Default is \_default_folder_setup.
  * **ensure_exists** – Whether to ensure the directory exists.
  * **folder_kind** – Type of folder (‘config’, ‘data’, ‘cache’, ‘state’, or ‘runtime’).
    Default is ‘config’ for backward compatibility.
* **Returns:**
  Path to the app directory.
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

By default, the app will be “config2py” and folder_kind will be “config”.
The exact text of the path is platform-specific (`~/.config/config2py` under
the XDG standards, `%APPDATA%\config2py` on Windows), so we assert the
properties that hold everywhere: it is an absolute path named after the app,
sitting directly inside the ‘config’ root directory.

```pycon
>>> folder = get_app_folder()
>>> os.path.isabs(folder)
True
>>> os.path.basename(folder)
'config2py'
>>> os.path.dirname(folder) == get_app_rootdir('config')
True
```

You can specify a different app name and folder kind:

```pycon
>>> get_app_folder('my_app', folder_kind='data')
'/Users/.../.local/share/my_app'
>>> get_app_folder('my_app', folder_kind='cache')
'/Users/.../.cache/my_app'
```

You can also specify a path relative to the app root directory:

```pycon
>>> get_app_folder('another/app/subfolder', folder_kind='data')
'/Users/.../.local/share/another/app/subfolder'
```

If ensure_exists is True, the directory will be created and initialized
with the setup_callback:

```pycon
>>> path = get_app_folder('my_app', ensure_exists=True)
>>> os.path.exists(path)
True
```

### config2py.util.get_app_data_directory(app_name='config2py', \*, setup_callback=<function \_default_folder_setup>, ensure_exists=False, folder_kind='config')

Retrieve or create the app directory specific to the given app name and folder kind.

The folder kind determines where the app’s files are stored. Here are concise
explanations for each folder kind:

- **config**: User preferences and settings files (e.g., API keys, theme
  preferences, editor settings). Files users might edit manually or that
  define how the app behaves.
- **data**: Essential user-created content and application state (e.g.,
  databases, saved games, user documents, session files). Data that should
  be backed up and persists across updates.
- **cache**: Temporary, regeneratable files (e.g., downloaded images,
  compiled assets, web cache). Can be safely deleted to free space without
  losing user work.
- **state**: Application state and logs that persist between sessions but
  aren’t critical user data (e.g., command history, undo history, recently
  opened files, log files). Unlike cache, shouldn’t be auto-deleted.
- **runtime**: Temporary runtime files that only exist while the app runs
  (e.g., PID files, Unix sockets, lock files, named pipes). Typically
  cleared on logout/reboot.
- **TL;DR**: config = settings, data = user files, cache = disposable,
  state = logs/history, runtime = process files.

* **Parameters:**
  * **app_name** – Name of the app for which the directory is needed.
  * **setup_callback** – A callback function to initialize the directory.
    Default is \_default_folder_setup.
  * **ensure_exists** – Whether to ensure the directory exists.
  * **folder_kind** – Type of folder (‘config’, ‘data’, ‘cache’, ‘state’, or ‘runtime’).
    Default is ‘config’ for backward compatibility.
* **Returns:**
  Path to the app directory.
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

By default, the app will be “config2py” and folder_kind will be “config”.
The exact text of the path is platform-specific (`~/.config/config2py` under
the XDG standards, `%APPDATA%\config2py` on Windows), so we assert the
properties that hold everywhere: it is an absolute path named after the app,
sitting directly inside the ‘config’ root directory.

```pycon
>>> folder = get_app_folder()
>>> os.path.isabs(folder)
True
>>> os.path.basename(folder)
'config2py'
>>> os.path.dirname(folder) == get_app_rootdir('config')
True
```

You can specify a different app name and folder kind:

```pycon
>>> get_app_folder('my_app', folder_kind='data')
'/Users/.../.local/share/my_app'
>>> get_app_folder('my_app', folder_kind='cache')
'/Users/.../.cache/my_app'
```

You can also specify a path relative to the app root directory:

```pycon
>>> get_app_folder('another/app/subfolder', folder_kind='data')
'/Users/.../.local/share/another/app/subfolder'
```

If ensure_exists is True, the directory will be created and initialized
with the setup_callback:

```pycon
>>> path = get_app_folder('my_app', ensure_exists=True)
>>> os.path.exists(path)
True
```

### config2py.util.get_app_data_folder(app_name='config2py', \*, setup_callback=<function \_default_folder_setup>, ensure_exists=False, folder_kind='data')

Retrieve or create the app directory specific to the given app name and folder kind.

The folder kind determines where the app’s files are stored. Here are concise
explanations for each folder kind:

- **config**: User preferences and settings files (e.g., API keys, theme
  preferences, editor settings). Files users might edit manually or that
  define how the app behaves.
- **data**: Essential user-created content and application state (e.g.,
  databases, saved games, user documents, session files). Data that should
  be backed up and persists across updates.
- **cache**: Temporary, regeneratable files (e.g., downloaded images,
  compiled assets, web cache). Can be safely deleted to free space without
  losing user work.
- **state**: Application state and logs that persist between sessions but
  aren’t critical user data (e.g., command history, undo history, recently
  opened files, log files). Unlike cache, shouldn’t be auto-deleted.
- **runtime**: Temporary runtime files that only exist while the app runs
  (e.g., PID files, Unix sockets, lock files, named pipes). Typically
  cleared on logout/reboot.
- **TL;DR**: config = settings, data = user files, cache = disposable,
  state = logs/history, runtime = process files.

* **Parameters:**
  * **app_name** – Name of the app for which the directory is needed.
  * **setup_callback** – A callback function to initialize the directory.
    Default is \_default_folder_setup.
  * **ensure_exists** – Whether to ensure the directory exists.
  * **folder_kind** – Type of folder (‘config’, ‘data’, ‘cache’, ‘state’, or ‘runtime’).
    Default is ‘config’ for backward compatibility.
* **Returns:**
  Path to the app directory.
* **Return type:**
  [*str*](https://docs.python.org/3/builtins/stdtypes.html#str)

By default, the app will be “config2py” and folder_kind will be “config”.
The exact text of the path is platform-specific (`~/.config/config2py` under
the XDG standards, `%APPDATA%\config2py` on Windows), so we assert the
properties that hold everywhere: it is an absolute path named after the app,
sitting directly inside the ‘config’ root directory.

```pycon
>>> folder = get_app_folder()
>>> os.path.isabs(folder)
True
>>> os.path.basename(folder)
'config2py'
>>> os.path.dirname(folder) == get_app_rootdir('config')
True
```

You can specify a different app name and folder kind:

```pycon
>>> get_app_folder('my_app', folder_kind='data')
'/Users/.../.local/share/my_app'
>>> get_app_folder('my_app', folder_kind='cache')
'/Users/.../.cache/my_app'
```

You can also specify a path relative to the app root directory:

```pycon
>>> get_app_folder('another/app/subfolder', folder_kind='data')
'/Users/.../.local/share/another/app/subfolder'
```

If ensure_exists is True, the directory will be created and initialized
with the setup_callback:

```pycon
>>> path = get_app_folder('my_app', ensure_exists=True)
>>> os.path.exists(path)
True
```

### config2py.util.get_app_folder(app_name='config2py', \*, setup_callback=<function \_default_folder_setup>, ensure_exists=False, folder_kind='config')

Retrieve or create the app directory specific to the given app name and folder kind.

The folder kind determines where the app’s files are stored. Here are concise
explanations for each folder kind:

- **config**: User preferences and settings files (e.g., API keys, theme
  preferences, editor settings). Files users might edit manually or that
  define how the app behaves.
- **data**: Essential user-created content and application state (e.g.,
  databases, saved games, user documents, session files). Data that should
  be backed up and persists across updates.
- **cache**: Temporary, regeneratable files (e.g., downloaded images,
  compiled assets, web cache). Can be safely deleted to free space without
  losing user work.
- **state**: Application state and logs that persist between sessions but
  aren’t critical user data (e.g., command history, undo history, recently
  opened files, log files). Unlike cache, shouldn’t be auto-deleted.
- **runtime**: Temporary runtime files that only exist while the app runs
  (e.g., PID files, Unix sockets, lock files, named pipes). Typically
  cleared on logout/reboot.
- **TL;DR**: config = settings, data = user files, cache = disposable,
  state = logs/history, runtime = process files.

* **Parameters:**
  * **app_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the app for which the directory is needed.
  * **setup_callback** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – A callback function to initialize the directory.
    Default is \_default_folder_setup.
  * **ensure_exists** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to ensure the directory exists.
  * **folder_kind** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'config'`, `'data'`, `'cache'`, `'state'`, `'runtime'`]) – Type of folder (‘config’, ‘data’, ‘cache’, ‘state’, or ‘runtime’).
    Default is ‘config’ for backward compatibility.
* **Returns:**
  Path to the app directory.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

By default, the app will be “config2py” and folder_kind will be “config”.
The exact text of the path is platform-specific (`~/.config/config2py` under
the XDG standards, `%APPDATA%\config2py` on Windows), so we assert the
properties that hold everywhere: it is an absolute path named after the app,
sitting directly inside the ‘config’ root directory.

```pycon
>>> folder = get_app_folder()
>>> os.path.isabs(folder)
True
>>> os.path.basename(folder)
'config2py'
>>> os.path.dirname(folder) == get_app_rootdir('config')
True
```

You can specify a different app name and folder kind:

```pycon
>>> get_app_folder('my_app', folder_kind='data')
'/Users/.../.local/share/my_app'
>>> get_app_folder('my_app', folder_kind='cache')
'/Users/.../.cache/my_app'
```

You can also specify a path relative to the app root directory:

```pycon
>>> get_app_folder('another/app/subfolder', folder_kind='data')
'/Users/.../.local/share/another/app/subfolder'
```

If ensure_exists is True, the directory will be created and initialized
with the setup_callback:

```pycon
>>> path = get_app_folder('my_app', ensure_exists=True)
>>> os.path.exists(path)
True
```

### config2py.util.get_app_rootdir(folder_kind='config', , ensure_exists=True)

Returns the root directory for a specific folder kind.

The folder kind determines which standard directory is returned:

- ‘config’: Configuration files (XDG_CONFIG_HOME, default ~/.config)
- ‘data’: Application data (XDG_DATA_HOME, default ~/.local/share)
- ‘cache’: Temporary/cache files (XDG_CACHE_HOME, default ~/.cache)
- ‘state’: State data/logs (XDG_STATE_HOME, default ~/.local/state)
- ‘runtime’: Runtime files (XDG_RUNTIME_DIR, default /tmp)

On Windows:

- ‘config’: %APPDATA%
- ‘data’: %LOCALAPPDATA%
- ‘cache’: %LOCALAPPDATA%Temp
- ‘state’: %LOCALAPPDATA%
- ‘runtime’: %TEMP%

* **Parameters:**
  * **folder_kind** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'config'`, `'data'`, `'cache'`, `'state'`, `'runtime'`]) – The kind of folder to get. One of ‘config’, ‘data’, ‘cache’, ‘state’, ‘runtime’.
    Defaults to ‘config’.
    Here are concise explanations for each folder kind:
    **config**: User preferences and settings files (e.g., API keys, theme preferences, editor settings). Files users might edit manually or that define how the app behaves.
    **data**: Essential user-created content and application state (e.g., databases, saved games, user documents, session files). Data that should be backed up and persists across updates.
    **cache**: Temporary, regeneratable files (e.g., downloaded images, compiled assets, web cache). Can be safely deleted to free space without losing user work.
    **state**: Application state and logs that persist between sessions but aren’t critical user data (e.g., command history, undo history, recently opened files, log files). Unlike cache, shouldn’t be auto-deleted.
    **runtime**: Temporary runtime files that only exist while the app runs (e.g., PID files, Unix sockets, lock files, named pipes). Typically cleared on logout/reboot.
    **TL;DR**: config = settings, data = user files, cache = disposable, state = logs/history, runtime = process files.
  * **ensure_exists** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool)) – Whether to create the directory if it doesn’t exist
* **Returns:**
  The full path of the app root folder for the specified kind.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### NOTE
The default root folder follows XDG Base Directory standards on Unix/Linux/macOS.
You can override this by setting environment variables:

- CONFIG2PY_CONFIG_DIR, CONFIG2PY_DATA_DIR, CONFIG2PY_CACHE_DIR, etc.
  (highest priority, overrides everything, and works on **every** platform –
  see `config2py_env_var` for the full list of names)
- The platform’s own standard variable: XDG_CONFIG_HOME, XDG_DATA_HOME,
  XDG_CACHE_HOME, etc. on Unix/Linux/macOS; APPDATA / LOCALAPPDATA / TEMP on
  Windows. The XDG variables are a POSIX standard and are **not** consulted on
  Windows – use the CONFIG2PY_\* variables above for platform-neutral overrides.
- If neither is set, uses platform defaults

### Examples

```pycon
>>> get_app_rootdir('config')
'/Users/.../.config'
>>> get_app_rootdir('data')
'/Users/.../.local/share'
>>> get_app_rootdir('cache')
'/Users/.../.cache'
```

### config2py.util.get_configs_directory_for_app(app_name='config2py', \*, configs_name='configs', app_dir_setup_callback=<function \_default_folder_setup>, config_dir_setup_callback=<function \_default_folder_setup>)

Retrieve or create the configs directory specific to the given app name.

* **Parameters:**
  * **app_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the app for which the configs directory is needed.
  * **configs_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the configs directory.
  * **app_dir_setup_callback** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – A callback function to
    initialize the app directory. Default is \_default_folder_setup.
  * **config_dir_setup_callback** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – A callback function to
    initialize the configs directory. Default is \_default_folder_setup.

### config2py.util.get_configs_folder_for_app(app_name='config2py', \*, configs_name='configs', app_dir_setup_callback=<function \_default_folder_setup>, config_dir_setup_callback=<function \_default_folder_setup>)

Retrieve or create the configs directory specific to the given app name.

* **Parameters:**
  * **app_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the app for which the configs directory is needed.
  * **configs_name** ([`str`](https://docs.python.org/3/builtins/stdtypes.html#str)) – Name of the configs directory.
  * **app_dir_setup_callback** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – A callback function to
    initialize the app directory. Default is \_default_folder_setup.
  * **config_dir_setup_callback** ([`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`None`](https://docs.python.org/3/builtins/constants.html#None)]) – A callback function to
    initialize the configs directory. Default is \_default_folder_setup.

### config2py.util.identity(x)

Function that just returns its argument.

* **Return type:**
  [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)

### config2py.util.is_not_empty(x)

Function that returns True if x is not empty.

* **Return type:**
  [`bool`](https://docs.python.org/3/builtins/functions.html#bool)

### config2py.util.is_repl()

Determines if the Python interpreter is running in REPL.

To test: If you put it in a module.py, do a print of it in the module, and do
`python module.py` it should print False.
If you do `python -i module.py`, or call it from a python console or jupyter
notebook, it should return `True`.

* **Returns:**
  True if running in a REPL, False otherwise.
* **Return type:**
  [*bool*](https://docs.python.org/3/builtins/functions.html#bool)

`is_repl` returns `True` if any function in `is_repl.repl_conditions`
(a set of no-argument callables) returns `True`. By default that set checks
whether `get_ipython` is in globals, or whether `__main__` has no
`__file__` attribute. Mutate `is_repl.repl_conditions` in place (e.g.
`is_repl.repl_conditions.add(fn)`) to change the checks – rebinding the
attribute to a new set has no effect, since `is_repl` reads the original set.

### config2py.util.parse_assignments_from_py_source(source_code, \*, name_filt=None, value_filt=<function \_value_node_is_instance_of>)

Parse assignments from python source code.

```pycon
>>> source_code = '''a = 1
... b = 'hello'
... c = [1, 2, 3]
... def func():
...     d = 4
... '''
>>> dict(parse_assignments_from_py_source(source_code))
{'a': 1, 'b': 'hello', 'c': [1, 2, 3], 'd': 4}
```

### config2py.util.secure_makedirs(dirpath, , exist_ok=True)

`os.makedirs(dirpath, mode=0o700)`, re-tightening the mode if it already exists.

`os.makedirs(..., mode=0o700, exist_ok=True)` alone won’t re-tighten an existing
directory’s mode, so this follows up with an explicit `os.chmod`. Intended for
directories that may hold config/secret files (see i2mint/config2py#15).

### config2py.util.secure_open(path, mode='w')

Open `path` for writing with owner-only (`0o600`) permissions.

Two cases, both handled:

- *New* file: the restrictive mode is applied atomically at creation via
  `os.open`, so there is no window where the file briefly exists with the
  process’s default umask (commonly world-readable, `0o644`).
- *Pre-existing* file with looser permissions: `os.open`’s `mode` argument
  is a POSIX no-op in this case (only consulted when a new file is actually
  created), so an explicit `os.fchmod` re-tightens it – on the open file
  descriptor, not the path, so it’s not subject to a TOCTOU swap either.

Intended for files that may hold secrets (see i2mint/config2py#15).

```pycon
>>> import tempfile, os
>>> path = tempfile.mktemp()
>>> with secure_open(path, "w") as f:
...     _ = f.write("secret")
>>> # Unix mode bits aren't meaningful on Windows -- os.stat there reports 0o666
>>> # regardless of what secure_open does, so only assert the mode on POSIX.
>>> oct(os.stat(path).st_mode & 0o777) if os.name == "posix" else "0o600"
'0o600'
>>> os.remove(path)
```

### config2py.util.system_default_for_app_data_folder(folder_kind='config', , standards=None)

Get the system default folder for `folder_kind`.

The root is the value of the platform’s standard environment variable for
that kind, falling back to the spec’s `default_path`; the spec’s
`subpath` (usually empty) is then appended.

* **Parameters:**
  * **folder_kind** ([`Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)[`'config'`, `'data'`, `'cache'`, `'state'`, `'runtime'`]) – One of ‘config’, ‘data’, ‘cache’, ‘state’, ‘runtime’.
  * **standards** ([`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]) – The `{folder_kind: FolderSpec}` table to resolve against.
    Defaults to the running platform’s (`APP_FOLDER_STANDARDS`);
    pass another platform’s table to resolve as that platform would.
* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-09-22 14:46 UTC** from commit <a href="https://github.com/i2mint/config2py/commit/10e900555e6b9f26827a6493f3a9c1cb3af47a83"><code>10e9005</code></a> on branch <code>master</code>, for **config2py 0.1.54** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                         |
|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/config2py/commit/10e900555e6b9f26827a6493f3a9c1cb3af47a83"><code>10e900555e6b9f26827a6493f3a9c1cb3af47a83</code></a> |
| Branch              | <code>master</code>                                                                                                                                     |
| Tags at this commit | <code>0.1.54</code>                                                                                                                                     |
| Working tree        | clean                                                                                                                                                   |
| Remote              | <code>https://github.com/i2mint/config2py</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/config2py</code>                                                              |
| Run          | <a href="https://github.com/i2mint/config2py/actions/runs/35742479925">35742479925</a>     |
| Ref          | <code>refs/heads/master</code>                                                             |
| Event commit | <code>c7603902ac98e2dd5ba05de396921c48a4c7f4a5</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.12  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.14 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>pydata_sphinx_theme</code>) |
| accent        | <code>#723e8a</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/config2py/0.1.54/">0.1.54</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/i2mint/config2py && cd config2py
git checkout 10e900555e6b9f26827a6493f3a9c1cb3af47a83
pip install "epythet==0.2.12"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`config2py` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/i2mint/config2py/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://i2mint.github.io/config2py/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`config2py.md`](https://i2mint.github.io/config2py/config2py.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://i2mint.github.io/config2py/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`config2py`](_autosummary/config2py.html.md#module-config2py)   | Tools to read and write configurations from various sources and formats   |
|-------------------------------------------------------------------------------|---------------------------------------------------------------------------|


