Source code for nbtparse.semantics.fields

"""A field is part of a ``TAG_Compound`` that corresponds to a top-level tag.

It knows how to translate a tag or group of tags into a Pythonic value and
back again, and uses the descriptor protocol to make this translation
seamless.

Typical usage would be something like this::

    class FooFile(NBTFile):
        bar = LongField(u'Bar')

Thereafter, when saving or loading a :class:`FooFile`, the top-level
``TAG_Long`` called ``Bar`` will be accessible under the attribute :obj:`bar`.
The NBT version is still accessible under the ``FooFile.data`` attribute (or
by calling the :meth:`~nbtparse.semantics.nbtobject.NBTObject.to_nbt` method,
which is usually more correct).

Fields are a thin abstraction over the data dict in an :class:`NBTObject`;
modifying the dict may cause Fields to misbehave.

Idea shamelessly stolen from Django, but massively simplified.

"""

import abc
import collections.abc as cabc
import contextlib
import datetime as dt
import enum
import itertools
import logging
import struct
import types
import uuid
import warnings
import weakref

from ..syntax import tags, ids
from .. import exceptions, _utils


logger = logging.getLogger(__name__)


NBT_OBJECT = __name__.split('.')
NBT_OBJECT[-1] = 'nbtobject'
NBT_META = list(NBT_OBJECT)
NBT_META.append('NBTMeta')
NBT_META = '.'.join(NBT_META)
NBT_OBJECT.append('NBTObject')
NBT_OBJECT = '.'.join(NBT_OBJECT)


[docs]class AbstractField(metaclass=abc.ABCMeta): """Root of the Field class hierarchy.""" def __init__(self, *args, default=None, **kwargs): self.default = default """Default value used by :meth:`set_default`.""" super().__init__(*args, **kwargs) @abc.abstractmethod
[docs] def get_field_value(self, obj: NBT_OBJECT, field_name: str) -> object: raise NotImplementedError('AbstractField does not implement ' 'get_field_value.')
@abc.abstractmethod
[docs] def set_field_value(self, obj: NBT_OBJECT, field_name: str, value: object): raise NotImplementedError('AbstractField does not implement ' 'set_field_value.')
@abc.abstractmethod
[docs] def delete_field_value(self, obj: NBT_OBJECT, field_name: str): raise NotImplementedError('AbstractField does not implement ' 'delete_field_value.')
[docs] def set_default(self, obj: NBT_OBJECT, field_name: str): """Reset this field to its "default" value. Useful when constructing an NBTObject. """ logger.debug("Resetting %r on %s object to %s.", self, type(obj).__name__, self.default) self.set_field_value(obj, field_name, self.default)
[docs] def save(self, obj: NBT_OBJECT, field_name: str): """Hook called during :meth:`~.NBTObject.to_nbt`. Does nothing by default. """ pass
[docs] def load(self, obj: NBT_OBJECT, field_name: str): """Hook called during :meth:`~.NBTObject.from_nbt`. Does nothing by default. """ pass
[docs] def attach(self, owner: NBT_META): """Hook called on fields declared within a given class. Does nothing by default. """ pass
[docs]class MutableField(AbstractField): """Mutable variant of a field. Caches the Pythonic value and holds a reference to it. Such a value can be safely mutated in-place. Needed for mutable types like lists, as well as immutable types whose contents may be mutable, such as tuples. The value is cached in ``obj.__dict__[field_name]``. This class must precede any class which overrides :meth:`__get__`, :meth:`__set__`, and/or :meth:`__delete__` in the method resolution order. In particular, it must precede :class:`MultiField` or :class:`SingleField`. If you do not understand what that means, just make sure your code looks like this:: class FooField(MutableField, SingleField): pass ...rather than like this:: class FooField(SingleField, MutableField): pass """
[docs] def save(self, obj: NBT_OBJECT, field_name: str): try: value = obj.__dict__[field_name] except KeyError: super().delete_field_value(obj, field_name) else: super().set_field_value(obj, field_name, value)
[docs] def load(self, obj: NBT_OBJECT, field_name: str): value = super().get_field_value(obj, field_name) obj.__dict__[field_name] = value
[docs] def get_field_value(self, obj: NBT_OBJECT, field_name: str) -> object: if obj is None: return self logger.debug('Retrieving mutable value via %r from %s object', self, type(obj).__name__) try: result = obj.__dict__[field_name] except KeyError: logger.debug('Creating new mutable value (via %r, for %s object)', self, type(obj).__name__) result = super().get_field_value(obj, field_name) obj.__dict__[field_name] = result return result
[docs] def set_field_value(self, obj: NBT_OBJECT, field_name: str, value: object): obj.__dict__[field_name] = value
[docs] def delete_field_value(self, obj: NBT_OBJECT, field_name: str): obj.__dict__.pop(field_name, None) super().delete_field_value(obj, field_name)
[docs]class MultiField(AbstractField): """Field combining multiple top-level tags into one object. Controls a series of one or more top-level tags which should be combined into a single Pythonic object. If the tags are enclosed in a ``TAG_Compound``, consider using an :class:`NBTObjectField` instead. This is an abstract class. :meth:`to_python` will be called with the same number of arguments as :obj:`nbt_names` has elements, in the same order specified, and should return a value acceptable to :meth:`from_python`, which should produce a sequence of the same values passed as arguments to :meth:`to_python`. If a tag does not exist, :meth:`to_python` will be passed :obj:`None` instead of the corresponding tag. If :meth:`from_python` produces a :obj:`None` value, the corresponding tag will be removed. :obj:`default` should be a value acceptable to :meth:`from_python`, which will be called when setting the field to its default value. .. note:: In general, these methods should not have side effects, and should not assume they are called at any particular time. The results may be cached and this class may assume that composing the two conversion methods produces the identity function. The reliable and comfortable hook for "heavy" NBT manipulation is :meth:`save` and :meth:`load`. """ def __init__(self, nbt_names: (str, ...), *, default: object=None): self.nbt_names = nbt_names self.__doc__ = str(self) super().__init__(default=default)
[docs] def get_field_value(self, obj: NBT_OBJECT, field_name: str=None) -> object: if obj is None: return self raws = [] for nbt_name in self.nbt_names: logger.debug('Retrieving value %s from %s object', nbt_name, type(obj).__name__) raw_value = obj.data.get(nbt_name) raws.append(raw_value) args = tuple(raws) logger.debug('All values retrieved, converting to Pythonic') result = self.to_python(*args) return result
[docs] def set_field_value(self, obj: NBT_OBJECT, field_name: str, value: object): logger.debug('Converting %r to NBT', value) raws = self.from_python(value) for nbt_name, raw_value in zip(self.nbt_names, raws): if raw_value is None: logger.debug('Skipping value %s (for %s object)', nbt_name, type(obj).__name__) obj.data.pop(nbt_name, None) continue logger.debug('Storing value %s in %s object', nbt_name, type(obj).__name__) obj.data[nbt_name] = raw_value
[docs] def delete_field_value(self, obj: NBT_OBJECT, field_name: str): nones = (None,) * len(self.nbt_names) value = self.to_python(*nones) self.set_field_value(obj, field_name, value)
def __repr__(self) -> str: return ('<MultiField: nbt_names={!r}, default={!r}>' .format(self.nbt_names, self.default)) def __str__(self) -> str: return "Multi-field combining {}".format(self.nbt_names) @abc.abstractmethod
[docs] def to_python(self, *tags: (tags.AbstractTag, ...)) -> object: """Transform several tags into a single Pythonic object.""" raise NotImplementedError('MultiField does not implement to_python')
@abc.abstractmethod
[docs] def from_python(self, value: object) -> (tags.AbstractTag, ...): """Transform a single Pythonic object into several tags.""" raise NotImplementedError('MultiField does not implement from_python')
[docs]class SingleField(AbstractField): """Field for a single top-level tag. This is an abstract class. :meth:`to_python` is passed an NBT tag (see :mod:`nbtparse.syntax.tags`) and should return some object. That object should be acceptable to :meth:`from_python`, which should return the equivalent NBT tag. Concrete implementations must override either :meth:`to_python` or :meth:`to_python_ex`, and similarly either :meth:`from_python` or :meth:`from_python_ex`. .. note:: In general, these methods should not have side effects, and should not assume they are called at any particular time. The results may be cached and this class may assume that composing the two conversion methods produces the identity function. The reliable and comfortable hook for "heavy" NBT manipulation is :meth:`save` and :meth:`load`. """ def __init__(self, nbt_name: str, *, typename: str=None, default: object=None): self.typename = typename self.nbt_name = nbt_name self.__doc__ = str(self) super().__init__(default=default)
[docs] def get_field_value(self, obj: NBT_OBJECT, field_name: str=None) -> object: if obj is None: return self try: raw_value = obj.data[self.nbt_name] except KeyError: return None return self.to_python_ex(raw_value, obj)
[docs] def set_field_value(self, obj: NBT_OBJECT, field_name: str, value: object): if value is None: self.delete_field_value(obj, field_name) else: raw_value = self.from_python_ex(value, obj) obj.data[self.nbt_name] = raw_value
[docs] def delete_field_value(self, obj: NBT_OBJECT, field_name: str): with contextlib.suppress(KeyError): del obj.data[self.nbt_name]
def __repr__(self) -> str: return ('<SingleField: nbt_name={!r}, default={!r}>' .format(self.nbt_name, self.default)) def __str__(self) -> str: if self.typename is not None: return "{} field called {}".format(self.typename, self.nbt_name) else: return "Field called {}".format(self.nbt_name)
[docs] def to_python(self, tag: tags.AbstractTag) -> object: """Transform this tag into a Pythonic object.""" raise NotImplementedError('SingleField does not implement to_python')
[docs] def to_python_ex(self, tag: tags.AbstractTag, obj: NBT_OBJECT) -> object: """Extended form of to_python(). Takes precedence if both are defined. """ return self.to_python(tag)
[docs] def from_python(self, value: object) -> tags.AbstractTag: """Transform a Pythonic object into this tag.""" raise NotImplementedError('SingleField does not implement ' 'from_python')
[docs] def from_python_ex(self, value: object, obj: NBT_OBJECT) -> tags.AbstractTag: """Extended form of from_python(). Takes precedence if both are defined. """ return self.from_python(value)
[docs]class TupleMultiField(MutableField, MultiField): """Field which combines several tags into a tuple. :obj:`to_pythons` and :obj:`from_pythons` should be sequences of functions which translate each item listed in :obj:`nbt_names` into its corresponding Python value. """ def __init__(self, nbt_names: (str, ...), to_pythons: (types.FunctionType, ...), from_pythons: (types.FunctionType, ...), *, default: (object, ...)=None): if default is None: default = (None,) * len(nbt_names) self.to_pythons = to_pythons self.from_pythons = from_pythons super().__init__(nbt_names, default=default) def __repr__(self): return ('<TupleMultiField: nbt_names={!r}, default={!r}>' .format(self.nbt_names, self.default))
[docs] def to_python(self, *raws: (tags.AbstractTag, ...)) -> (object, ...): to_pythons = self.to_pythons return tuple(None if raw_value is None else func(raw_value) for func, raw_value in zip(to_pythons, raws))
[docs] def from_python(self, values: (object, ...)) -> (tags.AbstractTag, ...): from_pythons = self.from_pythons return tuple(None if cooked_value is None else func(cooked_value) for func, cooked_value in zip(from_pythons, values))
UUID_STRUCT = struct.Struct('>qq') NEW_UUID = _utils.Sentinel(__name__, 'NEW_UUID',""" Sentinel value for :class:`UUIDField`. When used as the default value, indicates that the field should create a new type 4 UUID for each instantiation. """.strip())
[docs]class UUIDField(MultiField): """Field which combines two ``TAG_Long``'s into a `UUID`_. A :obj:`default` of :obj:`NEW_UUID` will generate a new UUID every time. Assigning :obj:`None` in the ordinary fashion is equivalent to deleting the field. .. _UUID: http://en.wikipedia.org/wiki/UUID """ def __init__(self, high_name: str, low_name: str, *, default: uuid.UUID=None): nbt_names = (high_name, low_name) super().__init__(nbt_names, default=default) def __str__(self) -> str: return ("Universally unique identifier field split between {} " "and {}".format(*self.nbt_names)) def __repr__(self) -> str: high_name, low_name = self.nbt_names return ('UUIDField({!r}, {!r}, default={!r})' .format(high_name, low_name, self.default))
[docs] def set_default(self, obj: object, field_name: str): """If this field's default is :obj:`None`, generate a UUID.""" if self.default is NEW_UUID: self.set_field_value(obj, field_name, uuid.uuid4()) else: super().set_default(obj, field_name)
@staticmethod
[docs] def to_python(high_tag: tags.LongTag, low_tag: tags.LongTag) -> uuid.UUID: if high_tag is None or low_tag is None: return None raw_bytes = UUID_STRUCT.pack(high_tag, low_tag) return uuid.UUID(bytes=raw_bytes)
@staticmethod
[docs] def from_python(uuid_instance: uuid.UUID) -> (tags.LongTag, tags.LongTag): if uuid_instance is None: return (None, None) raw_bytes = uuid_instance.bytes int_high, int_low = UUID_STRUCT.unpack(raw_bytes) return (tags.LongTag(int_high), tags.LongTag(int_low))
[docs]class BooleanField(SingleField): """Field for a ``TAG_Byte`` acting as a boolean.""" def __init__(self, nbt_name: str, *, default: bool=False): super().__init__(nbt_name, typename="boolean", default=default) def __repr__(self) -> str: return 'BooleanField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.ByteTag) -> bool: return bool(tag)
@staticmethod
[docs] def from_python(value: bool) -> tags.ByteTag: return tags.ByteTag(value)
[docs]class ByteField(SingleField): """Field for a ``TAG_Byte`` acting as an integer.""" def __init__(self, nbt_name: str, *, default: int=0): super().__init__(nbt_name, typename="byte", default=default) def __repr__(self) -> str: return ('ByteField({!r}, default={!r})' .format(self.nbt_name, self.default)) @staticmethod
[docs] def to_python(tag: tags.ByteTag) -> int: return int(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.ByteTag: return tags.ByteTag(value)
[docs]class ShortField(SingleField): """Field for a ``TAG_Short``.""" def __init__(self, nbt_name: str, *, default: int=0): super().__init__(nbt_name, typename="short integer", default=default) def __repr__(self) -> str: return ('ShortField({!r}, default={!r})' .format(self.nbt_name, self.default)) @staticmethod
[docs] def to_python(tag: tags.ShortTag) -> int: return int(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.ShortTag: return tags.ShortTag(value)
[docs]class IntField(SingleField): """Field for a ``TAG_Int``.""" def __init__(self, nbt_name: str, *, default: int=0): super().__init__(nbt_name, typename="integer", default=default) def __repr__(self) -> str: return 'IntField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.IntTag) -> int: return int(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.IntTag: return tags.IntTag(value)
[docs]class LongField(SingleField): """Field for a ``TAG_Long``.""" def __init__(self, nbt_name: str, *, default: int=0): super().__init__(nbt_name, typename="long integer", default=default) def __repr__(self) -> str: return 'LongField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.LongTag) -> int: return int(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.LongTag: return tags.LongTag(value)
[docs]class FloatField(SingleField): """Field for a ``TAG_Float``.""" def __init__(self, nbt_name: str, *, default: float=0.0): super().__init__(nbt_name, typename="floating point", default=default) def __repr__(self): return 'FloatField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.FloatTag) -> int: return float(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.FloatTag: return tags.FloatTag(value)
[docs]class DoubleField(SingleField): """Field for a ``TAG_Double``.""" def __init__(self, nbt_name: str, *, default: float=0.0): super().__init__(nbt_name, typename="floating point (high " "precision)", default=default) def __repr__(self) -> str: return ('DoubleField({!r}, default={!r})' .format(self.nbt_name, self.default)) @staticmethod
[docs] def to_python(tag: tags.DoubleTag) -> int: return float(tag)
@staticmethod
[docs] def from_python(value: int) -> tags.DoubleTag: return tags.DoubleTag(value)
[docs]class ByteArrayField(MutableField, SingleField): """Field for a ``TAG_Byte_Array``.""" def __init__(self, nbt_name: str, *, default: bytearray=None): if default is None: default = bytearray() super().__init__(nbt_name, typename="string (8-bit)", default=default) def __repr__(self) -> str: return ('ByteArrayField({!r}, default={!r})' .format(self.nbt_name, self.default)) @staticmethod
[docs] def to_python(tag: tags.ByteArrayTag) -> bytearray: return bytearray(tag)
@staticmethod
[docs] def from_python(value: bytearray) -> tags.ByteArrayTag: return tags.ByteArrayTag(value)
[docs]class UnicodeField(SingleField): """Field for a ``TAG_String``.""" def __init__(self, nbt_name: str, *, default: str=''): super().__init__(nbt_name, typename="string (unicode)", default=default) def __repr__(self) -> str: return 'UnicodeField({!r}, default={!r})'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.StringTag) -> str: return str(tag)
@staticmethod
[docs] def from_python(value: str) -> tags.StringTag: return tags.StringTag(value)
[docs]class IntArrayField(MutableField, SingleField): """Field for a ``TAG_Int_Array``.""" def __init__(self, nbt_name: str, *, default: list=()): super().__init__(nbt_name, default=default) def __repr__(self) -> str: return 'IntArrayField({!r}, default={!r}'.format(self.nbt_name, self.default) @staticmethod
[docs] def to_python(tag: tags.IntArrayTag) -> list: return list(tag)
@staticmethod
[docs] def from_python(value: list) -> tags.IntArrayTag: return tags.IntArrayTag(value)
EPOCH = dt.datetime.utcfromtimestamp(0) EPOCH = EPOCH.replace(tzinfo=dt.timezone.utc) NOW = _utils.Sentinel(__name__, 'NOW', """ Sentinel value for :class:`UTCField`. Indicates the field should default to the current time. """.strip())
[docs]class UTCField(SingleField): """Field for a ``TAG_Long`` holding a `Unix timestamp`_. A default of :obj:`NOW` indicates the field should default to the current time. .. _Unix timestamp: http://en.wikipedia.org/wiki/Unix_time .. note:: The :class:`~datetime.datetime` objects produced by this field are always *aware*, with a tzinfo value of :obj:`~datetime.timezone.utc`. The field does accept naive objects, but it assumes they represent local time, which may not be correct for your application. In particular, the convention of using naive objects to represent UTC is not supported and will produce incorrect results unless local time is UTC. """ def __init__(self, nbt_name: str, *, default: dt.datetime=EPOCH): super().__init__(nbt_name, typename="UTC timestamp", default=default) def __repr__(self) -> str: return ('UTCField({!r}, default={!r})'.format(self.nbt_name, self.default)) @staticmethod
[docs] def to_python(nbt: tags.LongTag) -> dt.datetime: utc = dt.timezone.utc try: return dt.datetime.fromtimestamp(nbt, utc) except ValueError: return None
@staticmethod
[docs] def from_python(dt: dt.datetime) -> tags.LongTag: posix_time = dt.timestamp() return tags.LongTag(posix_time)
[docs] def set_default(self, obj: NBT_OBJECT, field_name: str): if self.default is NOW: self.set_field_value(obj, field_name, dt.datetime.now(dt.timezone.utc)) else: super().set_default(obj, field_name)
SELF_REFERENCE = _utils.Sentinel(__name__, 'SELF_REFERENCE', """ Sentinel value for :class:`NBTObjectField`. When passed as the obj_class argument, refers to the class in which the field is declared. If the field is not declared inside a subclass of NBTObject, undefined behavior occurs. """.strip())
[docs]class NBTObjectField(MutableField, SingleField): """Field for an :class:`~nbtparse.semantics.nbtobject.NBTObject`. Usually a ``TAG_Compound`` but exact contents will vary. """ def __init__(self, nbt_name: str, obj_class: NBT_META, *, default: NBT_OBJECT=None): self.obj_class = obj_class super().__init__(nbt_name, typename="NBTObject", default=default) def __repr__(self) -> str: return ('NBTObjectField({!r}, {}, default={!r})' .format(self.nbt_name, self.obj_class.__name__, self.default))
[docs] def attach(self, owner: NBT_META): super().attach(owner) if self.obj_class is SELF_REFERENCE: self.obj_class = owner
[docs] def to_python(self, nbt: tags.AbstractTag) -> NBT_OBJECT: return self.obj_class.from_nbt(nbt)
@staticmethod
[docs] def from_python(obj: NBT_OBJECT) -> tags.AbstractTag: return obj.to_nbt()
[docs]class ListField(MutableField, SingleField): """Field for a ``TAG_List``. The ListTag will be given an id of :obj:`content_id` for the elements. """ def __init__(self, nbt_name: str, item_to_python: types.FunctionType, item_from_python: types.FunctionType, content_id: int, *, default: list=()): self.item_to_python = item_to_python self.item_from_python = item_from_python self.content_id = content_id super().__init__(nbt_name, typename="list", default=default) def __repr__(self) -> str: return ('<ListField: nbt_name={!r}, contend_id={!r}, default={!r}>' .format(self.nbt_name, self.content_id, self.default))
[docs] def to_python(self, l: tags.ListTag) -> list: if l is None: return None return list(map(self.item_to_python, l))
[docs] def from_python(self, l: list) -> tags.ListTag: if l is None: return None return tags.ListTag(list(map(self.item_from_python, l)), self.content_id)
[docs]class ObjectListField(ListField): """Field for a ``TAG_List`` of :class:`~.nbtobject.NBTObject`\ s.""" def __init__(self, nbt_name: str, obj_class: NBT_META, *, default: list=()): item_to_python = obj_class.from_nbt item_from_python = lambda item: item.to_nbt() self.obj_class_name = obj_class.__name__ super().__init__(nbt_name, item_to_python, item_from_python, ids.TAG_Compound, default=default) def __repr__(self) -> str: return ('ObjectListField({!r}, {}, default={!r})' .format(self.nbt_name, self.obj_class_name, self.default))
[docs]class TupleListField(MutableField, SingleField): """Field for a ``TAG_List``; converts to a tuple. This should not be confused with :class:`TupleMultiField`, which takes several top-level tags and wraps them together into a single field. This takes a single ``TAG_List`` and converts it into a tuple. :class:`ListField` takes a single TAG_List and converts it into a list. """ def __init__(self, nbt_name: str, item_to_python: types.FunctionType, item_from_python: types.FunctionType, content_id: int, *, default: (object, ...)=()): self.item_to_python = item_to_python self.item_from_python = item_from_python self.content_id = content_id super().__init__(nbt_name, typename="tuple", default=default) def __repr__(self): return ('TupleListField({!r}, {!r}, {!r}, {!r}, default={!r})' .format(self.nbt_name, self.item_to_python, self.item_from_python, self.content_id, self.default))
[docs] def to_python(self, l: tags.ListTag) -> tuple: if l is None: return None return tuple(map(self.item_to_python, l))
[docs] def from_python(self, l: tuple) -> tags.ListTag: if l is None: return None return tags.ListTag(list(map(self.item_from_python, l)), self.content_id)
[docs]class ObjectTupleField(TupleListField): """Field for a ``TAG_List`` of :class:`.NBTObject`\ s, as a tuple.""" def __init__(self, nbt_name: str, obj_class: NBT_META, *, default: (NBT_OBJECT, ...)=()): item_to_python = obj_class.from_nbt item_from_python = lambda item: item.to_nbt() self.obj_class = obj_class super().__init__(nbt_name, item_to_python, item_from_python, ids.TAG_Compound, default=default) def __repr__(self): return ('ObjectTupleField({!r}, {!r}, default={!r})' .format(self.nbt_name, self.obj_class, self.default))
[docs]class EnumField(SingleField): """Field for an enumerated type. See :mod:`enum`. """ def __init__(self, nbt_name: str, enum_class: type(enum.Enum), *, tag_type: type=tags.IntTag, default: enum.Enum=None): self.enum_class = enum_class self.tag_type = tag_type super().__init__(nbt_name, default=default) def __repr__(self): return ('EnumField({!r}, {!r}, tag_type={!r}, default={!r})' .format(self.nbt_name, self.enum_class, self.tag_type, self.default))
[docs] def to_python(self, number: tags.AbstractTag) -> enum.Enum: return self.enum_class(number)
[docs] def from_python(self, enum_value: enum.Enum) -> tags.AbstractTag: return self.tag_type(enum_value.value)
[docs]class HeterogenousDictField(MutableField, SingleField): """Field for a dictionary of heterogenous values. Keys are strings, values are arbitrary objects. The key ``'data'`` is reserved for storing the original value of the NBT. None of the keys may begin with underscores. """ def __init__(self, nbt_name: str, schema: {str: AbstractField}, *, default: {str: object}=None): self.schema = schema """Template for the dictionaries this field should create. The keys are arbitrary strings, which will be duplicated verbatim. Those strings must not begin with underscores. The values are fields, which will be invoked in the normal manner to create the values of this dictionary, and to parse them when converting back to NBT. The original NBT will be stored in the ``'data'`` key, which is reserved for this purpose. If the key still exists when converting back to NBT, it will be used to supply any values not specified elsewhere in the dictionary. This allows round-tripping of items not controlled by any field. """ for key in schema: if key.startswith('_'): raise ValueError('Key {!r} begins with an underscore' .format(key)) if 'data' in schema: raise ValueError('The name data is reserved') from . import nbtobject class ObjectProxy(nbtobject.NBTObject): pass for name, field in schema.items(): setattr(ObjectProxy, name, field) self.object_proxy_class = ObjectProxy super().__init__(nbt_name, default=default)
[docs] def to_python(self, nbt: tags.CompoundTag) -> {str: object}: object_proxy = self.object_proxy_class.from_nbt(nbt) result = {key: getattr(object_proxy, key) for key in self.schema if getattr(object_proxy, key) is not None} result['data'] = nbt return result
[docs] def from_python(self, mapping: {str: object}) -> tags.CompoundTag: result = mapping.pop('data', tags.CompoundTag()) try: object_proxy = self.object_proxy_class(**mapping) except TypeError as exc: raise ValueError('Unrecognized key') from exc result.update(object_proxy.to_nbt()) return result
[docs]class HomogenousDictField(MutableField, SingleField): """Field for a dictionary of homogenous values. Abstract. Subclasses should override :meth:`item_to_python` and :meth:`item_from_python`.""" def __init__(self, nbt_name: str, *, default: {str: object}=None): super().__init__(nbt_name, default=default) @abc.abstractmethod
[docs] def item_to_python(self, key: tags.StringTag, value: tags.AbstractTag ) -> (str, object): raise NotImplementedError()
@abc.abstractmethod
[docs] def item_from_python(self, key: str, value: object) -> (tags.StringTag, tags.AbstractTag): raise NotImplementedError()
[docs] def to_python(self, nbt: tags.CompoundTag) -> {str: object}: return dict(itertools.starmap(self.item_to_python, nbt.items()))
[docs] def from_python(self, mapping: {str: object}) -> tags.CompoundTag: return tags.CompoundTag(itertools.starmap(self.item_from_python, mapping.items()))