Source code for dolor.types.type

"""Base code for types of packet fields."""

import abc
import inspect
import copy

from .. import util
from ..versions import Version, VersionSwitcher

[docs]class TypeContext: """The context for a :class:`Type`. Parameters ---------- instance : :class:`~.Packet`, optional The packet instance that's being marshaled. ctx : :class:`~.PacketContext` or :class:`~.Version` or :class:`str` or :class:`int`, optional The context for the packet or the version to use for marshaling. Attributes ---------- instance : :class:`~.Packet` or ``None`` The packet instance that's being marshaled. version : :class:`~.Version` The version to use for marshaling. """ def __init__(self, instance=None, ctx=None): self.instance = instance if ctx is None or isinstance(ctx, (Version, str, int)): self.version = Version(ctx) else: self.version = ctx.version
[docs]class Type(abc.ABC): """Base class for types of packet fields. This class is used for marshaling raw data (typically in packets) to and from values, and it has some interesting quirks. For one, calling their constructor doesn't really construct them, but rather make new types. Take :class:`~.string.String` for instance: It can take an argument for the string's maximum length:: >>> import dolor >>> dolor.types.String.max_length 32767 >>> s = dolor.types.String(20) >>> s <class 'dolor.types.string.String(20)'> >>> s.max_length 20 To really get a :class:`Type` object, use the :meth:`descriptor` method, like so:: >>> import dolor >>> dolor.types.VarInt.descriptor("attr_name") # doctest: +SKIP <dolor.types.numeric.VarInt object at 0x7fe5ad4e5490> You can also generate arrays of types like so:: >>> import dolor >>> dolor.types.Short[1] <class 'dolor.types.array.Short[1]'> .. seealso:: :meth:`__class_getitem__` To marshal data to a value, see the :meth:`unpack` method. To marshal a value to data, see the :meth:`pack` method. """ _default = None
[docs] @classmethod def __init_subclass__(cls, **kwargs): """Spiffs up subclasses. If the :attr:`_default` attribute is a :class:`dict`, then it changes it to a :class:`~.VersionSwitcher`. It also sets :meth:`__new__` to :meth:`_call`. If you want to initialize a :class:`Type`, see :meth:`descriptor`. """ super().__init_subclass__(**kwargs) if isinstance(cls._default, dict): cls._default = VersionSwitcher(cls._default) # Set __new__ to _call's underlying function. # We don't just override __new__ instead of # _call so that it's more clear that calling # a Type is separate from actually initializing # an instance of Type. cls.__new__ = cls._call.__func__
[docs] @classmethod def descriptor(cls, name): """Gets the descriptor form of the type. Parameters ---------- name : :class:`str` The name of the packet field. Returns ------- :class:`Type` The descriptor form of the type. """ # Get the name here instead of in __set_name__ # to avoid the need for a metaclass for Packet. self = object.__new__(cls) self.name = name return self
def __get__(self, instance, owner=None): if instance is None: return self return instance._get_field(self.name) def __set__(self, instance, value): instance._set_field(self.name, value)
[docs] @classmethod def __class_getitem__(cls, index): """Gets an :class:`~.Array` of the type. Parameters ---------- index : :class:`int` or subclass of :class:`Type` or ``None`` The ``size`` argument passed to :class:`~.Array`. Examples -------- >>> import dolor >>> dolor.types.VarInt[1] <class 'dolor.types.array.VarInt[1]'> """ from .array import Array return Array(cls, index)
[docs] @classmethod def default(cls, *, ctx=None): """Gets the default value of the type. If the :attr:`_default` attribute is a classmethod, then it should look like this:: @classmethod def _default(cls, *, ctx=None): return my_default_value The return value of the classmethod will be returned from this method. If the :attr:`_default` attribute is a :class:`~.VersionSwitcher`, then the value for the corresponding version will be returned. Otherwise, if the :attr:`_default` attribute is any value other than ``None``, that value will be returned. Parameters ---------- ctx : :class:`TypeContext`, optional Returns ------- any The default value. Raises ------ :exc:`NotImplementedError` If the :attr:`_default` attribute is ``None``. """ if cls._default is None: raise NotImplementedError if inspect.ismethod(cls._default): return cls._default(ctx=ctx) if isinstance(cls._default, VersionSwitcher): default = cls._default[ctx.version] else: default = cls._default # Deepcopy because the default could be mutable return copy.deepcopy(default)
[docs] @classmethod def unpack(cls, buf, *, ctx=None): """Gets the corresponding value from the buffer. Warnings -------- Do **not** override this method. Instead override :meth:`_unpack`. Parameters ---------- buf : file object or :class:`bytes` or :class:`bytearray` The buffer containing the raw data. ctx : :class:`TypeContext`, optional Returns ------- any The corresponding value from the buffer. """ buf = util.file_object(buf) return cls._unpack(buf, ctx=ctx)
[docs] @classmethod def pack(cls, value, *, ctx=None): """Packs a value into raw data. Warnings -------- Do **not** override this method. Instead override :meth:`pack`. Parameters ---------- value The value to pack. ctx : :class:`TypeContext`, optional Returns ------- :class:`bytes` The corresponding raw data. """ return cls._pack(value, ctx=ctx)
[docs] @classmethod @abc.abstractmethod def _unpack(cls, buf, *, ctx=None): """Gets the corresponding value from the buffer. To be overridden by subclasses. Warnings -------- Do not use this method directly, **always** use :meth:`unpack` instead. Parameters ---------- buf : file object The buffer containing the raw data. ctx : :class:`TypeContext`, optional Returns ------- any The corresponding value from the buffer. """ raise NotImplementedError
[docs] @classmethod @abc.abstractmethod def _pack(cls, value, *, ctx=None): """Packs a value into raw data. To be overridden by subclasses. Warnings -------- Do not use this method directly, **always** use :meth:`pack` instead. Parameters ---------- value The value to pack. ctx : :class:`TypeContext`, optional Returns ------- :class:`bytes` The corresponding raw data. """ raise NotImplementedError
[docs] @classmethod def make_type(cls, name, bases=None, **namespace): """Utility for generating new types. The generated type's :attr:`__module__` attribute is set to be the same as the origin type's. This is done to get around an issue where generated types would have their :attr:`__module__` attribute be ``"abc"`` because :class:`Type` inherits from :class:`abc.ABC`. Parameters ---------- name : :class:`str` The generated type's name. bases : :class:`tuple`, optional The generated type's base classes. If unspecified, the origin type is the sole base class. **namespace The attributes and corresponding values of the generated type. Returns ------- subclass of :class:`Type` The generated type. """ if bases is None: bases = (cls,) namespace["__module__"] = cls.__module__ return type(name, bases, namespace)
@classmethod def _call(cls): # Called when the type's constructor is called. # # The arguments passed to the constructor get forwarded # to this method. typically overridden to enable # generating new types. raise NotImplementedError