Source code for bpack.typing

"""bpack support for type annotations."""

import re
from typing import (
    Annotated,
    NamedTuple,
    Optional,
    Type,
    Union,
    get_args,
    get_origin,
)

# @COMPATIBILITY: available in Python 3.7 ... 3.11
try:
    from typing import _tp_cache
except ImportError:

    def _tp_cache(x):
        return x


from .enums import EByteOrder

__all__ = ["T", "TypeParams", "is_annotated"]


_DTYPE_RE = re.compile(
    r"^(?P<byteorder>[<|>])?" r"(?P<type>[?bBiufcmMUVOSat])" r"(?P<size>\d+)?$"
)


FieldTypes = Type[Union[bool, int, float, complex, bytes, str]]


[docs] class TypeParams(NamedTuple): """Named tuple describing type parameters.""" byteorder: Optional[EByteOrder] type: FieldTypes # noqa: A003 size: Optional[int] signed: Optional[bool] def __repr__(self): """Return the string representation of the TypeParams object.""" byteorder = self.byteorder byteorder = repr(byteorder) if byteorder is not None else byteorder size = str(self.size) if self.size is not None else self.size return ( f"{self.__class__.__name__}(byteorder={byteorder}, " f"type={self.type.__name__!r}, size={size}, signed={self.signed})" )
def str_to_type_params(typestr: str) -> TypeParams: """Convert a string describing a data type into type parameters. The ``typestr`` parameter is a string describing a data type. The *typestr* string format consists of 3 parts: * an (optional) character describing the byte order of the data - ``<``: little-endian, - ``>``: big-endian, - ``|``: not-relevant * a character code giving the basic type of the array, and * an integer providing the number of bytes the type uses The basic type character codes are: * ``i``: sighed integer * ``u``: unsigned integer * ``f``: float * ``c``: complex * ``S``: bytes (string) .. note:: *typestr* the format described above is a sub-set of the one used in the numpy "array interface". .. seealso:: https://numpy.org/doc/stable/reference/arrays.dtypes.html and https://numpy.org/doc/stable/reference/arrays.interface.html """ mobj = _DTYPE_RE.match(typestr) if mobj is None: raise ValueError(f"invalid data type specifier: '{typestr}'") byteorder = mobj.group("byteorder") stype = mobj.group("type") size = mobj.group("size") signed = None if size is not None: size = int(size) if size <= 0: raise ValueError(f"invalid size: '{size}'") if byteorder == "|": byteorder = None elif byteorder is not None: byteorder = EByteOrder(byteorder) # if stype == '?' or (stype == 'b' and size == 1): # type_ = bool # elif stype in 'bB': # type_ = bytes # elif stype == 'i': if stype == "i": type_ = int signed = True elif stype == "u": type_ = int signed = False elif stype == "f": type_ = float elif stype == "c": type_ = complex # elif stype == 'm': # type_ = datetime.timedelta # elif stype == 'M': # type_ = datetime.datetime # elif stype == 'U': # type_ = str elif stype == "S": type_ = bytes # elif stype == 'V': # type_ = bytes else: # '?': bool # 'b': (signed) byte (single item) # 'B': (unsigned) byte (single item) # 't': bitfield # 'O': object # 'U': (unicode) str (32bit UCS4 encoding) # 'a' : null terminated strings # 'm', 'M': timedelta and datetime raise TypeError( f"type specifier '{stype}' is valid for the 'array protocol' but " f"not supported by bpack" ) return TypeParams(byteorder, type_, size, signed) def type_params_to_str(params: TypeParams) -> str: """Convert a ``TypeParams`` object into a ``typestr``. The returned ``typestr`` is a string describing a data type. .. seealso:: please refer to :func:`bpack.typing.str_to_type_params` for a detailed description of the *typestr* string format. """ byteorder = params.byteorder byteorder = "" if byteorder is None else EByteOrder(byteorder).value if params.type is int: if params.signed: type_ = "i" else: type_ = "u" elif params.type is float: type_ = "f" elif params.type is complex: type_ = "c" # elif params.type is datetime.timedelta: # type_ = "m" # elif params.type is datetime.datetime: # type_ = "M" # elif params.type is str: # type_ = "U" elif params.type is bytes: type_ = "S" # type_ = "V" else: raise TypeError(f"data type '{params.type}' is not supported in bpack") size = params.size if params.size is not None else "" return f"{byteorder}{type_}{size}"
[docs] class T: """Allow to specify numeric type annotations using string descriptors. Example:: >>> T['u4'] # doctest: +NORMALIZE_WHITESPACE typing.Annotated[int, TypeParams(byteorder=None, type='int', size=4, signed=False)] The resulting type annotation is a :class:`typing.Annotated` numeric type with attached a :class:`bpack.typing.TypeParams` instance. String descriptors, or *typestr*, are compatible with numpy (a sub-set of one used in the numpy "array interface"). The *typestr* string format consists of 3 parts: * an (optional) character describing the byte order of the data - ``<``: little-endian, - ``>``: big-endian, - ``|``: not-relevant * a character code giving the basic type of the array, and * an integer providing the number of bytes the type uses The basic type character codes are: * ``i``: sighed integer * ``u``: unsigned integer * ``f``: float * ``c``: complex * ``S``: bytes (string) .. note:: *typestr* the format described above is a sub-set of the one used in the numpy "array interface". .. seealso:: :func:`str_to_type_params`, :class:`TypeParams`, https://numpy.org/doc/stable/reference/arrays.dtypes.html and https://numpy.org/doc/stable/reference/arrays.interface.html """ __slots__ = () def __new__(cls, *args, **kwargs): """Initialize a new `T` descriptor.""" raise TypeError(f"Type '{cls.__name__}' cannot be instantiated.") @_tp_cache def __class_getitem__(cls, params): # noqa: D105, N805 if not isinstance(params, str): raise TypeError( f"{cls.__name__}[...] should be used with a single argument " f"(a string describing a basic numeric type)." ) typestr = params metadata = str_to_type_params(typestr) return Annotated[metadata.type, metadata] def __init_subclass__(cls, *args, **kwargs): """Subclass initializer. Alway raise a TypeError to prevent sub-classing. """ raise TypeError(f"Cannot subclass {cls.__module__}.{cls.__name__}")
[docs] def is_annotated(type_: Type) -> bool: """Return True if the input is an annotated numeric type. An *annotated numeric type* is assumed to be a :class:`typing.Annotated` type annotation of a basic numeric type with attached a :class:`bpack.typing.TypeParams` instance. .. seealso:: :class:`bpack.typing.T`. """ if get_origin(type_) is Annotated: args = get_args(type_) if len(args) == 2: etype, params = args return isinstance(etype, type) and isinstance(params, TypeParams) return False