Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 63 additions & 53 deletions src/maison/config.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
"""Module to hold the `UserConfig` class definition."""

from functools import reduce
from pathlib import Path
from typing import Any
from typing import Optional
from typing import Protocol
from typing import Union
import pathlib
import typing

from maison.errors import NoSchemaError
from maison.utils import _collect_configs
from maison.utils import deep_merge
from maison import config_reader
from maison import config_validator as validator
from maison import disk_filesystem
from maison import errors
from maison import readers
from maison import service
from maison import types


class _IsSchema(Protocol):
"""Protocol for config schemas."""
def _bootstrap_service(package_name: str) -> service.ConfigService:
_config_reader = config_reader.ConfigReader()

def model_dump(self) -> dict[Any, Any]:
"""Convert the validated config to a dict."""
...
pyproject_parser = readers.PyprojectReader(package_name=package_name)
toml_parser = readers.TomlReader()
ini_parser = readers.IniReader()

_config_reader.register_parser(
suffix=".toml", parser=pyproject_parser, stem="pyproject"
)
_config_reader.register_parser(suffix=".toml", parser=toml_parser)
_config_reader.register_parser(suffix=".ini", parser=ini_parser)

return service.ConfigService(
filesystem=disk_filesystem.DiskFilesystem(),
config_reader=_config_reader,
validator=validator.Validator(),
)


class UserConfig:
Expand All @@ -26,9 +38,9 @@ class UserConfig:
def __init__(
self,
package_name: str,
starting_path: Optional[Path] = None,
source_files: Optional[list[str]] = None,
schema: Optional[type[_IsSchema]] = None,
starting_path: typing.Optional[pathlib.Path] = None,
source_files: typing.Optional[list[str]] = None,
schema: typing.Optional[type[service.IsSchema]] = None,
merge_configs: bool = False,
) -> None:
"""Initialize the config.
Expand All @@ -45,14 +57,21 @@ def __init__(
merged if multiple are found
"""
self.source_files = source_files or ["pyproject.toml"]
self.starting_path = starting_path
self.merge_configs = merge_configs
self._sources = _collect_configs(
package_name=package_name,
self._schema = schema

self._service = _bootstrap_service(package_name=package_name)

_sources = self._service.find_configs(
source_files=self.source_files,
starting_path=starting_path,
)
self._schema = schema
self._values = self._generate_config_dict()

self._values = self._service.get_config_values(
config_file_paths=_sources,
merge_configs=merge_configs,
)

def __str__(self) -> str:
"""Return the __str__.
Expand All @@ -63,7 +82,7 @@ def __str__(self) -> str:
return f"<class '{self.__class__.__name__}'>"

@property
def values(self) -> dict[str, Any]:
def values(self) -> types.ConfigValues:
"""Return the user's configuration values.

Returns:
Expand All @@ -72,29 +91,34 @@ def values(self) -> dict[str, Any]:
return self._values

@values.setter
def values(self, values: dict[str, Any]) -> None:
def values(self, values: types.ConfigValues) -> None:
"""Set the user's configuration values."""
self._values = values

@property
def discovered_paths(self) -> list[Path]:
def discovered_paths(self) -> list[pathlib.Path]:
"""Return a list of the paths to the config sources found on the filesystem.

Returns:
a list of the paths to the config sources
"""
return [source.filepath for source in self._sources]
return list(
self._service.find_configs(
source_files=self.source_files,
starting_path=self.starting_path,
)
)

@property
def path(self) -> Optional[Union[Path, list[Path]]]:
def path(self) -> typing.Optional[typing.Union[pathlib.Path, list[pathlib.Path]]]:
"""Return the path to the selected config source.

Returns:
`None` is no config sources have been found, a list of the found config
sources if `merge_configs` is `True`, or the path to the active config
source if `False`
"""
if len(self._sources) == 0:
if len(self.discovered_paths) == 0:
return None

if self.merge_configs:
Expand All @@ -103,7 +127,7 @@ def path(self) -> Optional[Union[Path, list[Path]]]:
return self.discovered_paths[0]

@property
def schema(self) -> Optional[type[_IsSchema]]:
def schema(self) -> typing.Optional[type[service.IsSchema]]:
"""Return the schema.

Returns:
Expand All @@ -112,15 +136,15 @@ def schema(self) -> Optional[type[_IsSchema]]:
return self._schema

@schema.setter
def schema(self, schema: type[_IsSchema]) -> None:
def schema(self, schema: type[service.IsSchema]) -> None:
"""Set the schema."""
self._schema = schema

def validate(
self,
schema: Optional[type[_IsSchema]] = None,
schema: typing.Optional[type[service.IsSchema]] = None,
use_schema_values: bool = True,
) -> dict[str, Any]:
) -> types.ConfigValues:
"""Validate the configuration.

Warning:
Expand Down Expand Up @@ -153,32 +177,18 @@ class Schema(ConfigSchema):
Raises:
NoSchemaError: when validation is attempted but no schema has been provided
"""
selected_schema: Union[type[_IsSchema], None] = schema or self.schema
selected_schema: typing.Union[type[service.IsSchema], None] = (
schema or self.schema
)

if not selected_schema:
raise NoSchemaError
raise errors.NoSchemaError

validated_schema = selected_schema(**self.values)
validated_values = self._service.validate_config(
values=self.values, schema=selected_schema
)

if use_schema_values:
self.values = validated_schema.model_dump()
self.values = validated_values

return self.values

def _generate_config_dict(self) -> dict[str, Any]:
"""Generate the config dict.

If `merge_configs` is set to `False` then we use the first config. If `True`
then the dicts of the sources are merged from right to left.

Returns:
the config dict
"""
if len(self._sources) == 0:
return {}

if not self.merge_configs:
return self._sources[0].to_dict()

source_dicts = (source.to_dict() for source in self._sources)
return reduce(lambda a, b: deep_merge(a, b), source_dicts)
42 changes: 42 additions & 0 deletions src/maison/config_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pathlib
import typing

from maison import errors
from maison import types


ParserDictKey = tuple[str, typing.Union[str, None]]


class Parser(typing.Protocol):
def parse_config(self, file_path: pathlib.Path) -> types.ConfigValues: ...


class ConfigReader:
def __init__(self) -> None:
self._parsers: dict[ParserDictKey, Parser] = {}

def register_parser(
self,
suffix: str,
parser: Parser,
stem: typing.Optional[str] = None,
) -> None:
"""Register a parser for a file suffix, optionally restricted by filename stem."""
key = (suffix, stem)
self._parsers[key] = parser

def parse_config(self, file_path: pathlib.Path) -> types.ConfigValues:
key: ParserDictKey

# First try (suffix, stem)
key = (file_path.suffix, file_path.stem)
if key in self._parsers:
return self._parsers[key].parse_config(file_path)

# Then fallback to (suffix, None)
key = (file_path.suffix, None)
if key in self._parsers:
return self._parsers[key].parse_config(file_path)

raise errors.UnsupportedConfigError(f"No parser registered for {file_path}")
1 change: 0 additions & 1 deletion src/maison/config_sources/__init__.py

This file was deleted.

54 changes: 0 additions & 54 deletions src/maison/config_sources/base_source.py

This file was deleted.

31 changes: 0 additions & 31 deletions src/maison/config_sources/ini_source.py

This file was deleted.

20 changes: 0 additions & 20 deletions src/maison/config_sources/pyproject_source.py

This file was deleted.

38 changes: 0 additions & 38 deletions src/maison/config_sources/toml_source.py

This file was deleted.

Loading
Loading