Source code for schrodinger.models.jsonable

"""
A module for defining jsonable versions of classes (typically classes defined
in third-party modules).

You can also find the registry of classes that are supported by the load(s) and
dump(s) functions in `schrodinger.model.json`. Any object that is an instance
of one of the registered classes will be automatically jsonable using `dump`
and `dumps`. To deserialize, you must specify the registered class to
`load` or `loads`. Example::

    from schrodinger.models import json
    my_set = set(range(1,2,3))
    my_set_jsonstr = json.dumps(my_set)
    new_set = json.loads(my_set_jsonstr, DataClass=set)
    assert new_set == my_set
    assert isinstance(new_set, set)


Currently registered DataClasses:
    - structure.Structure
    - set
    - tuple
    - rdkit.Chem.rdchem.Mol
"""
import collections
import enum

from rdkit import Chem

from schrodinger import structure
from schrodinger.models import json
from schrodinger.models.json import JsonableClassMixin


[docs]class JsonableSet(JsonableClassMixin, set): # String used to signal that the list actually represents a set. ENCODING_KEY = '_python_set_'
[docs] def toJsonImplementation(self): return list(self) + [self.ENCODING_KEY]
[docs] @classmethod def fromJsonImplementation(cls, json_list): if not json_list or json_list.pop() != cls.ENCODING_KEY: err_msg = f'Given list was not originally encoded using {cls.__name__}' raise ValueError(err_msg) return cls(json_list)
[docs] def copy(self): return JsonableSet(self)
[docs]class JsonableStructure(JsonableClassMixin, structure.Structure):
[docs] def toJsonImplementation(self): return self.writeToString(structure.MAESTRO)
[docs] @classmethod def fromJsonImplementation(cls, json_str): assert json_str is not None with structure.StructureReader.fromString(json_str) as reader: try: return next(reader) except StopIteration: raise json.JSONDecodeError('No structure found', json_str, 0)
[docs]class JsonableTuple(JsonableClassMixin, tuple): # String used to signal that the list actually represents a set. ENCODING_KEY = '_python_tuple_'
[docs] def toJsonImplementation(self): return list(self) + [self.ENCODING_KEY]
[docs] @classmethod def fromJsonImplementation(cls, json_list): if not json_list or json_list.pop() != cls.ENCODING_KEY: err_msg = f'Given list was not originally encoded using {cls.__name__}' raise ValueError(err_msg) return cls(json_list)
class _JsonableNamedTupleMeta(type): """ Create a jsonable named tuple class. """ def __new__(cls, cls_name, bases, cls_dict): if cls_name == 'JsonableNamedTuple': return super().__new__(cls, cls_name, bases, cls_dict) fields = cls_dict.get('__annotations__', {}) namedtuple_cls = collections.namedtuple('_JsonableNamedTuple', list(fields.keys())) def toJsonImplementation(self): return JsonableTuple(self) @classmethod def fromJsonImplementation(cls, json_list): values = list(JsonableTuple.fromJson(json_list)) for value_idx, value_type in enumerate( cls.__annotations__.values()): values[value_idx] = json.decode(values[value_idx], DataClass=value_type) return cls(*values) bases = tuple([JsonableClassMixin] + namedtuple_cls.mro()) cls_dict = { 'toJsonImplementation': toJsonImplementation, 'fromJsonImplementation': fromJsonImplementation, '__annotations__': fields } jsonable_namedtuple_cls = type(cls_name, bases, cls_dict) # This is functionally equivalent to: # class cls_name(JsonableClassMixin, namedtuple_cls): # def toJsonImplementation(self): # return JsonableTuple(self) # @classmethod # def fromJsonImplementation(cls, json_list): # # ... # __annotations__ = fields return jsonable_namedtuple_cls
[docs]class JsonableNamedTuple(JsonableClassMixin, metaclass=_JsonableNamedTupleMeta): """ A jsonabled NamedTuple that behaves like a normal named tuple but is jsonable if its fields are jsonable. Example:: class Coordinate(JsonableNamedTuple): x: float y: float description: str coord = Coordinate(x=1, y=2, description="molecule coord") assert coord == (1, 2, "molecule coord") serialized_coord = json.dumps(c) deserialized_coord = json.loads(serialized_coord, DataClass=Coordinate) assert deserialized_coord == (1, 2, "molecule coord") WARNING:: Instances of subclasses of this class will not evaluate as instances of `JsonableNamedTuple`. This replicates the behavior of `typing.NamedTuple`. """ pass
class _JsonableEnumBase(JsonableClassMixin): """ The Enum class checks mixins to see if __reduce_ex__ is defined. If it isn't, it makes the class unpicklable and as a consequence undeepcopyable. We just use the regular Enum's picklable protocol since JsonableClassMixin doesn't need anything extra. """ def __reduce_ex__(self, proto): return enum.Enum.__reduce_ex__(self, proto)
[docs]class JsonableEnum(_JsonableEnumBase, enum.Enum):
[docs] def __init__(self, *args, **kwargs): self._setJsonAdapters()
[docs] @classmethod def fromJsonImplementation(cls, json_obj): return cls(json_obj)
[docs] def toJsonImplementation(self): return self.value
[docs]class JsonableIntEnum(int, JsonableClassMixin, enum.Enum):
[docs] def __init__(self, *args, **kwargs): self._setJsonAdapters()
[docs] @classmethod def fromJsonImplementation(cls, json_obj): return cls(json_obj)
[docs] def toJsonImplementation(self): return self.value
class _JsonableMolWrapper(JsonableClassMixin): def __init__(self, mol_block=None): if mol_block is None: self._mol = Chem.rdchem.Mol() else: self._mol = Chem.MolFromMolBlock(mol_block) def toJsonImplementation(self): return Chem.MolToMolBlock(self._mol) @classmethod def fromJsonImplementation(cls, json_str): return cls(mol_block=json_str) """---------------------- DataClass Registry ---------------------------------- To add a new class to the registry, subclass `AbstractJsonSerializer` and implement the abstract variables and methods. See `AbstractJsonSerializer` for more information. """
[docs]class AbstractJsonSerializer: """ A class for defining how serialization should be done for a particular object. This should only be used if you're unable to use `json.JsonableClassMixin`. This can be used in conjunction with `json.load(s)` and `json.dump(s)`. Subclasses must define `ObjectClass` and `JsonableClass` and override `objectFromJsonable` and `jsonableFromObject`. Create a subclass here to add a new class to the global default serialization registry. (Consult with relevant parties before doing so...) :cvar ObjectClass: The non-jsonable third-party class (e.g. set, rdkit.Mol, etc.) :cvar JsonableClass: The class that subclasses `ObjectClass` and mixes in JsonableClassMixin. """ ObjectClass = NotImplemented JsonableClass = NotImplemented
[docs] def __init__(self): raise TypeError("Serializers should not be instantiated.")
[docs] @classmethod def objectFromJsonable(cls, jsonable_obj): """ Return an instance of `ObjectClass` from an instance of `JsonableClass` """ raise NotImplementedError()
[docs] @classmethod def jsonableFromObject(cls, obj): """ Return an instance of `JsonableClass` from an instance of `ObjectClass` """ raise NotImplementedError()
[docs] @classmethod def objectFromJson(cls, json_obj): """ DO NOT OVERRIDE. Return an instance of ObjectClass from a json object (i.e. an object made up of json native types). """ jsonable_obj = json.decode(json_obj, DataClass=cls.JsonableClass) return cls.objectFromJsonable(jsonable_obj)
[docs]class StructureSerializer(AbstractJsonSerializer): ObjectClass = structure.Structure JsonableClass = JsonableStructure
[docs] @classmethod def objectFromJsonable(cls, jsonable_structure): return structure.Structure(jsonable_structure.handle)
[docs] @classmethod def jsonableFromObject(cls, structure_): return JsonableStructure(structure_.handle)
[docs]class TupleSerializer(AbstractJsonSerializer): ObjectClass = tuple JsonableClass = JsonableTuple
[docs] @classmethod def objectFromJsonable(cls, jsonable_tuple): return tuple(jsonable_tuple)
[docs] @classmethod def jsonableFromObject(cls, tuple_): return JsonableTuple(tuple_)
[docs]class SetSerializer(AbstractJsonSerializer): ObjectClass = set JsonableClass = JsonableSet
[docs] @classmethod def objectFromJsonable(cls, jsonable_set): return set(jsonable_set)
[docs] @classmethod def jsonableFromObject(cls, set_): return JsonableSet(set_)
[docs]class MolSerializer(AbstractJsonSerializer): ObjectClass = Chem.rdchem.Mol JsonableClass = _JsonableMolWrapper
[docs] @classmethod def objectFromJsonable(cls, jsonable_mol): mol_block = Chem.MolToMolBlock(jsonable_mol._mol) return Chem.MolFromMolBlock(mol_block)
[docs] @classmethod def jsonableFromObject(cls, mol): mol_block = Chem.MolToMolBlock(mol) return _JsonableMolWrapper.fromJson(mol_block)
DATACLASS_REGISTRY = {} for name, attr in dict(globals()).items(): try: is_serializer = issubclass(attr, AbstractJsonSerializer) except TypeError: continue if is_serializer: serializer = attr if serializer.ObjectClass is not NotImplemented: DATACLASS_REGISTRY[serializer.ObjectClass] = serializer
[docs]def get_default_serializer(DataClass): return DATACLASS_REGISTRY.get(DataClass)