Source

tutagx / tutagx / model / entity.py

Full commit
"""
Customized metaclass for all models used in the game.
"""
import uuid
import itertools
from tutagx.meta import model, from_yaml
from tutagx.model.context import GameContext


class EntityConstructionStrategy(from_yaml.ObjectCreationStrategy):
    def construct(self, target, cls):
        self._emit.linef(
            '{} = gcx.build(classes[{!r}, {!r}]).unfinished_object()',
            target, cls.__module__, cls.__name__
        )

    def add_attributes(self, obj, params):
        for attr, expr in params:
            self._emit.linef('{}._builder.set({!r}, {})', obj, attr, expr)

    def finish(self, obj):
        self._emit.line(obj, '._builder.finish()')

    @property
    def extra_args(self):
        return ('gcx',)


class _Reader(from_yaml.YAMLReader):
    @classmethod
    def _extra_decoder_args(cls):
        # XXX this assumes we actually want one context per decoding
        # "session". Fine for now, but may break down in the future.
        return (GameContext(),)


class GameEntityMeta(model.ModelMeta):
    _MODEL_BASE_NAME = 'GameEntity'
    CONSTRUCTION_STRATEGY = EntityConstructionStrategy

    def __repr__(self):
        s = type.__repr__(self)
        assert s.startswith('<class')
        return '<GameEntity class' + s[len('<class'):]


_counter = itertools.count()


class GameEntity(metaclass=GameEntityMeta):
    is_value_type = False
    __oid_overriden = False
    _yaml_reader = _Reader

    # Would be an abstractmethod, but ABCs use metaclasses and metaclasses
    # don't mix easily.
    def _derive_oid(self):
        # Make a first stab at a meaningful object ID:
        # Return a string that describes the object somewhat.
        # It does not have to be unique, will be adjusted to be unique.
        raise NotImplementedError

    def __setup_oid(self):
        try:
            canidate_oid = self._derive_oid()
        except NotImplementedError:
            canidate_oid = self._OID_PATTERN.format(self)
        while self.gcx.exists(canidate_oid):
            canidate_oid += '$' + str(uuid.uuid4())
        self.__id = canidate_oid
        self.gcx.register_object(self)

    def _pre_init(self, **extra):
        # We need an object ID at this point to register, but deriving the
        # object ID usually requires member access - hence we can only derive
        # in _on_loaded.
        # Thus, we generate a bogus (but unique) object ID to dispose later.
        if '$ctx' not in extra:
            raise TypeError("Object created without game context "
                            "($ctx kwdarg missing)")
        self.gcx = extra.pop('$ctx')
        if self.is_value_type:
            return
        TEMPLATE = '<fake ID #{}, you should not see this; please report it>'
        self.__id = TEMPLATE.format(next(_counter))
        while self.gcx.exists(self.__id):
            self.__id += str(uuid.uuid4())
        self.gcx.register_object(self)

    def _on_loaded(self):
        if not (self.is_value_type or self.__oid_overriden):
            self.__setup_oid()

    @property
    def object_id(self):
        model._value_type_oid_error(self)
        if self.is_value_type:
            raise TypeError('Value types ')
        return self.__id

    @object_id.setter
    def object_id(self, new_oid):
        model._value_type_oid_error(self)
        old_oid = self.__id
        self.__id = new_oid
        try:
            self.gcx.oid_changed(type(self), old_oid, new_oid)
        except:
            self.__id = old_oid
            raise
        self.__oid_overriden = True