1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
# Usage
Suppose a `pyproject.toml` file lives in the user's directory:
```toml
[tool.acme]
foo = "bar"
```
## Retrieving values
`UserConfig` objects have a `values` property that behaves as a dict which
allows config values to be retrieved:
```python
>>> from maison import UserConfig
>>> config = UserConfig(package_name="acme")
>>> config.values
"{'foo': 'bar'}"
>>> config.values["foo"]
'bar'
>>> "baz" in config.values
False
>>> config.values.get("baz", "qux")
'qux'
```
## Source files
By default, `maison` will look for a `pyproject.toml` file. If you prefer to look
elsewhere, provide a `source_files` list to `UserConfig` and `maison` will select the
first source file it finds from the list.
```python
from maison import UserConfig
config = UserConfig(
package_name="acme",
source_files=["acme.ini", "pyproject.toml"]
)
print(config.path)
#> PosixPath(/path/to/acme.ini)
```
```{caution}
Currently only `.toml` and `.ini` files are supported. For `.ini` files,
`maison` assumes that the whole file is relevant. For `pyproject.toml` files,
`maison` assumes that the relevant section will be in a
`[tool.{package_name}]` section. For other `.toml` files `maison` assumes the whole
file is relevant.
```
To verify which source config file has been found, `UserConfig` exposes a
`path` property:
```python
>>> config.path
PosixPath('/path/to/pyproject.toml')
```
The source file can either be a filename or an absolute path to a config:
```python
from maison import UserConfig
config = UserConfig(
package_name="acme",
source_files=["~/.config/acme.ini", "pyproject.toml"]
)
print(config.path)
#> PosixPath(/Users/tom.jones/.config/acme.ini)
```
## Merging configs
`maison` offers support for merging multiple configs. To do so, set the `merge_configs`
flag to `True` in the constructor for `UserConfig`:
```python
from maison import UserConfig
config = UserConfig(
package_name="acme",
source_files=["~/.config/acme.toml", "~/.acme.ini", "pyproject.toml"],
merge_configs=True
)
print(config.path)
"""
[
PosixPath(/Users/tom.jones/.config/acme.toml),
PosixPath(/Users/tom.jones/.acme.ini),
PosixPath(/path/to/pyproject.toml),
]
"""
print(config.get_option("foo"))
#> "bar"
```
```{warning}
When merging configs, `maison` merges from **right to left**, ie. rightmost sources
take precedence. So in the above example, if `~/config/.acme.toml` and
`pyproject.toml` both set `nice_option`, the value from `pyproject.toml` will be
returned from `config.get_option("nice_option")`.
```
## Search paths
By default, `maison` searches for config files by starting at `Path.cwd()` and moving up
the tree until it finds the relevant config file or there are no more parent paths.
You can start searching from a different path by providing a `starting_path` property to
`UserConfig`:
```python
from maison import UserConfig
config = UserConfig(
package_name="acme",
starting_path=Path("/some/other/path")
)
print(config.path)
#> PosixPath(/some/other/path/pyproject.toml)
```
## Validation
`maison` offers optional schema validation.
To validate a configuration, first create a schema. The schema should implement
a method called `model_dump`. This can be achieved by writing the schema as a
`pydantic` model:
```python
from pydantic import BaseModel
class MySchema(BaseModel):
foo: str = "my_default"
```
```{note}
`maison` validation was built with using `pydantic` models as schemas in mind
but this package doesn't explicitly declare `pydantic` as a dependency so you
are free to use another validation package if you wish, you just need to ensure
that your schema follows the `maison.config._IsSchema` protocol.
```
Then inject the schema when instantiating a `UserConfig`:
```python
from maison import UserConfig
config = UserConfig(package_name="acme", schema=MySchema)
```
To validate the config, simply run `validate()` on the config instance:
```python
config.validate()
```
If the configuration is invalid and if you are using a `pydantic` base model as
your schema, a `pydantic` `ValidationError` will be raised. If the configuration
is valid, the validated values are returned.
If `validate` is invoked but no schema has been provided, a `NoSchemaError` will
be raised. A schema can be added after instantiation through a setter:
```python
config.schema = MySchema
```
## Casting and default values
By default, `maison` will replace the values in the config with whatever comes back from
the validation. For example, for a config file that looks like this:
```toml
[tool.acme]
foo = 1
```
And a schema that looks like this:
```python
class MySchema(BaseModel):
foo: str
bar: str = "my_default"
```
Running the config through validation will render the following:
```python
config = UserConfig(package_name="acme", schema=MySchema)
print(config)
#> {"foo": 1}
config.validate()
print(config)
#> {"foo": "1", "bar": "my_default"}
```
If you prefer to keep the config values untouched and just perform simple validation,
add a `use_schema_values=False` argument to the `validate` method.
### Schema precedence
The `validate` method also accepts a `config_schema` is an argument. If one is provided here,
it will be used instead of a schema passed as an init argument.
|