# 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
from pydantic import BaseModel, ConfigDict

class MySchema(BaseModel):
  model_config = ConfigDict(coerce_numbers_to_str=True)

  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.
