"""NBT high-level object-oriented class
:class:`NBTObject` is an object-oriented wrapper for
:class:`~.tags.CompoundTag`, designed to expose its fields in a standard and
well-documented fashion.
"""
from collections import abc as cabc
import io
import logging
import reprlib
import warnings
from ..syntax import tags
from .. import exceptions
from . import fields
logger = logging.getLogger(__name__)
class _FieldShim:
def __init__(self, name, wrapped) -> None:
self.wrapped = wrapped
self.name = name
def __repr__(self):
return '_FieldShim({!r}, {!r})'.format(self.name, self.wrapped)
def __get__(self, obj: 'NBTObject', owner: 'NBTMeta') -> object:
if obj is None:
return self.wrapped
return self.wrapped.get_field_value(obj, self.name)
def __set__(self, obj: 'NBTObject', value: object) -> None:
self.wrapped.set_field_value(obj, self.name, value)
def __delete__(self, obj: 'NBTObject') -> None:
self.wrapped.delete_field_value(obj, self.name)
def __getattr__(self, name) -> object:
return getattr(self.wrapped, name)
[docs]class NBTObject(metaclass=NBTMeta):
"""Thin wrapper over a ``TAG_Compound``.
Typically houses one or more :mod:`fields<nbtparse.semantics.fields>`.
Calling the constructor directly will create an empty object with all
fields set to their default values, except for any specifically
initialized as keyword arguments. Calling :meth:`from_nbt` will populate
fields from the provided NBT.
.. note:: If the default of a field is :obj:`None`, that field will be set
to :obj:`None`, which is equivalent to leaving it empty. Such fields
will not be present in the generated NBT unless their values are
changed by hand. In some cases, this means newly created objects will
require modification before they can be meaningfully saved.
"""
def __init__(self, *args, **kwargs):
logger.debug('Creating new empty %s instance', type(self))
#: The underlying :class:`~.tags.CompoundTag` for this NBTObject.
#:
#: Fields store and retrieve instance data in this attribute. It is
#: largely used as-is when calling :meth:`to_nbt`, but some fields may
#: customize this process in their :meth:`~.fields.AbstractField.save`
#: methods. Some NBTObjects will also alter the output by overriding
#: :meth:`to_nbt`. For these reasons, direct manipulation of this
#: attribute is discouraged. It may still prove useful for debugging
#: or for cases where a key is not controlled by any field.
self.data = tags.CompoundTag()
all_fields = self.all_fields()
public_fields = self.fields()
missing = object() # sentinel value which is not in kwargs
for name in all_fields:
if name in public_fields:
value = kwargs.pop(name, missing)
if value is not missing:
logger.debug('Setting %s to %r', name, value)
setattr(self, name, value)
continue
logger.debug('Setting default value for %s', name)
field = getattr(type(self), name)
field.set_default(self, name)
super().__init__(*args, **kwargs)
[docs] def to_nbt(self) -> tags.CompoundTag:
"""Returns an NBT representation of this object.
By default, return self.data, which is sufficient if all data is
kept in fields.
Also calls the :meth:`~.fields.AbstractField.save` methods of all the
fields.
"""
logger.debug('Converting %r to NBT', self)
for name in self.all_fields():
field = getattr(type(self), name)
logger.debug('Saving %r', field)
field.save(self, name)
result = tags.CompoundTag(self.data)
return self.prepare_save(result)
[docs] def prepare_save(self, nbt: tags.CompoundTag) -> tags.CompoundTag:
"""Hook called during :meth:`to_nbt` with the to-be-saved NBT.
Should return a :class:`~.tags.CompoundTag` after (for instance)
wrapping data inside a singleton CompoundTag or performing other
transformations.
Unless overridden, return argument unchanged.
This hook runs after all fields'
:meth:`~.fields.AbstractField.save` methods are called. It is the
last hook before the NBT is saved.
"""
return nbt
@classmethod
[docs] def from_nbt(cls, nbt: tags.CompoundTag):
"""Creates a new NBTObject from the given NBT representation.
Stores the given NBT in the data attribute of the newly-created
object.
Also calls the :meth:`~.fields.AbstractField.load` methods of all the
fields.
"""
if nbt is None:
raise TypeError('Cannot create empty NBTObject via from_nbt')
cls._NBTMeta__used = True
logger.debug('Creating %s from NBT', cls)
nbt = tags.CompoundTag(nbt)
nbt = cls.prepare_load(nbt)
result = super().__new__(cls)
result.data = nbt
result._cached = {}
for name in cls.all_fields():
field = getattr(cls, name)
logger.debug('Loading %r', field)
field.load(result, name)
return result
@classmethod
[docs] def prepare_load(cls, nbt: tags.CompoundTag) -> tags.CompoundTag:
"""Hook called during :meth:`from_nbt` with the loaded NBT.
Should return a :class:`~.tags.CompoundTag` after (for instance)
unwrapping data inside a singleton CompoundTag or performing other
transformations.
Unless overridden, return argument unchanged.
This hook runs before any fields'
:meth:`~.fields.AbstractField.load` methods are called. It is the
first hook after the NBT has been loaded.
.. note::
Unlike :meth:`prepare_save`, this method must be callable as a
class method. In practice, both methods can often be implemented
as static methods anyway.
"""
return nbt
[docs] def to_bytes(self) -> bytes:
"""Serialize this NBTObject directly to a :class:`bytes` object.
The root tag will have no name.
"""
result = io.BytesIO()
nbt = self.to_nbt()
nbt.encode_named('', result)
return result.getvalue()
@classmethod
[docs] def from_bytes(cls, raw: bytes):
"""Deserialize a new NBTObject from a :class:`bytes` object.
The name of the root tag is discarded.
"""
buf = io.BytesIO(raw)
_, nbt = tags.decode_named(buf)
return cls.from_nbt(nbt)
@classmethod
[docs] def fields(cls) -> cabc.Set:
"""Forwards to :meth:`NBTMeta.fields`, which see."""
return type(cls).fields(cls)
@classmethod
[docs] def all_fields(cls) -> cabc.Set:
"""Forwards to :meth:`NBTMeta.all_fields`, which see."""
return type(cls).all_fields(cls)
def __reduce__(self):
return (_from_nbt, (type(self), self.to_nbt(),))
def __repr__(self):
return '<NBTObject: {}>'.format(reprlib.repr(self.data))
def _from_nbt(klass, nbt):
"""Helper function for unpickling.
Don't use.
"""
return klass.from_nbt(nbt)