# config2py.util

Utility functions for config2py.

### Module Attributes

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

### Functions

| [`DFLT_MASKING_INPUT`](#config2py.util.DFLT_MASKING_INPUT)(text)                          | True if `text` (typically a prompt naming a config key) looks secret.                                                                                                                   |
|----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`always_true`](#config2py.util.always_true)(x)                                    | Function that just returns True.                                                                                                                                                        |
| [`app_folder_standards`](#config2py.util.app_folder_standards)([os_name])                   | Return the `{folder_kind: FolderSpec}` table for the given `os.name`.                                                                                                                   |
| [`ask_user_for_input`](#config2py.util.ask_user_for_input)(prompt[, default, ...])        | Ask the user for input, optionally masking, validating and transforming the input.                                                                                                      |
| [`create_directories`](#config2py.util.create_directories)(dirpath[, max_dirs_to_make])   | Create directories up to a specified limit.                                                                                                                                             |
| [`ensure_seeded`](#config2py.util.ensure_seeded)(target, package_name, ...[, ...])   | Copy a bundled seed file to *target* if it does not already exist.                                                                                                                      |
| [`extract_variable_declarations`](#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`](#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`](#config2py.util.get_app_rootdir)([folder_kind, ensure_exists])     | Returns the root directory for a specific folder kind.                                                                                                                                  |
| [`get_configs_directory_for_app`](#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`](#config2py.util.get_configs_folder_for_app)([app_name, ...])       | Retrieve or create the configs directory specific to the given app name.                                                                                                                |
| [`identity`](#config2py.util.identity)(x)                                       | Function that just returns its argument.                                                                                                                                                |
| [`is_not_empty`](#config2py.util.is_not_empty)(x)                                   | Function that returns True if x is not empty.                                                                                                                                           |
| [`is_repl`](#config2py.util.is_repl)()                                         | Determines if the Python interpreter is running in REPL.                                                                                                                                |
| [`looks_like_secret`](#config2py.util.looks_like_secret)(text)                           | True if `text` (typically a prompt naming a config key) looks secret.                                                                                                                   |
| [`parse_assignments_from_py_source`](#config2py.util.parse_assignments_from_py_source)(source_code, \*) | Parse assignments from python source code.                                                                                                                                              |
| [`secure_makedirs`](#config2py.util.secure_makedirs)(dirpath, \*[, exist_ok])          | `os.makedirs(dirpath, mode=0o700)`, re-tightening the mode if it already exists.                                                                                                        |
| [`secure_open`](#config2py.util.secure_open)(path[, mode])                         | Open `path` for writing with owner-only (`0o600`) permissions.                                                                                                                          |
| [`system_default_for_app_data_folder`](#config2py.util.system_default_for_app_data_folder)([...])         | Get the system default folder for `folder_kind`.                                                                                                                                        |

### Classes

| [`AppData`](#config2py.util.AppData)(app_name, \*[, package_name, ...])   | Per-user data directory facade for a Python application.                     |
|-----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`EnvironmentVariables`](#config2py.util.EnvironmentVariables)()                       | Class to wrap environment variables, hiding values from `repr`/`print` only. |
| [`FolderSpec`](#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)

### config2py.util.DFLT_MASKING_INPUT(text)

True if `text` (typically a prompt naming a config key) looks secret.

It errs on the side of masking: a false positive only means the user doesn’t see
what they type, while a false negative echoes a secret to the terminal. It is a
plain substring match on the whole prompt, so `KEYS_DIR` or `AUTHOR` also
match, as would a custom prompt template mentioning “key”. Pass an explicit
`mask_input` (or your own predicate) when that matters.

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

```pycon
>>> looks_like_secret("Enter a value for OPENAI_API_KEY: ")
True
>>> looks_like_secret("Enter a value for github_token: ")
True
>>> looks_like_secret("Enter a value for DATA_DIR: ")
False
```

### *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=<function looks_like_secret>, 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** ([`bool`](https://docs.python.org/3/builtins/functions.html#bool) | [`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)], [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]) – Whether to mask the user’s input: a bool, or a
    `prompt -> bool` predicate. The default, `looks_like_secret`, masks
    prompts that mention something secret-looking (`API_KEY`, `TOKEN`,
    `PASSWORD`, …) and echoes the others. When masking is decided by a
    predicate and stdin is piped (not a terminal), the response is read from
    stdin, as it was before this default existed. An explicit `True` always
    uses `getpass.getpass`, which reads the terminal even when stdin is piped.
  * **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.looks_like_secret(text)

True if `text` (typically a prompt naming a config key) looks secret.

It errs on the side of masking: a false positive only means the user doesn’t see
what they type, while a false negative echoes a secret to the terminal. It is a
plain substring match on the whole prompt, so `KEYS_DIR` or `AUTHOR` also
match, as would a custom prompt template mentioning “key”. Pass an explicit
`mask_input` (or your own predicate) when that matters.

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

```pycon
>>> looks_like_secret("Enter a value for OPENAI_API_KEY: ")
True
>>> looks_like_secret("Enter a value for github_token: ")
True
>>> looks_like_secret("Enter a value for DATA_DIR: ")
False
```

### 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)
