Skip to content

Commit

Permalink
Merge branch 'development'
Browse files Browse the repository at this point in the history
  • Loading branch information
robinvandernoord committed Jun 19, 2023
2 parents 78f6f10 + 57a21c2 commit fc17dda
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 7 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,12 @@ if __name__ == '__main__':
# Hello World!
print(my_config.reference.numbers)
# [41, 43]

# TypedConfig has an extra benefit of allowing .update:
my_config.update(numbers=[68, 70])
```

More examples can be round in [examples](https://github.com/trialandsuccess/configuraptor/blob/master/examples).
More examples can be found in [examples](https://github.com/trialandsuccess/configuraptor/blob/master/examples).

## License

Expand Down
48 changes: 44 additions & 4 deletions examples/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
For basic usage, see [../README.md](https://github.com/trialandsuccess/configuraptor/blob/master/README.md#usage)
and [./example_from_readme.py](https://github.com/trialandsuccess/configuraptor/blob/master/examples/example_from_readme.py).
Normal classes can be used with `configuraptor` (with `load_into(YourClass, data, ...)`) or you can
inherit `TypedConfig` (and
use `YourClass.load(data, ...)`).
inherit `TypedConfig` (and use `YourClass.load(data, ...)`).

In the examples above, `data` can be either

Expand Down Expand Up @@ -124,8 +123,49 @@ See also: [./dataclass.py](https://github.com/trialandsuccess/configuraptor/blob

## Inheriting from TypedConfig

Currently, Inheriting from TypedConfig only adds the functionality of doing `MyClass.load`.
However, more functionality could follow later.
In addition to the `MyClass.load` shortcut, inheriting from TypedConfig also gives you the ability to `.update` your
config instances. Update will check whether the type you're trying to assign to a property is inline with its
annotation. By default, `None` values will be skipped to preserve the default or previous value.
These two features can be bypassed with `strict=False` and `allow_none=True` respectively.

```python
from configuraptor import TypedConfig


class SomeConfig(TypedConfig):
string: str
num_key: int


config = SomeConfig.load("./some/config.toml")

assert config.string != "updated"
config.update(string="updated")
assert config.string == "updated"

# `string` will not be updated:
config.update(string=None)
assert config.string == "updated"

# `string` will be updated:
config.update(string=None, allow_none=True)
assert config.string is None

# will raise a `ConfigErrorInvalidType`:
config.update(string=123)

# will work:
config.update(string=123, strict=False)
assert config.string == 123

# will raise a `ConfigErrorExtraKey`:
config.update(new_key="some value")

# will work:
config.update(new_key="some value", strict=False)
assert config.new_key == "some value"

```

## Existing Instances

Expand Down
27 changes: 26 additions & 1 deletion src/configuraptor/cls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import typing

from .core import T_data, load_into
from .core import T_data, all_annotations, check_type, load_into
from .errors import ConfigErrorExtraKey, ConfigErrorInvalidType

C = typing.TypeVar("C", bound=typing.Any)

Expand All @@ -24,3 +25,27 @@ def load(
SomeClass.load(data, ...) = load_into(SomeClass, data, ...).
"""
return load_into(cls, data, key=key, init=init, strict=strict)

def update(self, strict: bool = True, allow_none: bool = False, **values: typing.Any) -> None:
"""
Update values on this config.
Args:
strict: allow wrong types?
allow_none: allow None or skip those entries?
**values: key: value pairs in the right types to update.
"""
annotations = all_annotations(self.__class__)

for key, value in values.items():
if value is None and not allow_none:
continue

if strict and key not in annotations:
raise ConfigErrorExtraKey(cls=self.__class__, key=key, value=value)

if strict and not check_type(value, annotations[key]) and not (value is None and allow_none):
raise ConfigErrorInvalidType(expected_type=annotations[key], key=key, value=value)

setattr(self, key, value)
28 changes: 28 additions & 0 deletions src/configuraptor/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,34 @@ def __str__(self) -> str:
)


@dataclass
class ConfigErrorExtraKey(ConfigError):
"""
Exception for when the config file is missing a required key.
"""

key: str
value: str
cls: type

def __post_init__(self) -> None:
"""
Automatically filles in the names of annotated type and cls for printing from __str__.
"""
self._cls = self.cls.__name__
self._type = type(self.value)

def __str__(self) -> str:
"""
Custom error message based on dataclass values and calculated actual type.
"""
return (
f"Config key '{self.key}' (value: `{self.value}` type `{self._type}`) "
f"does not exist on class `{self._cls}`, but was attempted to be updated. "
f"Use strict = False to allow this behavior."
)


@dataclass
class ConfigErrorInvalidType(ConfigError):
"""
Expand Down
32 changes: 31 additions & 1 deletion tests/test_toml_typedconfig_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

from src import configuraptor
from src.configuraptor.errors import ConfigError
from src.configuraptor.errors import ConfigError, ConfigErrorInvalidType, ConfigErrorExtraKey

from .constants import EMPTY_FILE, EXAMPLE_FILE, _load_toml

Expand Down Expand Up @@ -106,3 +106,33 @@ def test_typedconfig_classes():
first = First.load(EXAMPLE_FILE, key="tool.first")

assert tool.first.extra["name"]["first"] == first.extra["name"]["first"]


def test_typedconfig_update():
first = First.load(EXAMPLE_FILE, key="tool.first")

assert first.string != "updated"
first.update(string="updated")
assert first.string == "updated"

first.update(string=None)
assert first.string == "updated"

first.update(string=None, allow_none=True)
assert first.string is None

with pytest.raises(ConfigErrorInvalidType):
first.update(string=123)

first.update(string=123, strict=False)
assert first.string == 123

with pytest.raises(ConfigErrorExtraKey):
try:
first.update(new_key="some value")
except ConfigErrorExtraKey as e:
assert "new_key" in str(e)
raise e

first.update(new_key="some value", strict=False)
assert first.new_key == "some value"

0 comments on commit fc17dda

Please sign in to comment.