Skip to content

Commit

Permalink
feat: better format and schema selection
Browse files Browse the repository at this point in the history
  • Loading branch information
Gabriel Pajot authored and gpajot committed Sep 19, 2022
1 parent 0663941 commit 67913be
Show file tree
Hide file tree
Showing 20 changed files with 143 additions and 164 deletions.
31 changes: 11 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,12 @@ Currently, those formats are supported:
- TOML - requires the `toml` extra

The format is automatically inferred from the config file extension.
You can still specify it manually using `Config.FORMAT`, for custom ones or configuring dump options.
Other formats can be added by subclassing either `Format` or `ReadOnlyFormat`.

To support more formats:
```python
from zenconfig import Config
Other formats can be added by subclassing `Format`.

Config.FORMATS.append(MyFormat)
# or
Config.FORMATS = [MyFormat]
```
To register more formats: `Config.register_format(MyFormat)`.

You can also force the format using `Config.FORMAT = MyFormat(...)`. This can be used to disable auto selection, or pass parameters to the format.

## Supported schemas
Currently, those schemas are supported:
Expand All @@ -75,8 +70,13 @@ Currently, those schemas are supported:
- pydantic models - requires the `pydantic` extra


The format is automatically inferred from the config class.
You can still specify it manually using `Config.SCHEMA`, for custom ones or configuring dump options.
The schema is automatically inferred from the config class.

Other schemas can be added by subclassing `Schema`.

To register more schemas: `Config.register_schema(MySchema)`.

You can also force the schema using `Config.SCHEMA = MySchema(...)`. This can be used to disable auto selection, or pass parameters to the schema.

To use pydantic:
```python
Expand All @@ -91,14 +91,5 @@ class MyPydanticConfig(Config, BaseModel):
> to all class variable you override
> otherwise pydantic will treat those as its own fields and complain.
To support more schemas:
```python
from zenconfig import Config

Config.SCHEMAS.append(MySchema)
# or
Config.SCHEMAS = [MySchema]
```

## Contributing
See [contributing guide](https://github.com/gpajot/zen-config/blob/main/CONTRIBUTING.md).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "zenconfig"
version = "1.0.0"
version = "1.1.0"
description = "Simple configuration loader for python."
authors = ["Gabriel Pajot <gab@les-cactus.co>"]
license = "MIT"
Expand Down
4 changes: 1 addition & 3 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@

import pytest

from zenconfig.base import BaseConfig
from zenconfig.formats.abc import Format
from zenconfig.schemas.abc import Schema
from zenconfig.base import BaseConfig, Format, Schema


class TestBaseConfig:
Expand Down
Empty file added tests/test_config_file.json
Empty file.
3 changes: 1 addition & 2 deletions tests/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import pytest

from zenconfig.formats.abc import Format
from zenconfig.base import Format, Schema
from zenconfig.read import ReadOnlyConfig
from zenconfig.schemas.abc import Schema


class TestReadOnlyConfig:
Expand Down
3 changes: 1 addition & 2 deletions tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import pytest

from zenconfig.formats.abc import Format
from zenconfig.schemas.abc import Schema
from zenconfig.base import Format, Schema
from zenconfig.write import Config


Expand Down
3 changes: 3 additions & 0 deletions zenconfig/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
import zenconfig.formats
import zenconfig.schemas
from zenconfig.base import Format, Schema
from zenconfig.read import ReadOnlyConfig
from zenconfig.write import Config
75 changes: 56 additions & 19 deletions zenconfig/base.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,61 @@
import os
from abc import ABC
from abc import ABC, abstractmethod
from pathlib import Path
from typing import ClassVar, List, Union
from typing import Any, ClassVar, Dict, Generic, List, Type, TypeVar, Union

from zenconfig.formats.abc import Format
from zenconfig.formats.selectors import FormatSelector, format_selectors
from zenconfig.schemas.abc import Schema
from zenconfig.schemas.selectors import SchemaSelector, schema_selectors

class Format(ABC):
@classmethod
@abstractmethod
def handles(cls, path: Path) -> bool:
"""Return whether the format handles the extension."""

@abstractmethod
def load(self, path: Path) -> Dict[str, Any]:
"""Load the configuration file into a dict."""

@abstractmethod
def dump(self, path: Path, config: Dict[str, Any]) -> None:
"""Dump in the configuration file."""


C = TypeVar("C")


class Schema(ABC, Generic[C]):
@classmethod
@abstractmethod
def handles(cls, config_class: type) -> bool:
"""Return whether the schema handles the config."""

@abstractmethod
def from_dict(self, cls: Type[C], cfg: Dict[str, Any]) -> C:
"""Load the schema based on a dict configuration."""

@abstractmethod
def to_dict(self, config: Any) -> Dict[str, Any]:
"""Dump the config to dict."""


class BaseConfig(ABC):
ENV_PATH: ClassVar[str] = "CONFIG"
PATH: ClassVar[Union[str, None]] = None
_PATH: ClassVar[Union[Path, None]] = None

FORMATS: ClassVar[List[FormatSelector]] = format_selectors
FORMATS: ClassVar[List[Type[Format]]] = []
FORMAT: ClassVar[Union[Format, None]] = None

SCHEMAS: ClassVar[List[SchemaSelector]] = schema_selectors
SCHEMAS: ClassVar[List[Type[Schema]]] = []
SCHEMA: ClassVar[Union[Schema, None]] = None

@classmethod
def register_format(cls, format_class: Type[Format]) -> None:
cls.FORMATS.append(format_class)

@classmethod
def register_schema(cls, schema_class: Type[Schema]) -> None:
cls.SCHEMAS.append(schema_class)

@classmethod
def _path(cls) -> Path:
if cls._PATH:
Expand All @@ -41,24 +77,25 @@ def _format(cls) -> Format:
if cls.FORMAT:
return cls.FORMAT
ext = cls._path().suffix
for selector in cls.FORMATS:
fmt = selector(ext)
if fmt:
cls.FORMAT = fmt
return fmt
path = cls._path()
for format_class in cls.FORMATS:
if not format_class.handles(path):
continue
cls.FORMAT = format_class()
return cls.FORMAT
raise ValueError(
f"unsupported config file extension {ext} for config {cls.__qualname__}"
f"unsupported config file extension {ext} for config {cls.__qualname__}, maybe you are missing an extra"
)

@classmethod
def _schema(cls) -> Schema:
if cls.SCHEMA:
return cls.SCHEMA
for selector in cls.SCHEMAS:
schema = selector(cls)
if schema:
cls.SCHEMA = schema
return schema
for schema_class in cls.SCHEMAS:
if not schema_class.handles(cls):
continue
cls.SCHEMA = schema_class()
return cls.SCHEMA
raise ValueError(
f"could not infer config schema for config {cls.__qualname__}, maybe you are missing an extra"
)
8 changes: 8 additions & 0 deletions zenconfig/formats/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import contextlib

from zenconfig.formats.json import JSONFormat

with contextlib.suppress(ImportError):
from zenconfig.formats.yaml import YAMLFormat
with contextlib.suppress(ImportError):
from zenconfig.formats.toml import TOMLFormat
13 changes: 0 additions & 13 deletions zenconfig/formats/abc.py

This file was deleted.

9 changes: 8 additions & 1 deletion zenconfig/formats/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from pathlib import Path
from typing import Any, Dict

from zenconfig.formats.abc import Format
from zenconfig.base import BaseConfig, Format


@dataclass
Expand All @@ -12,6 +12,10 @@ class JSONFormat(Format):
sort_keys: bool = True
ensure_ascii: bool = False

@classmethod
def handles(cls, path: Path) -> bool:
return path.suffix == ".json"

def load(self, path: Path) -> Dict[str, Any]:
return json.loads(path.read_text())

Expand All @@ -24,3 +28,6 @@ def dump(self, path: Path, config: Dict[str, Any]) -> None:
ensure_ascii=self.ensure_ascii,
),
)


BaseConfig.register_format(JSONFormat)
42 changes: 0 additions & 42 deletions zenconfig/formats/selectors.py

This file was deleted.

16 changes: 14 additions & 2 deletions zenconfig/formats/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,27 @@
import tomli
import tomli_w

from zenconfig.formats.abc import Format
from zenconfig.base import BaseConfig, Format


@dataclass
class TOMLFormat(Format):
multiline_strings: bool = True

@classmethod
def handles(cls, path: Path) -> bool:
return path.suffix == ".toml"

def load(self, path: Path) -> Dict[str, Any]:
return tomli.loads(path.read_text())

def dump(self, path: Path, config: Dict[str, Any]) -> None:
path.write_text(tomli_w.dumps(config, multiline_strings=self.multiline_strings))
path.write_text(
tomli_w.dumps(
config,
multiline_strings=self.multiline_strings,
)
)


BaseConfig.register_format(TOMLFormat)
9 changes: 8 additions & 1 deletion zenconfig/formats/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@

import yaml

from zenconfig.formats.abc import Format
from zenconfig.base import BaseConfig, Format


@dataclass
class YAMLFormat(Format):
indent: int = 2
sort_keys: bool = True

@classmethod
def handles(cls, path: Path) -> bool:
return path.suffix in {".yml", ".yaml"}

def load(self, path: Path) -> Dict[str, Any]:
return yaml.safe_load(path.read_text())

def dump(self, path: Path, config: Dict[str, Any]) -> None:
path.write_text(
yaml.safe_dump(config, indent=self.indent, sort_keys=self.sort_keys)
)


BaseConfig.register_format(YAMLFormat)
7 changes: 7 additions & 0 deletions zenconfig/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import contextlib

from zenconfig.schemas.dataclass import DataclassSchema
from zenconfig.schemas.dict import DictSchema

with contextlib.suppress(ImportError):
from zenconfig.schemas.pydantic import PydanticSchema
14 changes: 0 additions & 14 deletions zenconfig/schemas/abc.py

This file was deleted.

0 comments on commit 67913be

Please sign in to comment.