Usage¶
Suppose a pyproject.toml
file lives in the user’s directory:
[tool.acme]
foo = "bar"
Retrieving values¶
UserConfig
objects have a values
property that behaves as a dict which
allows config values to be retrieved:
>>> 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.
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:
>>> config.path
PosixPath('/path/to/pyproject.toml')
The source file can either be a filename or an absolute path to a config:
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
:
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
:
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:
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
:
from maison import UserConfig
config = UserConfig(package_name="acme", schema=MySchema)
To validate the config, simply run validate()
on the config instance:
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:
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:
[tool.acme]
foo = 1
And a schema that looks like this:
class MySchema(BaseModel):
foo: str
bar: str = "my_default"
Running the config through validation will render the following:
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.