Source code for bpack.typing

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

import re
from typing import NamedTuple, Optional, Type, Union

try:
    from typing import _tp_cache  # noqa
except ImportError:
    def _tp_cache(x):
        return x

try:                                                        # pragma: no cover
    # @COMPATIBILITY: with Python < 3.9
    from typing_extensions import Annotated
    # @COMPATIBILITY: with Python < 3.7 (and Python < 3.8.3)
    from typing_extensions import get_origin, get_args
except ImportError:                                         # pragma: no cover
    from typing import Annotated, get_origin, get_args

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): byteorder: Optional[EByteOrder] type: FieldTypes size: Optional[int] signed: Optional[bool] def __repr__(self): 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)
[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): raise TypeError(f'Type "{cls.__name__}" cannot be instantiated.') @_tp_cache def __class_getitem__(cls, params): 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): 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