Skip to content

Commit

Permalink
Add converters from typed-settings
Browse files Browse the repository at this point in the history
See: #813
  • Loading branch information
sscherfke committed Jun 5, 2021
1 parent 7cc3121 commit 1fa8d08
Show file tree
Hide file tree
Showing 3 changed files with 581 additions and 2 deletions.
232 changes: 232 additions & 0 deletions src/attr/converters.py
Expand Up @@ -4,6 +4,8 @@

from __future__ import absolute_import, division, print_function

from datetime import datetime

from ._compat import PY2
from ._make import NOTHING, Factory, pipe

Expand All @@ -17,6 +19,13 @@
"pipe",
"optional",
"default_if_none",
"to_attrs",
"to_bool",
"to_dt",
"to_iterable",
"to_mapping",
"to_tuple",
"to_union",
]


Expand Down Expand Up @@ -109,3 +118,226 @@ def default_if_none_converter(val):
return default

return default_if_none_converter


def to_attrs(cls):
"""
A converter that creates an instance of *cls* from a dict but leaves
instances of that class as they are.
Classes can define a ``from_dict()`` classmethod which will be called
instead of the their `__init__()`. This can be useful if you want to
create different sub classes of *cls* depending on the data (e.g.,
a ``Cat`` or a ``Dog`` inheriting ``Animal``).
:param type cls: The class to convert data to.
:returns: The converter function for *cls*.
:rtype: callable
"""
type_ = cls.from_dict if hasattr(cls, "from_dict") else cls

def convert(val):
if not isinstance(val, (cls, dict)):
raise TypeError(
f'Invalid type "{type(val).__name__}"; expected '
f'"{cls.__name__}" or "dict".'
)
return type_(**val) if isinstance(val, dict) else val

n = cls.__name__
convert.__doc__ = f"""
Convert *data* to an intance of {n} if it is not already an instance
of it.
:param Union[dict, {n}] data: The input data
:returns: The converted data
:rtype: {n}
:raises TypeError: if *data* is neither a dict nor an instance of {n}.
"""

return convert


def to_bool(val):
"""
Convert "boolean" strings (e.g., from env. vars.) to real booleans.
Values mapping to :code:`True`:
- :code:`True`
- :code:`"true"` / :code:`"t"`
- :code:`"yes"` / :code:`"y"`
- :code:`"on"`
- :code:`"1"`
- :code:`1`
Values mapping to :code:`False`:
- :code:`False`
- :code:`"false"` / :code:`"f"`
- :code:`"no"` / :code:`"n"`
- :code:`"off"`
- :code:`"0"`
- :code:`0`
Raise :exc:`ValueError` for any other value.
"""
if isinstance(val, str):
val = val.lower()
truthy = {True, "true", "t", "yes", "y", "on", "1", 1}
falsy = {False, "false", "f", "no", "n", "off", "0", 0}
try:
if val in truthy:
return True
if val in falsy:
return False
except TypeError:
# Raised when "val" is not hashable (e.g., lists)
pass
raise ValueError(f"Cannot convert value to bool: {val}")


def to_dt(val):
"""
Convert an ISO formatted string to :class:`datetime.datetime`. Leave the
input untouched if it is already a datetime.
See: :func:`datetime.datetime.fromisoformat()`
The ``Z`` suffix is also supported and will be replaced with ``+00:00``.
:param Union[str,datetime.datetime] data: The input data
:returns: A parsed datetime object
:rtype: datetime.datetime
:raises TypeError: If *val* is neither a str nor a datetime.
"""
if not isinstance(val, (datetime, str)):
raise TypeError(
f'Invalid type "{type(val).__name__}"; expected "datetime" or '
f'"str".'
)
if isinstance(val, str):
if val[-1] == "Z":
val = val.replace("Z", "+00:00")
return datetime.fromisoformat(val)
return val


def to_enum(cls):
"""
Return a converter that creates an instance of the :class:`.Enum` *cls*.
If the to be converted value is not already an enum, the converter will
first try to create one by name (``MyEnum[val]``) and, if that fails, by
value (``MyEnum(val)``).
"""

def convert(val):
if isinstance(val, cls):
return val
try:
return cls[val]
except KeyError:
return cls(val)

return convert


def to_iterable(cls, converter):
"""
A converter that creates a *cls* iterable (e.g., ``list``) and calls
*converter* for each element.
:param Type[Iterable] cls: The type of the iterable to create
:param callable converter: The converter to apply to all items of the
input data.
:returns: The converter function
:rtype: callable
"""

def convert(val):
return cls(converter(d) for d in val)

return convert


def to_tuple(cls, converters):
"""
A converter that creates a struct-like tuple (or namedtuple or similar)
and converts each item via the corresponding converter from *converters*
The input value must have exactly as many elements as there are converters.
:param Type[Tuple] cls: The type of the tuple to create
:param List[callable] converters: The respective converters for each tuple
item.
:returns: The converter function
:rtype: callable
"""

def convert(val):
if len(val) != len(converters):
raise TypeError(
"Value must have {} items but has: {}".format(
len(converters), len(val)
)
)
return cls(c(v) for c, v in zip(converters, val))

return convert


def to_mapping(cls, key_converter, val_converter):
"""
A converter that creates a mapping and converts all keys and values using
the respective converters.
:param Type[Mapping] cls: The mapping type to create (e.g., ``dict``).
:param callable key_converter: The converter function to apply to all keys.
:param callable val_converter: The converter function to apply to all
values.
:returns: The converter function
:rtype: callable
"""

def convert(val):
return cls(
(key_converter(k), val_converter(v)) for k, v in val.items()
)

return convert


def to_union(converters):
"""
A converter that applies a number of converters to the input value and
returns the result of the first converter that does not raise a
:exc:`TypeError` or :exc:`ValueError`.
If the input value already has one of the required types, it will be
returned unchanged.
:param List[callable] converters: A list of converters to try on the input.
:returns: The converter function
:rtype: callable
"""

def convert(val):
if type(val) in converters:
# Preserve val as-is if it already has a matching type.
# Otherwise float(3.2) would be converted to int
# if the converters are [int, float].
return val
for converter in converters:
try:
return converter(val)
except (TypeError, ValueError):
pass
raise ValueError(
"Failed to convert value to any Union type: {}".format(val)
)

return convert
68 changes: 67 additions & 1 deletion src/attr/converters.pyi
@@ -1,7 +1,23 @@
from typing import Callable, Optional, TypeVar, overload
from datetime import datetime
from enum import Enum
from typing import (
Any,
Callable,
Collection,
Iterable,
List,
Mapping,
Optional,
Tuple,
Type,
TypeVar,
Union,
overload,
)

from . import _ConverterType


_T = TypeVar("_T")

def pipe(*validators: _ConverterType) -> _ConverterType: ...
Expand All @@ -10,3 +26,53 @@ def optional(converter: _ConverterType) -> _ConverterType: ...
def default_if_none(default: _T) -> _ConverterType: ...
@overload
def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ...

def to_attrs(cls: Type[_T]) -> Callable[[Union[_T, dict]], _T]: ...

def to_dt(val: Union[datetime, str]) -> datetime: ...

def to_bool(val: Union[bool, int, str]) -> bool: ...

_E = TypeVar("_E", bound=Enum)
def to_enum(cls: Type[_E]) -> Callable[[Union[_E, Any]], _E]: ...

# This is currently not expressible:
# cls: Type[_ITER]
# converter: Callable[[Any], _T]
# return: _ITER[_T]
_ITER = TypeVar("_ITER", bound=Iterable)
def to_iterable(
cls: Type[_ITER], converter: Callable[[Any], _T]
) -> Callable[[Iterable], _ITER]: ...

# This is currently not expressible:
# cls: Type[_TUPEL]
# converters: List[Callable[[Any], T1], Callable[[Any], T2], ...]
# return: Callable[[Collection], _TUPEL[T1, T2, ...]
_TUPLE = TypeVar("_TUPLE", bound=Tuple)

def to_tuple(
cls: Type[_TUPLE], converters: List[Callable[[Any], _T]]
) -> _TUPLE: ...

_MAP = TypeVar("_MAP", bound=Mapping)
_KT = TypeVar("_KT")
_VT = TypeVar("_VT")

# This is currently not expressible:
# cls: Type[_MAP]
# key_converter: Callable[[Any], _KT],
# val_converter: Callable[[Any], _VT],
# return: _MAP[_KT, _VT]
def to_mapping(
cls: Type[_MAP],
key_converter: Callable[[Any], _KT],
val_converter: Callable[[Any], _VT],
) -> _MAP: ...

# This is currently not expressible:
# converter: List[Callable[[Any], _T1], Callable[[Any], _T2], ...]
# return: Callable[[Any], Union[T1, T2, ...]]
def to_union(
converters: List[Callable[[Any], Any]]
) -> Callable[[Any], Any]: ...

0 comments on commit 1fa8d08

Please sign in to comment.