Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add converters from typed-settings #820

Closed
wants to merge 3 commits into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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]: ...