Commits

rkruppe committed 93b3d95

Scalar components and some simplification

Comments (0)

Files changed (4)

 import base64
 from types import SimpleNamespace
-from collections import OrderedDict
 import logging
 import jinja2
 import ttgx.rawbuf
 
-
-class _Type:
-    def __init__(self, name):
-        self._name = name
-
-    def _make_array(self):
-        buf_type = _dtype_to_buf[self]
-        return ttgx.rawbuf.Array(buf_type)
-
-    def __repr__(self):
-        return __name__ + '.dtype.' + self._name
-
-
-dtype = SimpleNamespace()
-
-for typename in ('I32', 'U32'):
-    setattr(dtype, typename, _Type(typename))
-del typename
-
-_dtype_to_buf = {
-    dtype.I32: ttgx.rawbuf.I32,
-    dtype.U32: ttgx.rawbuf.U32
-}
+#TODO
+# * merge this module with rawbuf
+# * add vector types, like 3 x I32 (preferably in a general manner)
+#   * make sure those are only accessed like ``pos[e, 1] = new_y``
+#     (to prevent lifetime issues)
+# * perhaps add an escape hatch for arbitrary Python types?
 
 log = logging.getLogger(__name__)
 _jinja_env = jinja2.Environment(
 _jinja_env.filters['repr'] = repr
 _component_template = _jinja_env.from_string('''
 import ttgx.dodecs
+import ttgx.rawbuf
 
 {# Is actually completely independent and could live in dodecs.
     Including it here only to permit better error messages. #}
         self.__indices = {}
         self.__entities = []
 {%- for name, t in attrs.items() %}
-        self._{{name}} = {{t}}._make_array()
+        self._{{name}} = ttgx.rawbuf.Array(ttgx.rawbuf.{{t}})
         self.{{name}} = _{{clsname}}AttributeView(
             self, self.__indices, self._{{name}})
 {%- endfor %}
     src = _component_template.render(
         clsname=clsname, identifier=identifier, attrs=attrs
     )
-    log.debug('Code for component %s:%s\n%s', clsname, src, '-' * 80)
+    log.debug("Code for component %s:%s\n%s", clsname, src, '-' * 80)
     code = compile(src, '<dodecs component>', 'exec')
     namespace = {}
     exec(code, namespace, namespace)
     return namespace[clsname]
 
 
+_scalar_component_template = _jinja_env.from_string('''
+import ttgx.dodecs
+
+class {{clsname}}:
+    identifier = {{identifier|repr}}
+
+    def __init__(self, *, {{ attrs|join(', ') }}):
+    {%- for name in attrs %}
+        self._{{name}} = {{name}}
+    {%- endfor %}
+{% for name in attrs %}
+    @property
+    def {{name}}(self):
+        return self._{{name}}
+    @{{name}}.setter
+    def {{name}}(self, value):
+        self._{{name}} = value
+{% endfor %}
+
+    def __error(self, *args, **kwds):
+        raise TypeError("A scalar component only holds ONE record")
+
+    insert = delete = __error
+''')
+
+
+def scalar_component(clsname, identifier, attrs):
+    attrs = attrs.split()
+    src = _scalar_component_template.render(
+        clsname=clsname, identifier=identifier, attrs=attrs
+    )
+    code = compile(src, '<dodecs scalar component>', 'exec')
+    namespace = {}
+    exec(code, namespace, namespace)
+    return namespace[clsname]
+
+
 def _b64id(o):
     """A base64 variation on id() for humans.
 
     A dirty trick is used to make the ids more distinguishable:
-    The id() is interpreted LSByte-first, because the higher-valued bytes
-    the same for most objects with typical memory management.
+    The id() is displayed LSByte-first, because the higher-valued bytes
+    are the same for most objects with typical memory management.
     This is only useful if id() is indeed a memory address and the memory
     management is somewhat typical. That detail is true for CPython and the
     best/default PyPy GC.
         self.sys = SimpleNamespace()
 
     def _components(self):
-        return (getattr(self.comp, ident)
-                for ident in self._component_identifiers)
+        for ident in self._component_identifiers:
+            yield getattr(self.comp, ident)
 
     def _systems(self):
-        return (getattr(self.sys, ident)
-                for ident in self._system_identifiers)
+        for ident in self._system_identifiers:
+            yield getattr(self.sys, ident)
 
     def entity(self):
         e = _Entity()
         if hasattr(self.comp, ident):
             if getattr(self.comp, ident) is c:
                 raise RuntimeError("Component already registered")
-            raise KeyError("Component name already reserved")
+            raise KeyError("Component name already in use")
         self._component_identifiers.add(ident)
         setattr(self.comp, ident, c)
         c.universe = self
         if hasattr(self.sys, ident):
             if getattr(self.sys, ident) is s:
                 raise RuntimeError("System already registered")
-            raise KeyError("System name already reserved")
+            raise KeyError("System name already in use")
         self._system_identifiers.add(ident)
         setattr(self.sys, ident, s)
 
 _libc = _ffi.dlopen(None)
 
 
-#TODO maybe get rid of _Buffer, folding the relevant parts into Array?
+# TODO
+# * get rid of _Buffer, fold the relevant parts into Array
+# * make Array accept a type string like 'uint32_t'
+#   * turn I32 etc. into constant type strings instead of fully blown objects
+# * possibly turn Array into a factory delegating to different implementations
 class _Buffer:
     def __init__(self, size):
         self._data = _ffi.new(self._ctype, size)

ttgx/tests/test_dodecs.py

 import sys
 import base64
-from operator import getitem, setitem
 from unittest.mock import Mock, call
 from pytest import fixture, raises
 import pretend
-from ttgx.dodecs import Universe, InvalidEntityError, component, dtype
+import ttgx.dodecs as dodecs
+from ttgx.dodecs import InvalidEntityError, component, scalar_component
 
 
 @fixture
 def u():
-    return Universe()
+    return dodecs.Universe()
 
 
 @fixture
 def e():
-    return Universe().entity()
+    return dodecs.Universe().entity()
 
 
 @fixture
 def x():
-    u = Universe()
-    u.register_component(ScalarC())
-    return u.comp.scalar
+    u = dodecs.Universe()
+    u.register_component(ValueC())
+    return u.comp.value
 
 
 @fixture
 def pos():
-    u = Universe()
+    u = dodecs.Universe()
     u.register_component(PositionC())
     return u.comp.position
 
 
+@fixture
+def scalar():
+    u = dodecs.Universe()
+    u.register_component(ScalarC(a=1, b=2))
+    return u.comp.scalar
+
+
 def component_stub(ident):
     return pretend.stub(identifier=ident)
 
     return s
 
 
-ScalarC = component('ScalarC', 'scalar', val=dtype.I32)
-PositionC = component('PositionC', 'position', x=dtype.U32, y=dtype.U32)
+ValueC = component('ValueC', 'value', val='I32')
+PositionC = component('PositionC', 'position', x='U32', y='U32')
+ScalarC = scalar_component('ScalarC', 'scalar', 'a b')
 
 ##
 
 def test_entity_has_no_behavior(e):
     e_names = set(dir(e))
     allowed_names = set(dir(object()))
-    allowed_names |= {'__slots__', '__module__', '__qualname__', '__locals__'}
     # __locals__ pop up if run with coverage, which I assume is a weird
     # artifact of the implementation of trace functions, but whatever.
+    allowed_names |= {'__slots__', '__module__', '__qualname__', '__locals__'}
     assert e_names.issubset(allowed_names), e_names - allowed_names
 
 
-def test_delete(u):
+def test_dealloc(u):
     e = u.entity()
     u.dealloc(e)
     raises(InvalidEntityError, u.check_entity, e)
     raises(TypeError, u.register_component, c)
 
 
-def test_entity_delete_notifies_all_components(u):
+def test_entity_dealloc_notifies_components(u):
     e = u.entity()
     c = Mock()
     c.identifier = 'quux'
     assert s.mock_calls == [call.step(u.comp.c1, u.comp.c2)]
 
 
-class TestScalarComponent:
+class TestSimpleComponent:
     def test_keyerror_when_absent(self, x):
         e = x.universe.entity()
         with raises(KeyError):
         assert pos.x[e1] == 42
         assert pos.x[e2] == 0
 
+
+class TestScalarComponent:
+    def test_must_create_with_inital_values(self):
+        with raises(TypeError):
+            ScalarC()
+        s = ScalarC(a=2, b=3)
+        assert s.a == 2
+        assert s.b == 3
+
+    def test_no_per_entity_data(self, scalar):
+        e = scalar.universe.entity()
+        with raises(TypeError):
+            scalar.insert(e, a=1, b=2)
+        with raises(TypeError):
+            scalar.delete(e)
-import ttgx.dodecs as dodecs
+from ttgx.dodecs import component
 
-GeoPosC = ecs.component('GeoPosC', 'geopos', ['long', 'lat'],
-                        long=ecs.dtype.UInt32, lat=ecs.dtype.Uint32)
+GeoPosC = component('GeoPosC', 'geopos', ['long', 'lat'],
+                    long='U32', lat='U32')