Exemple #1
0
 def description(self) -> Description:
     decoded_record = json.loads(self._serialized_record)
     resource_type = TypeIdentifier(tuple(decoded_record['type']))
     # TODO: Handle multidimensional references.
     shape = (1, )
     value = Description(resource_type=resource_type, shape=shape)
     return value
Exemple #2
0
 def __init__(self, resource_type: TypeIdentifier, *, shape: tuple = (1,)):
     type_id = TypeIdentifier.copy_from(resource_type)
     if type_id is None:
         raise ValueError(f'Could not create a TypeIdentifier for {repr(resource_type)}')
     # TODO: Further input validation
     self._shape = shape
     self._type = type_id
Exemple #3
0
def test_encoder_registration():
    # Test low-level registration details for object representation round trip.
    # There were some to-dos of things we should check...
    ...

    # Test framework for type creation and automatic registration.
    class SpamInstance(BasicSerializable, base_type=('test', 'Spam')):
        ...

    instance = SpamInstance(label='my_spam',
                            identity=uuid.uuid4().hex,
                            dtype=['test', 'Spam'],
                            shape=(1, ),
                            data=['spam', 'eggs', 'spam', 'spam'])
    assert not type(instance) is BasicSerializable

    encoded = encode(instance)
    decoded = decode(encoded)
    assert not type(decoded) is BasicSerializable
    assert isinstance(decoded, SpamInstance)

    del instance
    del decoded
    del SpamInstance
    import gc
    gc.collect()
    with pytest.raises(ProtocolError):
        decode(encoded)

    decode.unregister(TypeIdentifier(('test', 'Spam')))
    decoded = decode(encoded)
    assert type(decoded) is BasicSerializable
Exemple #4
0
    def __init_subclass__(cls, **kwargs):
        assert cls is not BasicSerializable

        # Handle SCALE-MS Type registration.
        base = kwargs.pop('base_type', None)
        if base is not None:
            typeid = TypeIdentifier.copy_from(base)
        else:
            typeid = [str(cls.__module__)] + cls.__qualname__.split('.')
        registry = BasicSerializable._dtype.base
        if cls in registry and registry[cls] is not None:
            # This may be a customization or extension point in the future, but not today...
            raise ProtocolError(
                'Subclassing BasicSerializable for a Type that is already registered.'
            )
        BasicSerializable._dtype.base[cls] = typeid

        # Register encoder for all subclasses. Register the default encoder if not overridden.
        # Note: This does not allow us to retain the identity of *cls* for when we call the helpers.
        # We may require such information for encoder functions to know why they are being called.
        encoder = getattr(cls, 'encode', BasicSerializable.encode)
        PythonEncoder.register(cls, encoder)

        # Optionally, register a new decoder.
        # If no decoder is provided, use the basic decoder.
        if hasattr(cls, 'decode') and callable(cls.decode):
            _decoder = weakref.WeakMethod(cls.decode)

            # Note that we do not require that the decoded object is actually
            # an instance of cls.

            def _decode(encoded: dict):
                decoder = _decoder()
                if decoder is None:
                    raise ProtocolError(
                        'Decoding a type that has already been de-registered.')
                return decoder(encoded)

            PythonDecoder.register(cls._dtype, _decode)

        # TODO: Register optional instance initializer / input processor.
        # Allow instances to be created with something other than a single-argument
        # of the registered Input type.

        # TODO: Register/generate UI helper.
        # From the user's perspective, an importable module function interacts
        # with the WorkflowManager to add workflow items and return a handle.
        # Do we want to somehow generate an entry-point command

        # TODO: Register result dispatcher(s).
        # An AbstractDataSource must register a dispatcher to an implementation
        # that produces a ConcreteDataSource that provides the registered Result type.
        # A ConcreteDataSource must provide support for checksum calculation and verification.
        # Optionally, ConcreteDataSource may provide facilities to convert to/from
        # native Python objects or other types (such as .npz files).

        # Proceed dispatching along the MRO, per documented Python data model.
        super().__init_subclass__(**kwargs)
Exemple #5
0
 def get_decoder(cls, typeid) -> typing.Union[None, typing.Callable]:
     # Normalize the type identifier.
     try:
         identifier = TypeIdentifier.copy_from(typeid)
         typename = identifier.name()
     except TypeError:
         try:
             typename = str(typeid)
         except TypeError:
             typename = repr(typeid)
         identifier = None
     # Use the (hashable) normalized form to look up a decoder for dispatching.
     if identifier is None or identifier not in cls._dispatchers:
         raise TypeError('No decoder registered for {}'.format(typename))
     return cls._dispatchers[identifier]
Exemple #6
0
    def __init__(self, data, *, dtype, shape=(1, ), label=None, identity=None):
        if identity is None:
            # TODO: Calculate an appropriate identifier
            self.__identity = EphemeralIdentifier()
        else:
            # TODO: Validate identity
            self.__identity = identity
        self.__label = str(label)

        attrname = BasicSerializable._dtype.attr_name
        setattr(self, attrname, TypeIdentifier.copy_from(dtype))

        self._shape = Shape(shape)
        # TODO: validate data dtype and shape.
        # TODO: Ensure that we retain a reference to read-only data.
        # TODO: Allow a secondary localized / optimized / implementation-specific version of data.
        self.data = data
Exemple #7
0
 def decode(cls: typing.Type[ST], encoded: dict) -> ST:
     if not isinstance(encoded,
                       collections.abc.Mapping) or 'type' not in encoded:
         raise TypeError(
             'Expected a dictionary with a *type* specification for decoding.'
         )
     dtype = TypeIdentifier.copy_from(encoded['type'])
     label = encoded.get('label', None)
     identity = encoded.get(
         'identity')  # TODO: verify and use type schema to decode.
     shape = Shape(encoded['shape'])
     data = encoded[
         'data']  # TODO: use type schema / self._data_decoder to decode.
     logger.debug('Decoding {identity} as BasicSerializable.')
     return cls(label=label,
                identity=identity,
                dtype=dtype,
                shape=shape,
                data=data)
Exemple #8
0
def test_basic_decoding():
    # Let the basic encoder/decoder handle things that look like SCALE-MS objects.
    encoded = {
        'label': None,
        'identity': uuid.uuid4().hex,
        'type': ['test', 'Spam'],
        'shape': [1],
        'data': ['spam', 'eggs', 'spam', 'spam']
    }
    instance = decode(encoded)
    assert type(instance) is BasicSerializable
    shape_ref = Shape((1, ))
    assert instance.shape() == shape_ref
    type_ref = TypeIdentifier(('test', 'Spam'))
    instance_type = instance.dtype()
    assert instance_type == type_ref

    # Test basic encoding, too.
    assert tuple(instance.encode()['data']) == tuple(encoded['data'])
    assert instance.encode() == decode(instance.encode()).encode()
Exemple #9
0
class BasicSerializable(UnboundObject):
    __label: typing.Optional[str] = None
    __identity: Identifier
    _shape: Shape
    data: collections.abc.Container

    _data_encoder: typing.Callable
    _data_decoder: typing.Callable

    _dtype = TypeDataDescriptor(
        base_type=TypeIdentifier(('scalems', 'BasicSerializable')))

    def dtype(self) -> TypeIdentifier:
        # Part of the decision of whether to use a property or a method
        # is whether we want to normalize on dtype as an instance or class characteristic.
        # Initially, we are using inheritance to get registration behavior through metaprogramming.
        # In other words, the real question may be how we want to handle registration.
        return self._dtype

    def __init__(self, data, *, dtype, shape=(1, ), label=None, identity=None):
        if identity is None:
            # TODO: Calculate an appropriate identifier
            self.__identity = EphemeralIdentifier()
        else:
            # TODO: Validate identity
            self.__identity = identity
        self.__label = str(label)

        attrname = BasicSerializable._dtype.attr_name
        setattr(self, attrname, TypeIdentifier.copy_from(dtype))

        self._shape = Shape(shape)
        # TODO: validate data dtype and shape.
        # TODO: Ensure that we retain a reference to read-only data.
        # TODO: Allow a secondary localized / optimized / implementation-specific version of data.
        self.data = data

    def identity(self):
        return self.__identity

    def label(self):
        return str(self.__label)

    def shape(self):
        return Shape(self._shape)

    def encode(self) -> dict:
        representation = {
            'label': self.label(),
            'identity': str(self.identity()),
            'type': self.dtype().encode(),
            'shape': tuple(self.shape()),
            'data': self.data  # TODO: use self._data_encoder()
        }
        return representation

    @classmethod
    def decode(cls: typing.Type[ST], encoded: dict) -> ST:
        if not isinstance(encoded,
                          collections.abc.Mapping) or 'type' not in encoded:
            raise TypeError(
                'Expected a dictionary with a *type* specification for decoding.'
            )
        dtype = TypeIdentifier.copy_from(encoded['type'])
        label = encoded.get('label', None)
        identity = encoded.get(
            'identity')  # TODO: verify and use type schema to decode.
        shape = Shape(encoded['shape'])
        data = encoded[
            'data']  # TODO: use type schema / self._data_decoder to decode.
        logger.debug('Decoding {identity} as BasicSerializable.')
        return cls(label=label,
                   identity=identity,
                   dtype=dtype,
                   shape=shape,
                   data=data)

    def __init_subclass__(cls, **kwargs):
        assert cls is not BasicSerializable

        # Handle SCALE-MS Type registration.
        base = kwargs.pop('base_type', None)
        if base is not None:
            typeid = TypeIdentifier.copy_from(base)
        else:
            typeid = [str(cls.__module__)] + cls.__qualname__.split('.')
        registry = BasicSerializable._dtype.base
        if cls in registry and registry[cls] is not None:
            # This may be a customization or extension point in the future, but not today...
            raise ProtocolError(
                'Subclassing BasicSerializable for a Type that is already registered.'
            )
        BasicSerializable._dtype.base[cls] = typeid

        # Register encoder for all subclasses. Register the default encoder if not overridden.
        # Note: This does not allow us to retain the identity of *cls* for when we call the helpers.
        # We may require such information for encoder functions to know why they are being called.
        encoder = getattr(cls, 'encode', BasicSerializable.encode)
        PythonEncoder.register(cls, encoder)

        # Optionally, register a new decoder.
        # If no decoder is provided, use the basic decoder.
        if hasattr(cls, 'decode') and callable(cls.decode):
            _decoder = weakref.WeakMethod(cls.decode)

            # Note that we do not require that the decoded object is actually
            # an instance of cls.

            def _decode(encoded: dict):
                decoder = _decoder()
                if decoder is None:
                    raise ProtocolError(
                        'Decoding a type that has already been de-registered.')
                return decoder(encoded)

            PythonDecoder.register(cls._dtype, _decode)

        # TODO: Register optional instance initializer / input processor.
        # Allow instances to be created with something other than a single-argument
        # of the registered Input type.

        # TODO: Register/generate UI helper.
        # From the user's perspective, an importable module function interacts
        # with the WorkflowManager to add workflow items and return a handle.
        # Do we want to somehow generate an entry-point command

        # TODO: Register result dispatcher(s).
        # An AbstractDataSource must register a dispatcher to an implementation
        # that produces a ConcreteDataSource that provides the registered Result type.
        # A ConcreteDataSource must provide support for checksum calculation and verification.
        # Optionally, ConcreteDataSource may provide facilities to convert to/from
        # native Python objects or other types (such as .npz files).

        # Proceed dispatching along the MRO, per documented Python data model.
        super().__init_subclass__(**kwargs)
Exemple #10
0
 def register(cls, typeid: TypeIdentifier, handler: typing.Callable):
     # Normalize typeid
     typeid = TypeIdentifier.copy_from(typeid)
     if typeid in cls._dispatchers:
         raise ProtocolError('Type appears to be registered already.')
     cls._dispatchers[typeid] = handler
Exemple #11
0
def test_resource_type():
    scoped_name = ['scalems', 'subprocess', 'SubprocessTask']
    description = scalems.workflow.Description(resource_type=TypeIdentifier(
        tuple(scoped_name)),
                                               shape=(1, ))
    assert description.type() == TypeIdentifier(tuple(scoped_name))