Skip to content

Commit

Permalink
fix/update cattrs (#17)
Browse files Browse the repository at this point in the history
* fix: publish to pypi

* fix: update cattrs and get rid of attr.ib used for defaults and help
  • Loading branch information
diefans authored Sep 10, 2024
1 parent ad47b55 commit 9b2b6a9
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 121 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
matrix:
platform:
- manylinux2014_x86_64
- manylinux2014_aarch64
version:
- cp312-cp312
- cp311-cp311
Expand Down Expand Up @@ -111,6 +112,7 @@ jobs:
matrix:
platform:
- manylinux2014_x86_64
- manylinux2014_aarch64
version:
- cp312-cp312
- cp311-cp311
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def finalize_options(self):

install_requires = [
"attrs",
"cattrs<=22.1.0",
"cattrs",
"structlog>=20.1.0",
"toml>=0.10",
"typing_inspect>=0.4.0",
Expand Down
119 changes: 21 additions & 98 deletions src/buvar/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,75 +6,19 @@
import attr
import cattr
import structlog
import typing_inspect

from . import di, util

CNF_KEY = "buvar_config"


logger = structlog.get_logger()


# since we only need a single instance, we just hide the class magically
@functools.partial(lambda x: x())
class missing:
def __repr__(self):
return self.__class__.__name__


@attr.s(auto_attribs=True)
class ConfigValue:
name: typing.Optional[str] = None
help: typing.Optional[str] = None


def var(
default=missing,
converter=None,
factory=missing,
name=None,
validator=None,
help=None, # noqa: W0622,
):
return attr.ib(
metadata={CNF_KEY: ConfigValue(name, help)},
converter=converter,
validator=validator,
**({"default": default} if factory is missing else {"factory": factory}),
)


def _env_to_bool(val):
"""
Convert *val* to a bool if it's not a bool in the first place.
"""
if isinstance(val, bool):
return val
val = val.strip().lower()
if val in ("1", "true", "yes", "on"):
return True

return False


def _env_to_list(val):
"""Take a comma separated string and split it."""
if isinstance(val, str):
val = map(lambda x: x.strip(), val.split(","))
return val


def bool_var(default=missing, name=None, help=None): # noqa: W0622
return var(default=default, name=name, converter=_env_to_bool, help=help)


def list_var(default=missing, name=None, help=None): # noqa: W0622
return var(default=default, name=name, converter=_env_to_list, help=help)


class ConfigSource(dict):

"""Config dict, with loadable sections.
>>> @attr.s(auto_attribs=True)
Expand Down Expand Up @@ -117,8 +61,7 @@ def load(self, config_cls, name=None):
return config


class ConfigError(Exception):
...
class ConfigError(Exception): ...


# to get typing_inspect.is_generic_type()
Expand All @@ -133,7 +76,7 @@ class Config:
__buvar_config_section__: typing.Optional[str] = skip_section
__buvar_config_sections__: typing.Dict[str, type] = {}

def __init_subclass__(cls, *, section: str = skip_section, **kwargs):
def __init_subclass__(cls, *, section: str = skip_section, **_):
if section is skip_section:
return
if section in cls.__buvar_config_sections__:
Expand Down Expand Up @@ -201,45 +144,25 @@ def create_env_config(cls, *env_prefix):
return env_config


class RelaxedConverter(cattr.Converter):
"""This py:obj:`RelaxedConverter` is ignoring undefined and defaulting to
None on optional attributes."""

def structure_attrs_fromdict(
self, obj: typing.Mapping, cl: typing.Type
) -> typing.Any:
"""Instantiate an attrs class from a mapping (dict)."""
conv_obj = {}
dispatch = self._structure_func.dispatch
for a in attr.fields(cl):
# We detect the type by metadata.
type_ = a.type
if type_ is None:
# No type.
continue
name = a.name
try:
val = obj[name]
except KeyError:
if typing_inspect.is_optional_type(type_):
if a.default in (missing, attr.NOTHING):
val = None
else:
val = a.default
elif a.default in (missing, attr.NOTHING):
raise ValueError("Attribute is missing", a.name)
else:
continue

if a.converter is None:
val = dispatch(type_)(val, type_)

conv_obj[name] = val

return cl(**conv_obj)


relaxed_converter = RelaxedConverter()
# FIXME: deprecate relaxed_converter
converter = relaxed_converter = cattr.Converter()


def _env_to_bool(val, type):
"""
Convert *val* to a bool if it's not a bool in the first place.
"""
if isinstance(val, type):
return val
elif isinstance(val, str):
val = val.strip().lower()
if val in ("1", "true", "yes", "on"):
return True

return False


relaxed_converter.register_structure_hook(bool, _env_to_bool)


def generate_env_help(cls, env_prefix=""):
Expand Down
43 changes: 21 additions & 22 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async def test_config_source_schematize(mocker):
class FooConfig:
bar: str = "default"
foobar: float = 9.87
baz: bool = config.bool_var(default=False)
baz: bool = False

@attr.s(auto_attribs=True)
class BarConfig:
Expand All @@ -25,9 +25,9 @@ class BarConfig:
@attr.s(auto_attribs=True, kw_only=True)
class BimConfig:
bar: BarConfig
bam: bool = config.bool_var()
bum: int = config.var(123)
lst: typing.List = config.var(list)
bam: bool
bum: int = 123
lst: typing.List

sources = [
{"bar": {"bim": "123.4", "foo": {"bar": "1.23", "baz": "true"}}},
Expand All @@ -53,6 +53,7 @@ class BimConfig:
bim = cfg.load(BimConfig, "bim")
bar = cfg.load(BarConfig, "bar")
foo = cfg.load(FooConfig, "foo")
# (FooConfig(bar="value", foobar=123.5, baz=True),)

assert (bar, foo, bim) == (
BarConfig(bim=0.0, foo=FooConfig(bar="1.23", foobar=7.77, baz=False)),
Expand All @@ -74,7 +75,7 @@ async def test_config_generic_adapter(mocker):
class FooConfig(config.Config, section="foo"):
bar: str = "default"
foobar: float = 9.87
baz: bool = config.bool_var(default=False)
baz: bool = False

@attr.s(auto_attribs=True)
class BarConfig(config.Config, section="bar"):
Expand All @@ -84,9 +85,9 @@ class BarConfig(config.Config, section="bar"):
@attr.s(auto_attribs=True, kw_only=True)
class BimConfig(config.Config, section="bim"):
bar: BarConfig
bam: bool = config.bool_var()
bum: int = config.var(123)
lst: typing.List = config.var(list)
bam: bool
bum: int = 123
lst: typing.List

sources = [
{"bar": {"bim": "123.4", "foo": {"bar": "1.23", "baz": "true"}}},
Expand Down Expand Up @@ -130,16 +131,17 @@ class GeneralVars:

def test_config_missing():
import attr
from cattrs.errors import ClassValidationError
from buvar import config

source: dict = {"foo": {}}

@attr.s(auto_attribs=True)
class FooConfig:
bar: str = config.var()
bar: str

cfg = config.ConfigSource(source)
with pytest.raises(ValueError):
with pytest.raises(ClassValidationError):
cfg.load(FooConfig, "foo")


Expand All @@ -156,11 +158,11 @@ class FooConfig:
bim bam
"""

string_val: str = config.var(help="string")
float_val: float = config.var(9.87, help="float")
bool_val: bool = config.bool_var(help="bool")
int_val: int = config.var(help="int")
list_val: typing.List = config.var(list, help="list")
string_val: str
float_val: float = 9.87
bool_val: bool
int_val: int
list_val: typing.List

@attr.s(auto_attribs=True)
class BarConfig:
Expand All @@ -171,7 +173,7 @@ class BarConfig:
"""

bim: float
foo: FooConfig = config.var(help="foo")
foo: FooConfig

env_vars = {}
config_fields = list(config.traverse_attrs(BarConfig, target=env_vars))
Expand Down Expand Up @@ -279,20 +281,17 @@ async def test_config_subclass_abc(mocker):

mocker.patch.dict(config.Config.__buvar_config_sections__, clear=True)

class GeneralConfig(config.Config, section=None):
...
class GeneralConfig(config.Config, section=None): ...

class FooBase(metaclass=abc.ABCMeta):
@abc.abstractmethod
def foo(self):
...
def foo(self): ...

@attr.s(auto_attribs=True)
class FooConfig(config.Config, FooBase, section="foo"):
bar: str

def foo(self):
...
def foo(self): ...

assert config.skip_section not in config.Config.__buvar_config_sections__
assert FooBase not in config.Config.__buvar_config_sections__.values()
Expand Down

0 comments on commit 9b2b6a9

Please sign in to comment.