Package castfit
castfit: basic type casting
Cuddles the Cat
"If it fits, I sits."
Why?
I'm writing more and more type-checked code, but I often get a bunch of strings I need to convert (e.g., from docopt
).
pydantic
feels heavy.type-docopt
uses a new syntax.bottle
seems like good inspiration for small, useful libraries.
Install
python -m pip install castfit
Alternatively, you can just download the single file and name it castfit.py
.
Example
from pathlib import Path
from castfit import castfit
class Cat:
name: str
age: int
weight: float
logo: Path
bob = castfit(Cat, dict(name="Bob", age="4", weight="3.2", logo="./bob.png"))
assert bob.name == "Bob"
assert bob.age == 4
assert bob.weight == 3.2
assert bob.logo == Path("./bob.png")
License
Expand source code
"""castfit: basic type casting
.. include:: ../../README.md
:start-line: 2
"""
# std
from __future__ import annotations
from dataclasses import is_dataclass
from datetime import datetime
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import get_args
from typing import get_origin
from typing import get_type_hints
from typing import List
from typing import Literal
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import sys
if sys.version_info >= (3, 11): # pragma: no cover
from types import NoneType
from typing import Never
else: # pragma: no cover
NoneType = type(None)
Never = NoReturn
__all__ = [
"__version__",
"__pubdate__",
#
# imported
"Never",
"NoneType",
#
# types
"Ignored",
"TypeForm",
"CheckFn",
"Checks",
"CastFn",
"Casts",
#
# top-level API
"castfit",
"is_type",
"to_type",
#
# extensions
"checks_type",
"casts_to",
#
# lower-level API
"get_origin_type",
"setattrs",
"is_any",
"to_any",
"is_never",
"to_never",
"is_none",
"to_none",
"is_literal",
"to_literal",
"is_union",
"to_union",
"to_bytes",
"to_str",
"is_list",
"to_list",
"is_set",
"to_set",
"is_dict",
"to_dict",
"is_tuple",
"to_tuple",
"to_datetime",
]
__version__ = "0.1.0"
__pubdate__ = "2023-12-15T12:12:04Z"
K = TypeVar("K")
"""Type variable for keys."""
T = TypeVar("T")
"""Type variable for values."""
Ignored = Optional[Any]
"""A function argument that is ignored."""
TypeForm = Union[Type[T], Any]
"""`Type` and special forms like `All`, `Union`, etc."""
# NOTE: We want just `Type[T]`, but `mypy` treats special forms as `object`.
CheckFn = Callable[[Any, TypeForm[Any]], bool]
"""Function signature that checks if a value is of a type."""
Checks = Dict[TypeForm[Any], CheckFn]
"""Type of internal mapping of types to check functions."""
TYPE_CHECKS: Checks = {}
"""Mapping of types to check functions."""
CastFn = Callable[[Any, TypeForm[T]], T]
"""Function signature that maps a value to a type."""
Casts = Dict[TypeForm[Any], CastFn[Any]]
"""Type of internal mapping of types to cast functions."""
TYPE_CASTS: Casts = {}
"""Mapping of types to cast functions."""
@overload
def castfit(spec: Type[T], data: Dict[str, Any]) -> T:
...
@overload
def castfit(spec: T, data: Dict[str, Any]) -> T:
...
def castfit(spec, data): # type: ignore[no-untyped-def]
"""Construct a `spec` using `data` that has been cast appropriately."""
type_hints = get_type_hints(spec)
typed_data: Dict[str, Any] = {
name: to_type(value, type_hints.get(name, Any)) for name, value in data.items()
}
if is_dataclass(spec) and isinstance(spec, type):
return spec(**typed_data)
result = spec
if isinstance(spec, type):
result = spec()
return setattrs(result, **typed_data)
def is_type(value: Any, kind: TypeForm[Any], checks: Optional[Checks] = None) -> bool:
"""Return `True` if `value` is of a type compatible with `kind`."""
# TODO [2024-10-14]: @ py3.8 EOL, make return type `TypeGuard[T]`
checks = checks or TYPE_CHECKS
origin = get_origin(kind) or kind
checker = checks.get(origin)
if checker:
return checker(value, kind)
return isinstance(value, kind)
def to_type(
value: Any,
kind: TypeForm[T],
checks: Optional[Checks] = None,
casts: Optional[Casts] = None,
) -> T:
"""Try to cast `value` to the type of `kind`."""
checks = checks or TYPE_CHECKS
if is_type(value, kind, checks): # already done
return cast(T, value)
casts = casts or TYPE_CASTS
origin = get_origin(kind) or kind
caster = casts.get(origin)
if caster:
return cast(T, caster(value, kind))
try:
return cast(T, origin(value)) # type: ignore[call-arg]
except Exception:
raise TypeError(f"Cannot cast {value!r} to {kind}")
def get_origin_type(given: TypeForm[T]) -> Type[T]:
"""Returns the `given` type, its origin, or `type(obj)`.
See: [typing.get_origin](https://docs.python.org/3/library/typing.html#typing.get_origin)
"""
origin = get_origin(given) or given
if isinstance(origin, type):
return cast(Type[T], origin) # cast due to mypy
return cast(Type[T], type(given)) # cast due to mypy
def setattrs(obj: object, **values: Dict[str, Any]) -> object:
"""Like `setattr()` but for multiple values and returns the object."""
for name, val in values.items():
setattr(obj, name, val)
return obj
def checks_type(*types: Any) -> Callable[[CheckFn], CheckFn]:
"""Define a type-checker for one or more types."""
def _factory(func: CheckFn) -> CheckFn:
for t in types:
TYPE_CHECKS[t] = func
return func
return _factory
def casts_to(*types: Any) -> Callable[[T], T]:
"""Define a type-caster for one or more types."""
def _factory(func: T) -> T:
for t in types:
TYPE_CASTS[t] = cast(CastFn[Any], func)
return func
return _factory
@checks_type(Any)
def is_any(_value: Ignored = None, _kind: Ignored = Any) -> bool:
"""Always return `True`."""
return True
@casts_to(Any)
def to_any(value: T, _kind: Ignored = Any) -> T:
"""Always return `value`."""
return value
@checks_type(Never, NoReturn)
def is_never(_value: Ignored = None, _kind: Ignored = Never) -> bool:
"""Always return `False`."""
return False
@casts_to(Never, NoReturn)
def to_never(value: Any, _kind: Ignored = Never) -> NoReturn:
"""Always raise a `TypeError`."""
raise TypeError(f"Cannot cast {value!r} to Never (nothing can)")
@checks_type(NoneType)
def is_none(value: Any, _kind: Ignored = None) -> bool:
"""Return `True` if `value` is `None`."""
return value is None
@casts_to(NoneType)
def to_none(_value: Ignored = None, _kind: Ignored = None) -> None:
"""Always return `None`."""
return None
@checks_type(Literal)
def is_literal(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `Literal`."""
return value in get_args(kind)
@casts_to(Literal)
def to_literal(value: T, kind: TypeForm[T]) -> T:
"""Return `value` if it is one of the `Literal` values."""
if not is_literal(value, kind):
raise TypeError(f"Cannot cast {value!r} to {kind}")
return value
@checks_type(Union)
def is_union(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `Union`."""
return any(is_type(value, val_type) for val_type in get_args(kind))
@casts_to(Union)
def to_union(value: Any, kind: TypeForm[T]) -> T:
for arg in get_args(kind):
try:
return cast(T, to_type(value, arg))
except (TypeError, ValueError):
pass
raise TypeError(f"Cannot cast {value!r} to {kind}")
@casts_to(bytes)
def to_bytes(value: Any, kind: Type[bytes] = bytes) -> bytes:
"""Cast `value` into `bytes`, encoding `str` as UTF-8 bytes if needed."""
if isinstance(value, str):
return value.encode("utf-8")
cls: Type[bytes] = get_origin_type(kind)
return cls(value)
@casts_to(str)
def to_str(value: Any, kind: Type[str] = str) -> str:
"""Cast `value` into `str`, decoding `bytes` as UTF-8 strings if needed."""
if isinstance(value, bytes):
return value.decode("utf-8")
cls: Type[str] = get_origin_type(kind)
return cls(value)
@checks_type(list)
def is_list(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `list`."""
if not isinstance(value, list):
return False
if len(value) == 0: # list() matches List[Any]
return True
vt = get_args(kind)[0]
return all(is_type(v, vt) for v in value)
@casts_to(list)
def to_list(value: Any, kind: TypeForm[List[T]] = list) -> List[T]:
"""Cast `value` into `list`."""
cls: Type[List[T]] = get_origin_type(kind)
val_type = get_args(kind)[0]
return cls(to_type(val, val_type) for val in value)
@checks_type(set)
def is_set(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `set`."""
if not isinstance(value, set):
return False
if len(value) == 0: # set() matches Set[Any]
return True
val_type = get_args(kind)[0]
return all(is_type(val, val_type) for val in value)
@casts_to(set)
def to_set(value: Any, kind: TypeForm[Set[T]] = set) -> Set[T]:
"""Cast `value` into `set`."""
cls: Type[Set[T]] = get_origin_type(kind)
val_type = get_args(kind)[0]
return cls(to_type(val, val_type) for val in value)
@checks_type(dict)
def is_dict(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `dict`."""
if not isinstance(value, dict):
return False
if len(value) == 0: # dict() matches Dict[Any, Any]
return True
kt, vt = get_args(kind)
return all(is_type(k, kt) and is_type(v, vt) for k, v in value.items())
@casts_to(dict)
def to_dict(value: Any, kind: TypeForm[Dict[K, T]] = dict) -> Dict[K, T]:
"""Cast `value` into a `dict`."""
cls: Type[Dict[K, T]] = get_origin_type(kind)
if len(value) == 0:
return cls()
kt, vt = get_args(kind)
return cls({to_type(k, kt): to_type(v, vt) for k, v in value.items()})
@checks_type(tuple)
def is_tuple(value: Any, kind: TypeForm[T]) -> bool:
"""Return `True` if `value` is a valid `tuple`."""
args = get_args(kind)
if not isinstance(value, tuple):
return False
if len(args) == 0 or args == ((),): # special empty-tuple format
return value == ()
if len(args) > 1 and args[1] == ...:
args = args[:1] * len(value)
return len(value) == len(args) and all(is_type(v, vt) for v, vt in zip(value, args))
@casts_to(tuple)
def to_tuple(value: Any, kind: TypeForm[Tuple[Any, ...]] = tuple) -> Tuple[Any, ...]:
"""Cast `value` into a `tuple`."""
cls: Type[Tuple[Any, ...]] = get_origin_type(kind)
args = get_args(kind)
if len(value) == 0 and len(args) == 0 or args == ((),):
return cls()
if len(args) > 1 and args[1] == ...:
args = args[:1] * len(value)
if len(value) < len(args):
raise ValueError(f"Not enough values in {value!r} to cast to {kind}")
return cls(to_type(val, val_type) for val, val_type in zip(value, args))
@casts_to(datetime)
def to_datetime(value: Any, kind: Type[datetime] = datetime) -> datetime:
"""Cast `value` into a `datetime`."""
cls: Type[datetime] = get_origin_type(kind)
# TODO: Handle other kinds of casts (e.g., int -> datetime)
return cls.fromisoformat(value)
Global variables
var Ignored
-
A function argument that is ignored.
var TypeForm
-
Type
and special forms likeAll
,Union
, etc. var CheckFn
-
Function signature that checks if a value is of a type.
var Checks
-
Type of internal mapping of types to check functions.
var CastFn
-
Function signature that maps a value to a type.
var Casts
-
Type of internal mapping of types to cast functions.
Functions
def castfit(spec, data)
-
Construct a
spec
usingdata
that has been cast appropriately.Expand source code
def castfit(spec, data): # type: ignore[no-untyped-def] """Construct a `spec` using `data` that has been cast appropriately.""" type_hints = get_type_hints(spec) typed_data: Dict[str, Any] = { name: to_type(value, type_hints.get(name, Any)) for name, value in data.items() } if is_dataclass(spec) and isinstance(spec, type): return spec(**typed_data) result = spec if isinstance(spec, type): result = spec() return setattrs(result, **typed_data)
def is_type(value: Any, kind: TypeForm[Any], checks: Optional[Checks] = None) ‑> bool
-
Return
True
ifvalue
is of a type compatible withkind
.Expand source code
def is_type(value: Any, kind: TypeForm[Any], checks: Optional[Checks] = None) -> bool: """Return `True` if `value` is of a type compatible with `kind`.""" # TODO [2024-10-14]: @ py3.8 EOL, make return type `TypeGuard[T]` checks = checks or TYPE_CHECKS origin = get_origin(kind) or kind checker = checks.get(origin) if checker: return checker(value, kind) return isinstance(value, kind)
def to_type(value: Any, kind: TypeForm[T], checks: Optional[Checks] = None, casts: Optional[Casts] = None) ‑> ~T
-
Try to cast
value
to the type ofkind
.Expand source code
def to_type( value: Any, kind: TypeForm[T], checks: Optional[Checks] = None, casts: Optional[Casts] = None, ) -> T: """Try to cast `value` to the type of `kind`.""" checks = checks or TYPE_CHECKS if is_type(value, kind, checks): # already done return cast(T, value) casts = casts or TYPE_CASTS origin = get_origin(kind) or kind caster = casts.get(origin) if caster: return cast(T, caster(value, kind)) try: return cast(T, origin(value)) # type: ignore[call-arg] except Exception: raise TypeError(f"Cannot cast {value!r} to {kind}")
def checks_type(*types: Any) ‑> Callable[[Callable[[Any, Union[Type[Any], Any]], bool]], Callable[[Any, Union[Type[Any], Any]], bool]]
-
Define a type-checker for one or more types.
Expand source code
def checks_type(*types: Any) -> Callable[[CheckFn], CheckFn]: """Define a type-checker for one or more types.""" def _factory(func: CheckFn) -> CheckFn: for t in types: TYPE_CHECKS[t] = func return func return _factory
def casts_to(*types: Any) ‑> Callable[[~T], ~T]
-
Define a type-caster for one or more types.
Expand source code
def casts_to(*types: Any) -> Callable[[T], T]: """Define a type-caster for one or more types.""" def _factory(func: T) -> T: for t in types: TYPE_CASTS[t] = cast(CastFn[Any], func) return func return _factory
def get_origin_type(given: TypeForm[T]) ‑> Type[~T]
-
Returns the
given
type, its origin, ortype(obj)
.See: typing.get_origin
Expand source code
def get_origin_type(given: TypeForm[T]) -> Type[T]: """Returns the `given` type, its origin, or `type(obj)`. See: [typing.get_origin](https://docs.python.org/3/library/typing.html#typing.get_origin) """ origin = get_origin(given) or given if isinstance(origin, type): return cast(Type[T], origin) # cast due to mypy return cast(Type[T], type(given)) # cast due to mypy
def setattrs(obj: object, **values: Dict[str, Any]) ‑> object
-
Like
setattr()
but for multiple values and returns the object.Expand source code
def setattrs(obj: object, **values: Dict[str, Any]) -> object: """Like `setattr()` but for multiple values and returns the object.""" for name, val in values.items(): setattr(obj, name, val) return obj
def is_any() ‑> bool
-
Always return
True
.Expand source code
@checks_type(Any) def is_any(_value: Ignored = None, _kind: Ignored = Any) -> bool: """Always return `True`.""" return True
def to_any(value: T) ‑> ~T
-
Always return
value
.Expand source code
@casts_to(Any) def to_any(value: T, _kind: Ignored = Any) -> T: """Always return `value`.""" return value
def is_never() ‑> bool
-
Always return
False
.Expand source code
@checks_type(Never, NoReturn) def is_never(_value: Ignored = None, _kind: Ignored = Never) -> bool: """Always return `False`.""" return False
def to_never(value: Any) ‑> NoReturn
-
Always raise a
TypeError
.Expand source code
@casts_to(Never, NoReturn) def to_never(value: Any, _kind: Ignored = Never) -> NoReturn: """Always raise a `TypeError`.""" raise TypeError(f"Cannot cast {value!r} to Never (nothing can)")
def is_none(value: Any) ‑> bool
-
Return
True
ifvalue
isNone
.Expand source code
@checks_type(NoneType) def is_none(value: Any, _kind: Ignored = None) -> bool: """Return `True` if `value` is `None`.""" return value is None
def to_none() ‑> None
-
Always return
None
.Expand source code
@casts_to(NoneType) def to_none(_value: Ignored = None, _kind: Ignored = None) -> None: """Always return `None`.""" return None
def is_literal(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validLiteral
.Expand source code
@checks_type(Literal) def is_literal(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `Literal`.""" return value in get_args(kind)
def to_literal(value: T, kind: TypeForm[T]) ‑> ~T
-
Return
value
if it is one of theLiteral
values.Expand source code
@casts_to(Literal) def to_literal(value: T, kind: TypeForm[T]) -> T: """Return `value` if it is one of the `Literal` values.""" if not is_literal(value, kind): raise TypeError(f"Cannot cast {value!r} to {kind}") return value
def is_union(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validUnion
.Expand source code
@checks_type(Union) def is_union(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `Union`.""" return any(is_type(value, val_type) for val_type in get_args(kind))
def to_union(value: Any, kind: TypeForm[T]) ‑> ~T
-
Expand source code
@casts_to(Union) def to_union(value: Any, kind: TypeForm[T]) -> T: for arg in get_args(kind): try: return cast(T, to_type(value, arg)) except (TypeError, ValueError): pass raise TypeError(f"Cannot cast {value!r} to {kind}")
def to_bytes(value: Any, kind: Type[bytes] = builtins.bytes) ‑> bytes
-
Cast
value
intobytes
, encodingstr
as UTF-8 bytes if needed.Expand source code
@casts_to(bytes) def to_bytes(value: Any, kind: Type[bytes] = bytes) -> bytes: """Cast `value` into `bytes`, encoding `str` as UTF-8 bytes if needed.""" if isinstance(value, str): return value.encode("utf-8") cls: Type[bytes] = get_origin_type(kind) return cls(value)
def to_str(value: Any, kind: Type[str] = builtins.str) ‑> str
-
Cast
value
intostr
, decodingbytes
as UTF-8 strings if needed.Expand source code
@casts_to(str) def to_str(value: Any, kind: Type[str] = str) -> str: """Cast `value` into `str`, decoding `bytes` as UTF-8 strings if needed.""" if isinstance(value, bytes): return value.decode("utf-8") cls: Type[str] = get_origin_type(kind) return cls(value)
def is_list(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validlist
.Expand source code
@checks_type(list) def is_list(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `list`.""" if not isinstance(value, list): return False if len(value) == 0: # list() matches List[Any] return True vt = get_args(kind)[0] return all(is_type(v, vt) for v in value)
def to_list(value: Any, kind: TypeForm[List[T]] = builtins.list) ‑> List[~T]
-
Cast
value
intolist
.Expand source code
@casts_to(list) def to_list(value: Any, kind: TypeForm[List[T]] = list) -> List[T]: """Cast `value` into `list`.""" cls: Type[List[T]] = get_origin_type(kind) val_type = get_args(kind)[0] return cls(to_type(val, val_type) for val in value)
def is_set(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validset
.Expand source code
@checks_type(set) def is_set(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `set`.""" if not isinstance(value, set): return False if len(value) == 0: # set() matches Set[Any] return True val_type = get_args(kind)[0] return all(is_type(val, val_type) for val in value)
def to_set(value: Any, kind: TypeForm[Set[T]] = builtins.set) ‑> Set[~T]
-
Cast
value
intoset
.Expand source code
@casts_to(set) def to_set(value: Any, kind: TypeForm[Set[T]] = set) -> Set[T]: """Cast `value` into `set`.""" cls: Type[Set[T]] = get_origin_type(kind) val_type = get_args(kind)[0] return cls(to_type(val, val_type) for val in value)
def is_dict(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validdict
.Expand source code
@checks_type(dict) def is_dict(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `dict`.""" if not isinstance(value, dict): return False if len(value) == 0: # dict() matches Dict[Any, Any] return True kt, vt = get_args(kind) return all(is_type(k, kt) and is_type(v, vt) for k, v in value.items())
def to_dict(value: Any, kind: TypeForm[Dict[K, T]] = builtins.dict) ‑> Dict[~K, ~T]
-
Cast
value
into adict
.Expand source code
@casts_to(dict) def to_dict(value: Any, kind: TypeForm[Dict[K, T]] = dict) -> Dict[K, T]: """Cast `value` into a `dict`.""" cls: Type[Dict[K, T]] = get_origin_type(kind) if len(value) == 0: return cls() kt, vt = get_args(kind) return cls({to_type(k, kt): to_type(v, vt) for k, v in value.items()})
def is_tuple(value: Any, kind: TypeForm[T]) ‑> bool
-
Return
True
ifvalue
is a validtuple
.Expand source code
@checks_type(tuple) def is_tuple(value: Any, kind: TypeForm[T]) -> bool: """Return `True` if `value` is a valid `tuple`.""" args = get_args(kind) if not isinstance(value, tuple): return False if len(args) == 0 or args == ((),): # special empty-tuple format return value == () if len(args) > 1 and args[1] == ...: args = args[:1] * len(value) return len(value) == len(args) and all(is_type(v, vt) for v, vt in zip(value, args))
def to_tuple(value: Any, kind: TypeForm[Tuple[Any, ...]] = builtins.tuple) ‑> Tuple[Any, ...]
-
Cast
value
into atuple
.Expand source code
@casts_to(tuple) def to_tuple(value: Any, kind: TypeForm[Tuple[Any, ...]] = tuple) -> Tuple[Any, ...]: """Cast `value` into a `tuple`.""" cls: Type[Tuple[Any, ...]] = get_origin_type(kind) args = get_args(kind) if len(value) == 0 and len(args) == 0 or args == ((),): return cls() if len(args) > 1 and args[1] == ...: args = args[:1] * len(value) if len(value) < len(args): raise ValueError(f"Not enough values in {value!r} to cast to {kind}") return cls(to_type(val, val_type) for val, val_type in zip(value, args))
def to_datetime(value: Any, kind: Type[datetime] = datetime.datetime) ‑> datetime.datetime
-
Cast
value
into adatetime
.Expand source code
@casts_to(datetime) def to_datetime(value: Any, kind: Type[datetime] = datetime) -> datetime: """Cast `value` into a `datetime`.""" cls: Type[datetime] = get_origin_type(kind) # TODO: Handle other kinds of casts (e.g., int -> datetime) return cls.fromisoformat(value)
Classes
class NoneType (...)