diff --git a/src/rachis/core/transform.py b/src/rachis/core/transform.py index 30edca41c..1d7966abe 100644 --- a/src/rachis/core/transform.py +++ b/src/rachis/core/transform.py @@ -6,10 +6,12 @@ # The full license is in the file LICENSE, distributed with this software. # ---------------------------------------------------------------------------- import pathlib +from typing import get_origin, get_args, Union from rachis import sdk from rachis.plugin import model from rachis.core import util +from rachis.core.exceptions import ValidationError def identity_transformer(view): @@ -19,7 +21,9 @@ def identity_transformer(view): class ModelType: @staticmethod def from_view_type(view_type): - if issubclass(view_type, model.base.FormatBase): + if get_origin(view_type) is Union: + return UnionType(view_type) + elif issubclass(view_type, model.base.FormatBase): if issubclass(view_type, model.SingleFileDirectoryFormatBase): # HACK: this is necessary because we need to be able to "act" @@ -137,7 +141,7 @@ def coerce_view(self, view): def validate(self, view, level='min'): if not isinstance(view, self._view_type): - raise TypeError("%r is not an instance of %r." + raise ValidationError("%r is not an instance of %r." % (view, self._view_type)) # Formats have a validate method, so defer to it view.validate(level) @@ -230,5 +234,76 @@ def wrapped(view): class ObjectType(ModelType): def validate(self, view, level=None): if not isinstance(view, self._view_type): - raise TypeError("%r is not of type %r, cannot transform further." - % (view, self._view_type)) + raise ValidationError( + "%r is not of type %r, cannot transform further." + % (view, self._view_type) + ) + + +class UnionType(ModelType): + @classmethod + def _is_union_type(cls, type): + return get_origin(type) is Union + + def make_transformation(self, other, recorder=None): + transformations = [] + if ( + self._is_union_type(self._view_type) + and self._is_union_type(other._view_type) + ): + from_types = ', '.join( + [str(arg) for arg in get_args(self._view_type)] + ) + to_types = ', '.join( + [str(arg) for arg in get_args(other._view_type)] + ) + message = ( + f'No transformation from any of {from_types} to any of ' + f'{to_types}' + ) + for from_arg in get_args(self._view_type): + for to_arg in get_args(self._view_type): + from_type = ModelType.from_view_type(from_arg) + to_type = ModelType.from_view_type(to_arg) + try: + transformations.append( + from_type.make_transformation(to_type, recorder) + ) + except Exception: + pass + elif self._is_union_type(self._view_type): + from_types = ', '.join( + [str(arg) for arg in get_args(self._view_type)] + ) + message = ( + f'No transformation from any of {from_types} to ' + f'{other._view_type}' + ) + for arg in get_args(self._view_type): + type = ModelType.from_view_type(arg) + try: + transformations.append( + type.make_transformation(other, recorder) + ) + except Exception: + pass + else: + to_types = ', '.join( + [str(arg) for arg in get_args(other._view_type)] + ) + message = f'No transformation from {self._view_type} to {to_types}' + type = ModelType.from_view_type(self._view_type) + for arg in get_args(other._view_type): + arg = ModelType.from_view_type(arg) + try: + transformations.append( + type.make_transformation(arg, recorder) + ) + except Exception as e: + print(e) + pass + + if len(transformations) > 0: + return transformations + + raise Exception(message) diff --git a/src/rachis/sdk/result.py b/src/rachis/sdk/result.py index 29241a42d..129b33afc 100644 --- a/src/rachis/sdk/result.py +++ b/src/rachis/sdk/result.py @@ -15,9 +15,10 @@ import distutils.dir_util import pathlib import json -from typing import Union, get_args, get_origin +from typing import Union, get_origin from rachis.core.format import report +from rachis.core.exceptions import ValidationError import rachis.plugin import rachis.sdk import rachis.core.type @@ -437,13 +438,32 @@ def _from_view(cls, type, view, view_type, provenance_capture, # lookup default format for the type view_type = output_dir_fmt - from_type = transform.ModelType.from_view_type(view_type) + if get_origin(output_dir_fmt) is Union: + from_type = transform.UnionType(view_type) + else: + from_type = transform.ModelType.from_view_type(view_type) + to_type = transform.ModelType.from_view_type(output_dir_fmt) recorder = provenance_capture.transformation_recorder('return') transformation = from_type.make_transformation(to_type, recorder=recorder) - result = transformation(view, validate_level) + + result = None + if isinstance(transformation, list): + for trans in transformation: + try: + result = trans(view, validate_level) + break + except ValidationError: + pass + else: + result = transformation(view, validate_level) + + if result is None: + raise Exception( + f'No valid transformation from {view_type} to {output_dir_fmt}' + ) if type_raw in pm.validators: validation_object = pm.validators[type] @@ -465,32 +485,31 @@ def _view(self, view_type, recorder=None): raise TypeError( "Artifact %r cannot be viewed as Rachis Metadata." % self) - from_type = transform.ModelType.from_view_type(self.format) + if get_origin(view_type) is Union: + from_type = transform.UnionType(self.format) + else: + from_type = transform.ModelType.from_view_type(self.format) + + to_type = transform.ModelType.from_view_type(view_type) + transformation = from_type.make_transformation( + to_type, recorder=recorder + ) - if isinstance(get_origin(view_type), type(Union)): - transformation = None - for arg in get_args(view_type): - to_type = transform.ModelType.from_view_type(arg) + result = None + if isinstance(transformation, list): + for trans in transformation: try: - transformation = from_type.make_transformation( - to_type, recorder=recorder) - if transformation: - break - except Exception as e: - if str(e).startswith("No transformation from"): - continue - else: - raise e - if not transformation: - raise Exception( - "No transformation into either of %s was found" % - ", ".join([str(x) for x in view_type.__args__]) - ) + result = trans(self._archiver.data_dir) + break + except ValidationError: + pass else: - to_type = transform.ModelType.from_view_type(view_type) - transformation = from_type.make_transformation(to_type, - recorder=recorder) - result = transformation(self._archiver.data_dir) + result = transformation(self._archiver.data_dir) + + if result is None: + raise Exception( + f'No valid transformation from {self.format} to {view_type}' + ) if view_type is rachis.Metadata: result._add_artifacts([self]) diff --git a/src/rachis/sdk/tests/test_artifact.py b/src/rachis/sdk/tests/test_artifact.py index 0add93172..4b772739b 100644 --- a/src/rachis/sdk/tests/test_artifact.py +++ b/src/rachis/sdk/tests/test_artifact.py @@ -110,7 +110,7 @@ def test_from_view_union_not_valid(self): self.assertIsInstance(artifact.uuid, uuid.UUID) with self.assertRaisesRegex( Exception, - 'No transformation into either of'): + 'No transformation from'): self.assertEqual(artifact.view(Union[str, dict]), [-1, 42, 0, 43]) def test_from_view_different_type_with_multiple_view_types(self): @@ -666,6 +666,25 @@ def test_cannot_be_viewed_as_metadata(self): 'as Rachis Metadata'): A.view(Metadata) + def test_view_from_union(self): + artifact = Artifact._from_view( + IntSequence1, + [1, 2, 3, 4], + Union[IntSequenceFormat, list], + self.provenance_capture + ) + + self.assertEqual(artifact.type, IntSequence1) + self.assertIsInstance(artifact.uuid, uuid.UUID) + self.assertEqual(artifact.view(list), [1, 2, 3, 4]) + + def test_view_to_union(self): + artifact = Artifact.import_data(IntSequence1, [1, 2, 3, 4]) + artifact = artifact.view(Union[dict, list]) + + self.assertEqual(type(artifact), list) + self.assertEqual(artifact, [1, 2, 3, 4]) + if __name__ == '__main__': unittest.main()