"""Promotion and conversion functions."""
__all__ = [
"convert",
"add_conversion_method",
"conversion_method",
"add_promotion_rule",
"promote",
]
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, TypeVar
from beartype.door import TypeHint
import plum._function
from ._bear import is_bearable
from ._dispatcher import Dispatcher
from ._type import resolve_type_hint
from .repr import repr_short
T = TypeVar("T")
R = TypeVar("R")
if TYPE_CHECKING:
TypeTo = TypeVar("TypeTo")
typeTypeTo: TypeAlias = type[TypeTo]
else:
TypeTo = Any
typeTypeTo = Any
_dispatch = Dispatcher()
[docs]
@_dispatch
def convert(obj: object, type_to: typeTypeTo) -> TypeTo:
"""Convert an object to a particular type.
Args:
obj (object): Object to convert.
type_to (type): Type to convert to.
Returns:
object: `obj` converted to type `type_to`.
"""
type_to = resolve_type_hint(type_to)
# TODO: Can we implement this without using `type`?!
return _convert.invoke(type(obj), type_to)(obj, type_to)
# Deliver `convert`.
plum._function._promised_convert = convert
@_dispatch
def _convert(obj, type_to):
if not is_bearable(obj, resolve_type_hint(type_to)):
raise TypeError(f"Cannot convert `{obj}` to `{repr_short(type_to)}`.")
return obj
class _ConversionCallable(Protocol[T, R]):
def __call__(self, obj: T) -> R: ...
[docs]
def add_conversion_method(
type_from: type[T],
type_to: type[R],
f: _ConversionCallable[T, R],
) -> None:
"""Add a conversion method to convert an object from one type to another.
Args:
type_from (type): Type to convert from.
type_to (type): Type to convert to.
f (function): Function that converts an object of type `type_from` to
type `type_to`.
"""
@_convert.dispatch
def perform_conversion(obj: type_from, _: type_to):
return f(obj)
[docs]
def conversion_method(
type_from: type[T], type_to: type[R]
) -> Callable[[_ConversionCallable[T, R]], _ConversionCallable[T, R]]:
"""Decorator to add a conversion method to convert an object from one
type to another.
Args:
type_from (type): Type to convert from.
type_to (type): Type to convert to.
"""
def add_method(f: _ConversionCallable[T, R]) -> _ConversionCallable[T, R]:
add_conversion_method(type_from, type_to, f)
return f
return add_method
# Add some common conversion methods.
add_conversion_method(object, tuple, lambda x: (x,))
add_conversion_method(tuple, tuple, lambda x: x)
add_conversion_method(list, tuple, tuple)
add_conversion_method(object, list, lambda x: [x])
add_conversion_method(list, list, lambda x: x)
add_conversion_method(tuple, list, list)
add_conversion_method(bytes, str, lambda x: x.decode("utf-8", "replace"))
@_dispatch
def _promotion_rule(type1, type2):
"""Promotion rule.
Args:
type1 (type): First type to promote.
type2 (type): Second type to promote.
Returns:
type: Type to convert to.
"""
type1 = resolve_type_hint(type1)
type2 = resolve_type_hint(type2)
if TypeHint(type1) <= TypeHint(type2):
return type2
elif TypeHint(type2) <= TypeHint(type1):
return type1
else:
raise TypeError(
f"No promotion rule for `{repr_short(type1)}` and `{repr_short(type2)}`."
)
@_dispatch
def promote(obj1, obj2, *objs):
"""Promote objects to a common type.
Args:
\\*objs (object): Objects to convert.
Returns:
tuple: `objs`, but all converted to a common type.
"""
# Convert to a single tuple.
objs = (obj1, obj2) + objs
# Get the types of the objects.
# TODO: Can we implement this without calling `type`?!
types = [type(obj) for obj in objs]
def _promote_types(t0, t1):
return resolve_type_hint(_promotion_rule.invoke(t0, t1)(t0, t1))
# Find the common type.
_promotion_rule._resolve_pending_registrations()
common_type = _promote_types(types[0], types[1])
for t in types[2:]:
common_type = _promote_types(common_type, t)
# Convert objects and return.
return tuple(convert(obj, common_type) for obj in objs)
@_dispatch
def promote(obj: object): # noqa: F811
# Promote should always return a tuple to avoid edge cases.
return (obj,)