nested-config


Namenested-config JSON
Version 2.1.1 PyPI version JSON
download
home_pagehttps://gitlab.com/osu-nrsg/nested-config
SummaryParse configuration files that include paths to other config files into a singleconfiguration object
upload_time2024-04-19 16:45:56
maintainerNone
docs_urlNone
authorRandall Pittman
requires_python<4.0,>=3.8
licenseMIT
keywords config configuration files
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # nested-config <!-- omit in toc -->

<span style="font-size: larger">If you've ever wanted to have the option of replacing part
 of a configuration file with a path to another configuration file that contains those
sub-parameters, then _nested-config_ might be for you.</span>

_nested-config_ allows you to parse configuration files that contain references to other
configuration files using a series of [models](#model). If a model includes a [nested
model](#nested-model) as one of its attributes and _nested-config_ finds a string value
for that parameter in the configuration file instead of an associative
array[^assoc-array], then it assumes that this string is a path to another configuration
file that should be parsed and whose contents should replace the string in the main
configuration file. If the string appears to be a relative path, it is assumed to be
relative to the path of its parent configuration file.

## Contents

- [Contents](#contents)
- [Basic Usage](#basic-usage)
- [Nomenclature](#nomenclature)
  - [loader](#loader)
  - [model](#model)
  - [nested model](#nested-model)
  - [config dict](#config-dict)
- [API](#api)
  - [`nested_config.expand_config(config_path, model, *, default_suffix = None)`](#nested_configexpand_configconfig_path-model--default_suffix--none)
  - [`nested_config.config_dict_loaders`](#nested_configconfig_dict_loaders)
    - [Included loaders](#included-loaders)
    - [Adding loaders](#adding-loaders)
  - [_Deprecated features in v2.1.0, to be removed in v3.0.0_](#deprecated-features-in-v210-to-be-removed-in-v300)
- [Pydantic 1.0/2.0 Compatibility](#pydantic-1020-compatibility)
- [Footnotes](#footnotes)

## Basic Usage

Given the following configuration files `/tmp/house.toml` and `/tmp/tmp2/dimensions.toml`:

<figure>
<figcaption>Figure 1: /tmp/house.toml</figcaption>

```toml
name = "my house"
dimensions = "tmp2/dimensions.toml"
```

</figure>

<figure>
<figcaption>Figure 2: /tmp/tmp2/dimensions.toml</figcaption>

```toml
length = 10
width = 20
```

</figure>

You can expand these into a single dict with the following:

<figure>
<figcaption>Figure 3: Expand /tmp/house.toml</figcaption>

```python
import nested_config

class Dimensions:
    length: int
    width: int


class House:
    name: str
    dimensions: Dimensions


house_dict = nested_config.expand_config("/tmp/house.toml", House)
print(house_dict)
# {'name': 'my house', 'dimensions': {'length': 10, 'width': 20}}
```

Note that in `/tmp/house.toml`, `dimensions` is not a mapping but is a path to another
toml file at a path relative to `house.toml`.

See [tests](https://gitlab.com/osu-nrsg/nested-config/-/tree/master/tests) for more
detailed use-cases, such as where the root model contains lists or dicts of other models
and when those may be included in the root config file or specified as paths to sub-config
files.

## Nomenclature

### loader

A _loader_ is a function that reads a config file and returns a `dict` containing the
key-value pairs from the file. _nested-config_ includes loaders for JSON, TOML, and (if
PyYAML is installed) YAML. For example, the JSON loader looks like this:

```python
import json

def json_load(path):
    with open(path, "rb") as fobj:
        return json.load(fobj)
```

### model

_nested-config_ uses the term _model_ to refer to a class definition that includes
annotated attributes. For example, this model, `Dimensions`, includes three attributes,
each of float type, `x`, `y`, and `z`:

```python
class Dimensions:
    x: float
    y: float
    z: float
```

A model can be decorated as a [dataclass][dataclasses] or using [`attrs.define`][attrs] or
can subclass [`pydantic.BaseModel`][pydantic] to provide some method for instantiating an
object instance of the model but they aren't necessary to use _nested-config_.

The only criterion for a type to be a model is that is has a `__dict__` attribute that
includes an `__annotations__` member. _Note: This does **not** mean that **instances** of
the model must have a `__dict__` attribute. For example, instances of classes with
`__slots__` and `NamedTuple` instances may not have a `__dict__` attribute._

### nested model

A _nested model_ is a model that is included within another model as one of its class
attributes. For example, the below model `House` includes an `name` of string type, and an
attribute `dimensions` of `Dimensions` type (defined above). Since `Dimensions` is a
_model_ type, this is an example of a _nested model_.

```python
class House:
    name: str
    dimensions: Dimensions
```

### config dict

A _config dict_ is simply a `dict` with string keys such as may be obtained by reading in
configuration text. For example reading in a string of TOML text with `tomllib.loads`
returns a _config dict_.

```python
import tomllib

config = "x = 2\ny = 3"
print(tomllib.loads(config))
# {'x': 2, 'y': 3}
```

## API

### `nested_config.expand_config(config_path, model, *, default_suffix = None)`

This function first loads the config file at `config_path` into a [config
dict](#config-dict) using the appropriate [loader](#loader). It then uses the attribute
annotations of [`model`](#model) and/or any [nested models](#nested-model) within `model`
to see if any of the string values in the configuration file correspond to a nested model.
For each such case, the string is assumed to be a path and is loaded into another config
dict which replaces the string value in the parent config dict. This continues until all
paths are converted and then the fully-expanded config dict is returned.

Note that all non-absolute string paths are assumed to be relative to the path of their
parent config file.

The loader for a given config file is determined by file extension (AKA suffix). If
`default_suffix` is specified, any config file with an unknown suffix or no suffix will be
assumed to be of that type, e.g. `".toml"`. (Otherwise this is an error.) It is possible
for one config file to include a path to a config file of a different format, so long as
each file has the appropriate suffix and there is a loader for that suffix.

### `nested_config.config_dict_loaders`

`config_dict_loaders` is a `dict` that maps file suffixes to [loaders](#loader).

#### Included loaders

_nested-config_ automatically loads the following files based on extension:

| Format | Extensions(s) | Library                                    |
| ------ | ------------- | ------------------------------------------ |
| JSON   | .json         | `json` (stdlib)                            |
| TOML   | .toml         | `tomllib` (Python 3.11+ stdlib) or `tomli` |
| YAML   | .yaml, .yml   | `pyyaml` (extra dependency[^yaml-extra])   |

#### Adding loaders

To add a loader for another file extension, simply update `config_dict_loaders`:

```python
import nested_config
from nested_config import ConfigDict  # alias for dict[str, Any]

def dummy_loader(config_path: Path | str) -> ConfigDict:
    return {"a": 1, "b": 2}

nested_config.config_dict_loaders[".dmy"] = dummy_loader

# or add another extension for an existing loader
nested_config.config_dict_loaders[".jsn"] = nested_config.config_dict_loaders[".json"]

# or use a different library to replace an existing loader
import rtoml

def rtoml_load(path) -> ConfigDict:
    with open(path, "rb") as fobj:
        return rtoml.load(fobj)

nested_config.config_dict_loaders[".toml"] = rtoml_load
```

### _Deprecated features in v2.1.0, to be removed in v3.0.0_

The following functionality is available only if Pydantic is installed:

- `nested_config.validate_config()` expands a configuration file according to a Pydantic
  model and then validates the config dictionary into an instance of the Pydantic model.
- `nested_config.BaseModel` can be used as a replacement for `pydantic.BaseModel` to
  include a `from_config()` classmethod on all models that uses
  `nested_config.validate_config()` to create an instance of the model.
- By importing `nested_config`, `PurePath` validators and JSON encoders are added to
  `pydantic` in Pydantic 1.8-1.10 (they are included in Pydantic 2.0+)

## Pydantic 1.0/2.0 Compatibility

The [pydantic functionality](#deprecated-features-in-v210-to-be-removed-in-v300) in
nested-config is runtime compatible with Pydantic 1.8+ and Pydantic 2.0.

The follow table gives info on how to configure the [mypy](https://www.mypy-lang.org/) and
[Pyright](https://microsoft.github.io/pyright) type checkers to properly work, depending
on the version of Pydantic you are using.

| Pydantic Version | [mypy config][1]            | mypy cli                    | [Pyright config][2]                         |
|------------------|-----------------------------|-----------------------------|---------------------------------------------|
| 2.0+             | `always_false = PYDANTIC_1` | `--always-false PYDANTIC_1` | `defineConstant = { "PYDANTIC_1" = false }` |
| 1.8-1.10         | `always_true = PYDANTIC_1`  | `--always-true PYDANTIC_1`  | `defineConstant = { "PYDANTIC_1" = true }`  |

## Footnotes

[^yaml-extra]: Install `pyyaml` separately with `pip` or install _nested-config_ with
               `pip install nested-config[yaml]`.

[^assoc-array]: Each language uses one or more names for an associative arrays. JSON calls
                it an _object_, YAML calls is a _mapping_, and TOML calls is a _table_.
                Any of course in Python it's a _dictionary_, or `dict`.

[1]: https://mypy.readthedocs.io/en/latest/config_file.html
[2]: https://microsoft.github.io/pyright/#/configuration
[dataclasses]: https://docs.python.org/3/library/dataclasses.html
[attrs]: https://www.attrs.org
[pydantic]: https://pydantic.dev

            

Raw data

            {
    "_id": null,
    "home_page": "https://gitlab.com/osu-nrsg/nested-config",
    "name": "nested-config",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<4.0,>=3.8",
    "maintainer_email": null,
    "keywords": "config, configuration files",
    "author": "Randall Pittman",
    "author_email": "pittmara@oregonstate.edu",
    "download_url": "https://files.pythonhosted.org/packages/c4/4c/8e0fdb10f823819b659800ef6352160ceabf66f7aef21d1ea4160b7ea208/nested_config-2.1.1.tar.gz",
    "platform": null,
    "description": "# nested-config <!-- omit in toc -->\n\n<span style=\"font-size: larger\">If you've ever wanted to have the option of replacing part\n of a configuration file with a path to another configuration file that contains those\nsub-parameters, then _nested-config_ might be for you.</span>\n\n_nested-config_ allows you to parse configuration files that contain references to other\nconfiguration files using a series of [models](#model). If a model includes a [nested\nmodel](#nested-model) as one of its attributes and _nested-config_ finds a string value\nfor that parameter in the configuration file instead of an associative\narray[^assoc-array], then it assumes that this string is a path to another configuration\nfile that should be parsed and whose contents should replace the string in the main\nconfiguration file. If the string appears to be a relative path, it is assumed to be\nrelative to the path of its parent configuration file.\n\n## Contents\n\n- [Contents](#contents)\n- [Basic Usage](#basic-usage)\n- [Nomenclature](#nomenclature)\n  - [loader](#loader)\n  - [model](#model)\n  - [nested model](#nested-model)\n  - [config dict](#config-dict)\n- [API](#api)\n  - [`nested_config.expand_config(config_path, model, *, default_suffix = None)`](#nested_configexpand_configconfig_path-model--default_suffix--none)\n  - [`nested_config.config_dict_loaders`](#nested_configconfig_dict_loaders)\n    - [Included loaders](#included-loaders)\n    - [Adding loaders](#adding-loaders)\n  - [_Deprecated features in v2.1.0, to be removed in v3.0.0_](#deprecated-features-in-v210-to-be-removed-in-v300)\n- [Pydantic 1.0/2.0 Compatibility](#pydantic-1020-compatibility)\n- [Footnotes](#footnotes)\n\n## Basic Usage\n\nGiven the following configuration files `/tmp/house.toml` and `/tmp/tmp2/dimensions.toml`:\n\n<figure>\n<figcaption>Figure 1: /tmp/house.toml</figcaption>\n\n```toml\nname = \"my house\"\ndimensions = \"tmp2/dimensions.toml\"\n```\n\n</figure>\n\n<figure>\n<figcaption>Figure 2: /tmp/tmp2/dimensions.toml</figcaption>\n\n```toml\nlength = 10\nwidth = 20\n```\n\n</figure>\n\nYou can expand these into a single dict with the following:\n\n<figure>\n<figcaption>Figure 3: Expand /tmp/house.toml</figcaption>\n\n```python\nimport nested_config\n\nclass Dimensions:\n    length: int\n    width: int\n\n\nclass House:\n    name: str\n    dimensions: Dimensions\n\n\nhouse_dict = nested_config.expand_config(\"/tmp/house.toml\", House)\nprint(house_dict)\n# {'name': 'my house', 'dimensions': {'length': 10, 'width': 20}}\n```\n\nNote that in `/tmp/house.toml`, `dimensions` is not a mapping but is a path to another\ntoml file at a path relative to `house.toml`.\n\nSee [tests](https://gitlab.com/osu-nrsg/nested-config/-/tree/master/tests) for more\ndetailed use-cases, such as where the root model contains lists or dicts of other models\nand when those may be included in the root config file or specified as paths to sub-config\nfiles.\n\n## Nomenclature\n\n### loader\n\nA _loader_ is a function that reads a config file and returns a `dict` containing the\nkey-value pairs from the file. _nested-config_ includes loaders for JSON, TOML, and (if\nPyYAML is installed) YAML. For example, the JSON loader looks like this:\n\n```python\nimport json\n\ndef json_load(path):\n    with open(path, \"rb\") as fobj:\n        return json.load(fobj)\n```\n\n### model\n\n_nested-config_ uses the term _model_ to refer to a class definition that includes\nannotated attributes. For example, this model, `Dimensions`, includes three attributes,\neach of float type, `x`, `y`, and `z`:\n\n```python\nclass Dimensions:\n    x: float\n    y: float\n    z: float\n```\n\nA model can be decorated as a [dataclass][dataclasses] or using [`attrs.define`][attrs] or\ncan subclass [`pydantic.BaseModel`][pydantic] to provide some method for instantiating an\nobject instance of the model but they aren't necessary to use _nested-config_.\n\nThe only criterion for a type to be a model is that is has a `__dict__` attribute that\nincludes an `__annotations__` member. _Note: This does **not** mean that **instances** of\nthe model must have a `__dict__` attribute. For example, instances of classes with\n`__slots__` and `NamedTuple` instances may not have a `__dict__` attribute._\n\n### nested model\n\nA _nested model_ is a model that is included within another model as one of its class\nattributes. For example, the below model `House` includes an `name` of string type, and an\nattribute `dimensions` of `Dimensions` type (defined above). Since `Dimensions` is a\n_model_ type, this is an example of a _nested model_.\n\n```python\nclass House:\n    name: str\n    dimensions: Dimensions\n```\n\n### config dict\n\nA _config dict_ is simply a `dict` with string keys such as may be obtained by reading in\nconfiguration text. For example reading in a string of TOML text with `tomllib.loads`\nreturns a _config dict_.\n\n```python\nimport tomllib\n\nconfig = \"x = 2\\ny = 3\"\nprint(tomllib.loads(config))\n# {'x': 2, 'y': 3}\n```\n\n## API\n\n### `nested_config.expand_config(config_path, model, *, default_suffix = None)`\n\nThis function first loads the config file at `config_path` into a [config\ndict](#config-dict) using the appropriate [loader](#loader). It then uses the attribute\nannotations of [`model`](#model) and/or any [nested models](#nested-model) within `model`\nto see if any of the string values in the configuration file correspond to a nested model.\nFor each such case, the string is assumed to be a path and is loaded into another config\ndict which replaces the string value in the parent config dict. This continues until all\npaths are converted and then the fully-expanded config dict is returned.\n\nNote that all non-absolute string paths are assumed to be relative to the path of their\nparent config file.\n\nThe loader for a given config file is determined by file extension (AKA suffix). If\n`default_suffix` is specified, any config file with an unknown suffix or no suffix will be\nassumed to be of that type, e.g. `\".toml\"`. (Otherwise this is an error.) It is possible\nfor one config file to include a path to a config file of a different format, so long as\neach file has the appropriate suffix and there is a loader for that suffix.\n\n### `nested_config.config_dict_loaders`\n\n`config_dict_loaders` is a `dict` that maps file suffixes to [loaders](#loader).\n\n#### Included loaders\n\n_nested-config_ automatically loads the following files based on extension:\n\n| Format | Extensions(s) | Library                                    |\n| ------ | ------------- | ------------------------------------------ |\n| JSON   | .json         | `json` (stdlib)                            |\n| TOML   | .toml         | `tomllib` (Python 3.11+ stdlib) or `tomli` |\n| YAML   | .yaml, .yml   | `pyyaml` (extra dependency[^yaml-extra])   |\n\n#### Adding loaders\n\nTo add a loader for another file extension, simply update `config_dict_loaders`:\n\n```python\nimport nested_config\nfrom nested_config import ConfigDict  # alias for dict[str, Any]\n\ndef dummy_loader(config_path: Path | str) -> ConfigDict:\n    return {\"a\": 1, \"b\": 2}\n\nnested_config.config_dict_loaders[\".dmy\"] = dummy_loader\n\n# or add another extension for an existing loader\nnested_config.config_dict_loaders[\".jsn\"] = nested_config.config_dict_loaders[\".json\"]\n\n# or use a different library to replace an existing loader\nimport rtoml\n\ndef rtoml_load(path) -> ConfigDict:\n    with open(path, \"rb\") as fobj:\n        return rtoml.load(fobj)\n\nnested_config.config_dict_loaders[\".toml\"] = rtoml_load\n```\n\n### _Deprecated features in v2.1.0, to be removed in v3.0.0_\n\nThe following functionality is available only if Pydantic is installed:\n\n- `nested_config.validate_config()` expands a configuration file according to a Pydantic\n  model and then validates the config dictionary into an instance of the Pydantic model.\n- `nested_config.BaseModel` can be used as a replacement for `pydantic.BaseModel` to\n  include a `from_config()` classmethod on all models that uses\n  `nested_config.validate_config()` to create an instance of the model.\n- By importing `nested_config`, `PurePath` validators and JSON encoders are added to\n  `pydantic` in Pydantic 1.8-1.10 (they are included in Pydantic 2.0+)\n\n## Pydantic 1.0/2.0 Compatibility\n\nThe [pydantic functionality](#deprecated-features-in-v210-to-be-removed-in-v300) in\nnested-config is runtime compatible with Pydantic 1.8+ and Pydantic 2.0.\n\nThe follow table gives info on how to configure the [mypy](https://www.mypy-lang.org/) and\n[Pyright](https://microsoft.github.io/pyright) type checkers to properly work, depending\non the version of Pydantic you are using.\n\n| Pydantic Version | [mypy config][1]            | mypy cli                    | [Pyright config][2]                         |\n|------------------|-----------------------------|-----------------------------|---------------------------------------------|\n| 2.0+             | `always_false = PYDANTIC_1` | `--always-false PYDANTIC_1` | `defineConstant = { \"PYDANTIC_1\" = false }` |\n| 1.8-1.10         | `always_true = PYDANTIC_1`  | `--always-true PYDANTIC_1`  | `defineConstant = { \"PYDANTIC_1\" = true }`  |\n\n## Footnotes\n\n[^yaml-extra]: Install `pyyaml` separately with `pip` or install _nested-config_ with\n               `pip install nested-config[yaml]`.\n\n[^assoc-array]: Each language uses one or more names for an associative arrays. JSON calls\n                it an _object_, YAML calls is a _mapping_, and TOML calls is a _table_.\n                Any of course in Python it's a _dictionary_, or `dict`.\n\n[1]: https://mypy.readthedocs.io/en/latest/config_file.html\n[2]: https://microsoft.github.io/pyright/#/configuration\n[dataclasses]: https://docs.python.org/3/library/dataclasses.html\n[attrs]: https://www.attrs.org\n[pydantic]: https://pydantic.dev\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Parse configuration files that include paths to other config files into a singleconfiguration object",
    "version": "2.1.1",
    "project_urls": {
        "Changes": "https://gitlab.com/osu-nrsg/nested-config/-/blob/master/CHANGELOG.md",
        "Homepage": "https://gitlab.com/osu-nrsg/nested-config",
        "Repository": "https://gitlab.com/osu-nrsg/nested-config"
    },
    "split_keywords": [
        "config",
        " configuration files"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "56e7efd95dfd3ff7e0e1a3119d8e64c8491395e01d5bc5750c73c535d8d667f9",
                "md5": "cbab17ffba81a379b92e55c8320dd1bd",
                "sha256": "eeac03a333fdc7f08863a0dbb49edc82785874898750a484061ef10e398bf08b"
            },
            "downloads": -1,
            "filename": "nested_config-2.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "cbab17ffba81a379b92e55c8320dd1bd",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<4.0,>=3.8",
            "size": 14170,
            "upload_time": "2024-04-19T16:45:54",
            "upload_time_iso_8601": "2024-04-19T16:45:54.621564Z",
            "url": "https://files.pythonhosted.org/packages/56/e7/efd95dfd3ff7e0e1a3119d8e64c8491395e01d5bc5750c73c535d8d667f9/nested_config-2.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "c44c8e0fdb10f823819b659800ef6352160ceabf66f7aef21d1ea4160b7ea208",
                "md5": "c9fffa35475db730de97f5efef950b61",
                "sha256": "5e5a8bf52b68fcee687eeed6afb9c1c9249469f42cd697f978c5878640832854"
            },
            "downloads": -1,
            "filename": "nested_config-2.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "c9fffa35475db730de97f5efef950b61",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<4.0,>=3.8",
            "size": 16248,
            "upload_time": "2024-04-19T16:45:56",
            "upload_time_iso_8601": "2024-04-19T16:45:56.338909Z",
            "url": "https://files.pythonhosted.org/packages/c4/4c/8e0fdb10f823819b659800ef6352160ceabf66f7aef21d1ea4160b7ea208/nested_config-2.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-04-19 16:45:56",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "osu-nrsg",
    "gitlab_project": "nested-config",
    "lcname": "nested-config"
}
        
Elapsed time: 4.15991s