"""Contains the base class for PPP nodes."""

import json

from . import exceptions

TYPE_TO_CLASS = {}
def register(cls):
    """Register a class to make it available to the deserializer."""
    assert cls._type
    TYPE_TO_CLASS[cls._type] = cls
    return cls

class AbstractNode:
    """Base class for PPP nodes."""
    __slots__ = ('_attributes')
    _type = None
    _possible_attributes = None
    def __init__(self, **attributes):
        # Sanity checks
        if self._type is None or self._possible_attributes is None:
            raise TypeError('%s is an abstract class.' % self.__class__)
        if 'type' in attributes:
            assert attributes.pop('type') == self.type
        unknown_keys = set(attributes) - set(self._possible_attributes)
        if unknown_keys:
            raise TypeError('%s node got unknown attributes: %s' %
                            (self._type, unknown_keys))

        # Add the attributes object
        self._attributes = attributes
        self._attributes['type'] = self.type

    @property
    def type(self):
        """Type of the node."""
        return self._type

    def __repr__(self):
        return '<PPP node "%s" %r>' % (self.type, self._attributes)

    def get(self, name):
        """Get an attribute of the node (read-only access)."""
        if name not in self._possible_attributes:
            raise AttributeError('%s is not a valid attribute of %r.' %
                                 (name, self))
        elif name in self._attributes:
            return self._attributes[name]
        else:
            raise exceptions.AttributeNotProvided(name)
    __getattr__ = __getitem__ = get

    def has(self, name):
        """Check existence of an attribute."""
        return name in self._attributes
    __hasattr__ = __contains__ = has

    def as_json(self):
        """Return a JSON dump of the object."""
        conv = lambda v: v.as_json() if isinstance(v, AbstractNode) else v
        return json.dumps({k: conv(v) for (k, v) in self._attributes.items()})

    @classmethod
    def from_json(cls, data):
        """Decode a JSON string and inflate a node instance."""
        # Decode JSON string
        if isinstance(data, str):
            data = json.loads(data)
        assert isinstance(data, dict)

        # Sanity checks
        if 'type' not in data:
            raise exceptions.AttributeNotProvided('type')
        if data['type'] not in TYPE_TO_CLASS:
            raise exceptions.UnknownNodeType(data['type'])

        # Create node instances
        conv = lambda v: cls.from_json(v) if isinstance(v, dict) else v
        data = {k: conv(v) for (k, v) in data.items()}
        return TYPE_TO_CLASS[data['type']](**data)


@@ -597,16 +597,333 @@
 lass__)%0A
+        self._check_attributes(attributes)%0A%0A        # Add the attributes object%0A        self._attributes = attributes%0A        self._attributes%5B'type'%5D = self.type%0A%0A    def _check_attributes(self, attributes):%0A        %22%22%22Check if attributes given to the constructor can be used to%0A        instanciate a valid node.%22%22%22%0A
         
@@ -939,32 +939,32 @@
  in attributes:%0A
-
             asse
@@ -1227,128 +1227,8 @@
 ))%0A%0A
-        # Add the attributes object%0A        self._attributes = attributes%0A        self._attributes%5B'type'%5D = self.type%0A%0A
     
@@ -2230,16 +2230,312 @@
 ms()%7D)%0A%0A
+    @staticmethod%0A    def _test_can_import_json(data):%0A        %22%22%22Sanity check on input JSON data%22%22%22%0A        if 'type' not in data:%0A            raise exceptions.AttributeNotProvided('type')%0A        if data%5B'type'%5D not in TYPE_TO_CLASS:%0A            raise exceptions.UnknownNodeType(data%5B'type'%5D)%0A%0A
     @cla
@@ -2787,216 +2787,38 @@
     
-# Sanity checks%0A        if 'type' not in data:%0A            raise exceptions.AttributeNotProvided('type')%0A        if data%5B'type'%5D not in TYPE_TO_CLASS:%0A            raise exceptions.UnknownNodeType(data%5B'type'%5D
+cls._test_can_import_json(data
 )%0A%0A 

Use separate methods to make sanity checks.