Commits

rkruppe committed d7b7c4e

Similar overhaul for validator and yamlreader, though probably not finished

Comments (0)

Files changed (6)

tutagx/meta/decode.py

 import abc
+from collections import namedtuple
 from functools import lru_cache
 
 from tutagx.meta import model, process
 import tutagx.util.codegen as cg
 
 
+_DecodingContext = namedtuple('_DecodingContext', 'seen unfinished extra')
+
+
+class DecodingContext(_DecodingContext):
+    def __new__(cls):
+        return _DecodingContext.__new__(cls, {}, set(), {})
+
+
 class ObjectCreationStrategy(metaclass=abc.ABCMeta):
     def __init__(self, emit):
         self._emit = emit
     def finish(self, obj):
         pass
 
-    @property
-    def extra_args(self):
-        return ()
-
 
 class PlainObjectCreation(ObjectCreationStrategy):
     def construct(self, target, cls):
         self._emit.line(obj, '._on_loaded()')
 
 
-def _ocs(cls, emit):
-    ocs_class = getattr(cls, 'CONSTRUCTION_STRATEGY', PlainObjectCreation)
-    ocs = ocs_class(emit)
-    return ocs
-
-
 class _Placeholder:
     def __init__(self):
         self._locations = []
     # which refer to the placeholder, cycles would be inevitable.)
 
 
+def _ocs(cls, emit):
+    ocs_class = getattr(cls, 'CONSTRUCTION_STRATEGY', PlainObjectCreation)
+    return ocs_class(emit)
+
+
 @lru_cache()
 def make_decoder(cls):
     RESERVED = ('classes', 'result', 'oid', 'json_val', 'k', '__Placeholder')
     emit = cg.CodeEmitter()
-    ocs = _ocs(cls, emit)
-    sig = cg.Signature('$obj', 'seen', 'unfinished', *ocs.extra_args)
+    sig = cg.Signature('$obj', 'seen', 'unfinished', 'extra')
     symtab = cg.SymbolTable(RESERVED, sig)
     classes = {}
 
     )
     return gen(wrap_model(cls))
 
+
+def decode(cls, obj, context):
+    """
+    Decode the YAML data obj as serialized object of type cls in the context.
+    """
+    raw_decoder = make_decoder(cls)
+    return raw_decoder(obj, context.seen, context.unfinished, context.extra)
+

tutagx/meta/from_yaml.py

-from collections import namedtuple
 import yaml
 
 from tutagx.meta import validate, decode
-from tutagx.meta.decode import ObjectCreationStrategy
-
-_ReaderNamespace = namedtuple('_ReaderNamespace', 'seen unfinished extra')
+from tutagx.meta.decode import ObjectCreationStrategy, DecodingContext
 
 
 #TODO replace this API with something better
 class YAMLReader:
-    def __init__(self, model):
-        self._mcls = type(model)
-        self._cls = model
-        self._decoder = decode.make_decoder(model)
+    def __init__(self, context=None):
+        if context is None:
+            context = DecodingContext()
+        self.context = context
+        self.validator = validate.Validator()
 
-    def decode(self, val, namespace=None, validator_ns=None, source=None):
-        results, errors = self.decode_many(
-            [val], namespace, validator_ns, source
-        )
+    def decode(self, cls, val, source=None):
+        results, errors = self.decode_many(cls, [val], source)
         if errors:
             return None, errors
+        assert len(results) == 1
         return results[0], errors
 
-    def decode_many(self, raw_values,
-                    namespace=None, validator_ns=None,
-                    source=None):
-        if namespace is None:
-            namespace = type(self).new_namespace()
-        if validator_ns is None:
-            validator_ns = validate.Validator.new_namespace()
-        args = (namespace.seen, namespace.unfinished) + namespace.extra
-        validator = validate.Validator(self._cls)
+    def decode_many(self, cls, raw_values, source=None):
         results = []
+        valid = True
+        try:
+            add = cls.add_decoding_extra
+        except AttributeError:
+            pass
+        else:
+            add(self.context.extra)
         for raw_value in raw_values:
-            validator.validate(raw_value, validator_ns, source)
+            valid = self.validator.validate(cls, raw_value)
             # Note that we continue validating, but stop decoding,
             # as soon as ANY error is encountered.
             # This allows producing additional useful errors without
             # trying to decode possibly invalid data.
-            if validator_ns.errors:
-                continue
-            # If decoding raises an exception, it's a bug in the
-            # validation and SHOULD lead to a crash!
-            results.append(self._decoder(raw_value, *args))
+            if valid:
+                # If decoding raises an exception, it's a bug in the
+                # validation and SHOULD lead to a crash!
+                results.append(decode.decode(cls, raw_value, self.context))
         # If any error occured, the whole data set is likely invalid.
         # Thus, we do not attempt to return partial data.
-        if validator_ns.errors:
-            return None, validator_ns.errors
-        return results, validator_ns.errors
+        if not valid:
+            return None, self.validator.context.errors
+        return results, self.validator.context.errors
 
-    def load(self, f, namespace=None, validator_ns=None):
-        results, errors = self.load_many(f, namespace, validator_ns)
+    def load(self, cls, f):
+        results, errors = self.load_many(cls, f)
         if errors:
             return None, errors
+        assert len(results) == 1
         return results[0], errors
 
-    def load_many(self, f, namespace=None, validator_ns=None):
+    def load_many(self, cls, f):
         try:
             source = f.name
         except AttributeError:
             source = '<unknown file>'
         raw_data = yaml.safe_load_all(f)
-        return self.decode_many(raw_data, namespace, validator_ns, source)
+        return self.decode_many(cls, raw_data, source)
 
-    def loads(self, s, namespace=None):
-        return self.decode(yaml.safe_load(s), namespace)
+    def loads(self, cls, s):
+        return self.decode(cls, yaml.safe_load(s))
 
-    @classmethod
-    def new_namespace(cls):
-        """
-        Return a namespace object, which controls the scope of object IDs
-        encountered in YAML documents.
-        Reader namespaces have two members:
-
-        * ``seen``, a dictionary with object IDs encountered while decoding as
-          keys and the objects instanciated for these IDs as values.
-        * ``unfinished``, a set of object IDs that some loaded objects referred
-          to for which no object has been loaded (yet).
-          If this set is non-empty after all data was loaded, there were
-          bogus IDs in the input.
-
-        Namespaces are only relevant if you plan on using one namespace across
-        multiple documents, or need access to the information exposed.
-        """
-        return _ReaderNamespace({}, set(), cls._extra_decoder_args())
-
-    @classmethod
-    def _extra_decoder_args(cls):
-        # Dummy implementation, this is only useful for subclasses
-        return ()
-

tutagx/meta/tests/test_validate.py

 
 def check_yaml(cls, yaml_string):
     data = yaml.load(yaml_string)
-    ns = Validator.new_namespace()
-    Validator(cls).validate(data, ns)
-    add_errors_for_undefined(ns)
-    return ns.errors
+    v = Validator()
+    v.validate(cls, data)
+    add_errors_for_undefined(v.context)
+    return v.context.errors
 
 
 class EmptyModel(m.Model):

tutagx/meta/tests/test_yaml.py

 
 
 def decode(cls, *ds):
-    reader = getattr(cls, '_yaml_reader', from_yaml.YAMLReader)
-    obj, errors = reader(cls).decode_many(ds)
+    obj, errors = from_yaml.YAMLReader().decode_many(cls, ds)
     assert not errors
     return obj
 

tutagx/meta/validate.py

 from itertools import chain
 from contextlib import contextmanager
 from collections import defaultdict, namedtuple
+
 from tutagx.meta.collect_objects import collect_instances
 from tutagx.meta.common import typeident, wrap_model
 from tutagx.meta import model, process
 import tutagx.util.codegen as cg
 
-_ValidatorNamespace = namedtuple(
-    '_ValidatorNamespace',
+_ValidationContext = namedtuple(
+    '_ValidationContext',
     'seen defined errors access'
 )
 
 
-def undefined_ids(ns):
+class ValidationContext(_ValidationContext):
+    def __new__(cls):
+        return _ValidationContext.__new__(cls,
+            defaultdict(set), set(), [], []
+        )
+
+
+def undefined_ids(ctx):
     """
     Return a frozenset of object IDs that are "undefined" (or "unfinished").
     An object ID is undefined if it has been used to refer to an object,
     the result is not meaningful before all documents have been loaded into
     the same namespace.
     """
-    return frozenset(ns.seen) - frozenset(ns.defined)
+    return frozenset(ctx.seen) - frozenset(ctx.defined)
 
 
-def add_errors_for_undefined(ns):
+def add_errors_for_undefined(ctx):
     """
-    Add error messages for ``undefined_ids(ns)``.
+    Add error messages for ``undefined_ids(ctx)``.
     Note that this may add wrong error messages the data is split across
     multiple files and this is called before all sources are checked in
     the same namespace.
     """
-    for undef_id in undefined_ids(ns):
-        users = ns.seen[undef_id]
+    for undef_id in undefined_ids(ctx):
+        users = ctx.seen[undef_id]
         users_str = '; '.join(users)
         msg = 'referenced (from {}) but not defined'.format(users_str)
-        ns.errors.append((KeyError, repr(undef_id), msg))
+        ctx.errors.append((KeyError, repr(undef_id), msg))
 
 
-def add_errors_for_unreachable(cls, ns, loaded, errors):
+def add_errors_for_unreachable(cls, ctx, loaded, errors):
     """
-    Add error messages for ``unreachable_objects(cls, ns, loaded)``.
+    Add error messages for ``unreachable_objects(cls, ctx, loaded)``.
     The caveats of ``add_errors_for_undefined`` apply here too.
     """
-    for oid in unreachable_objects(cls, ns, loaded):
+    for oid in unreachable_objects(cls, ctx, loaded):
         errors.append((
             RuntimeError, repr(oid),
             "not used (would be dropped during serialization)"
 
 
 @process.oneshot
-def _validate():
+def make_validate():
     sig = cg.Signature(
         '$obj', 'seen', 'defined', 'errors', 'stack', 'source', '$is_union'
     )
     )
 
 
+def validate(cls, obj, context, source):
+    raw_validator = make_validate(wrap_model(cls))
+    raw_validator(
+        obj,
+        context.seen, context.defined, context.errors, context.access, source,
+        is_union=False
+    )
+
+
 class Validator:
-    def __init__(self, cls):
-        self._node = wrap_model(cls)
+    def __init__(self):
+        self.context = ValidationContext()
 
-    def validate(self, obj, ns=None, source='<string>'):
-        if ns is None:
-            ns = Validator.new_namespace()
+    def validate(self, cls, obj, source='<string>'):
         # This is necessary because even ``obj`` may be invalid.
         try:
             oid = obj['$id']
         except Exception:
-            ns.access.append(['<toplevel object>'])
+            self.context.access.append(['<toplevel object>'])
         else:
-            ns.access.append([oid])
-        _validate(self._node)(
-            obj, ns.seen, ns.defined, ns.errors, ns.access, source,
-            is_union=False
-        )
-        return ns.errors
+            self.context.access.append([oid])
+        validate(cls, obj, self.context, source)
+        return not self.context.errors
 
-    @staticmethod
-    def new_namespace():
-        """
-        Create a new namespace for validation, with similar meaning.
-        The only public property is ``errors``, a list of error tuples::
-
-            (kind, source, message)
-
-        Where ``kind`` is an exception class, ``source`` is a string
-        describing where the error occured, and ``message`` is the actual
-        error message.
-        """
-        return _ValidatorNamespace(defaultdict(set), set(), [], [])
-

tutagx/model/entity.py

 class EntityConstructionStrategy(from_yaml.ObjectCreationStrategy):
     def construct(self, target, cls):
         self._emit.linef(
-            '{} = gcx.build(classes[{!r}, {!r}]).unfinished_object()',
+            '{} = extra["gcx"].build(classes[{!r}, {!r}])' +
+            '.unfinished_object()',
             target, cls.__module__, cls.__name__
         )
 
     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)
+    def add_decoding_extra(cls, extra):
+        if 'gcx' not in extra:
+            extra['gcx'] = GameContext()
+
+    def __repr__(cls):
+        s = type.__repr__(cls)
         assert s.startswith('<class')
         return '<GameEntity class' + s[len('<class'):]
 
 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.