You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
6.4 KiB
Python

"""Collection of functions for building custom `json_default` functions.
In general functions come in pairs of `use_x_default` and `x_default`, where the former is used
to determine if you should call the latter.
Most `use_x_default` functions also act as a [`TypeGuard`](https://mypy.readthedocs.io/en/stable/type_narrowing.html#user-defined-type-guards).
"""
### IMPORTS
### ============================================================================
## Future
from __future__ import annotations
## Standard Library
import base64
import dataclasses
import datetime
import enum
import sys
from types import TracebackType
from typing import Any
import traceback
import uuid
if sys.version_info >= (3, 10):
from typing import TypeGuard
else:
from typing_extensions import TypeGuard
## Installed
## Application
### FUNCTIONS
### ============================================================================
def unknown_default(obj: Any) -> str:
"""Backup default function for any object type.
Will attempt to use `str` or `repr`. If both functions error will return
the string `"__could_not_encode__"`.
Args:
obj: object to handle
"""
try:
return str(obj)
except Exception: # pylint: disable=broad-exception-caught
pass
try:
return repr(obj)
except Exception: # pylint: disable=broad-exception-caught
pass
return "__could_not_encode__"
## Types
## -----------------------------------------------------------------------------
def use_type_default(obj: Any) -> TypeGuard[type]:
"""Default check function for `type` objects (aka classes)."""
return isinstance(obj, type)
def type_default(obj: type) -> str:
"""Default function for `type` objects.
Args:
obj: object to handle
"""
return obj.__name__
## Dataclasses
## -----------------------------------------------------------------------------
def use_dataclass_default(obj: Any) -> bool:
"""Default check function for dataclass instances"""
return dataclasses.is_dataclass(obj) and not isinstance(obj, type)
def dataclass_default(obj) -> dict[str, Any]:
"""Default function for dataclass instances
Args:
obj: object to handle
"""
return dataclasses.asdict(obj)
## Dates and Times
## -----------------------------------------------------------------------------
def use_time_default(obj: Any) -> TypeGuard[datetime.time]:
"""Default check function for `datetime.time` instances"""
return isinstance(obj, datetime.time)
def time_default(obj: datetime.time) -> str:
"""Default function for `datetime.time` instances
Args:
obj: object to handle
"""
return obj.isoformat()
def use_date_default(obj: Any) -> TypeGuard[datetime.date]:
"""Default check function for `datetime.date` instances"""
return isinstance(obj, datetime.date)
def date_default(obj: datetime.date) -> str:
"""Default function for `datetime.date` instances
Args:
obj: object to handle
"""
return obj.isoformat()
def use_datetime_default(obj: Any) -> TypeGuard[datetime.datetime]:
"""Default check function for `datetime.datetime` instances"""
return isinstance(obj, datetime.datetime)
def datetime_default(obj: datetime.datetime) -> str:
"""Default function for `datetime.datetime` instances
Args:
obj: object to handle
"""
return obj.isoformat()
def use_datetime_any(obj: Any) -> TypeGuard[datetime.time | datetime.date | datetime.datetime]:
"""Default check function for `datetime` related instances"""
return isinstance(obj, (datetime.time, datetime.date, datetime.datetime))
def datetime_any(obj: datetime.time | datetime.date | datetime.date) -> str:
"""Default function for `datetime` related instances
Args:
obj: object to handle
"""
return obj.isoformat()
## Exception and Tracebacks
## -----------------------------------------------------------------------------
def use_exception_default(obj: Any) -> TypeGuard[BaseException]:
"""Default check function for exception instances.
Exception classes are not treated specially and should be handled by the
`[use_]type_default` functions.
"""
return isinstance(obj, BaseException)
def exception_default(obj: BaseException) -> str:
"""Default function for exception instances
Args:
obj: object to handle
"""
return f"{obj.__class__.__name__}: {obj}"
def use_traceback_default(obj: Any) -> TypeGuard[TracebackType]:
"""Default check function for tracebacks"""
return isinstance(obj, TracebackType)
def traceback_default(obj: TracebackType) -> str:
"""Default function for tracebacks
Args:
obj: object to handle
"""
return "".join(traceback.format_tb(obj)).strip()
## Enums
## -----------------------------------------------------------------------------
def use_enum_default(obj: Any) -> TypeGuard[enum.Enum | enum.EnumMeta]:
"""Default check function for enums.
Supports both enum classes and enum values.
"""
return isinstance(obj, (enum.Enum, enum.EnumMeta))
def enum_default(obj: enum.Enum | enum.EnumMeta) -> Any | list[Any]:
"""Default function for enums.
Supports both enum classes and enum values.
Args:
obj: object to handle
"""
if isinstance(obj, enum.Enum):
return obj.value
return [e.value for e in obj] # type: ignore[var-annotated]
## UUIDs
## -----------------------------------------------------------------------------
def use_uuid_default(obj: Any) -> TypeGuard[uuid.UUID]:
"""Default check function for `uuid.UUID` instances"""
return isinstance(obj, uuid.UUID)
def uuid_default(obj: uuid.UUID) -> str:
"""Default function for `uuid.UUID` instances
Formats the UUID using "hyphen" format.
Args:
obj: object to handle
"""
return str(obj)
## Bytes
## -----------------------------------------------------------------------------
def use_bytes_default(obj: Any) -> TypeGuard[bytes | bytearray]:
"""Default check function for bytes"""
return isinstance(obj, (bytes, bytearray))
def bytes_default(obj: bytes | bytearray, url_safe: bool = True) -> str:
"""Default function for bytes
Args:
obj: object to handle
url_safe: use URL safe base 64 character set.
Returns:
The byte data as a base 64 string.
"""
if url_safe:
return base64.urlsafe_b64encode(obj).decode("utf8")
return base64.b64encode(obj).decode("utf8")