Skip to content

Commit

Permalink
Rewrite of Serializable class
Browse files Browse the repository at this point in the history
  • Loading branch information
pipermerriam committed Apr 16, 2018
1 parent f30fe1e commit 738d9da
Show file tree
Hide file tree
Showing 7 changed files with 497 additions and 366 deletions.
15 changes: 9 additions & 6 deletions rlp/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from rlp.exceptions import EncodingError, DecodingError
from rlp.sedes.binary import Binary as BinaryClass
from rlp.sedes import big_endian_int, binary
from rlp.sedes.lists import List, Serializable, is_sedes
from rlp.sedes.lists import List, is_sedes
from rlp.sedes.serializable import Serializable
from rlp.utils import ALL_BYTES


Expand Down Expand Up @@ -46,10 +47,14 @@ def encode(obj, sedes=None, infer_serializer=True, cache=False):
:raises: :exc:`rlp.SerializationError` if the serialization fails
"""
if isinstance(obj, Serializable):
if obj._cached_rlp and sedes is None:
return obj._cached_rlp
cached_rlp = obj._cached_rlp
if sedes is None and cached_rlp:
return cached_rlp
else:
really_cache = cache if sedes is None else False
really_cache = (
cache and
sedes is None
)
else:
really_cache = False

Expand All @@ -63,7 +68,6 @@ def encode(obj, sedes=None, infer_serializer=True, cache=False):
result = encode_raw(item)
if really_cache:
obj._cached_rlp = result
obj.make_immutable()
return result


Expand Down Expand Up @@ -218,7 +222,6 @@ def decode(rlp, sedes=None, strict=True, **kwargs):
obj = sedes.deserialize(item, **kwargs)
if hasattr(obj, '_cached_rlp'):
obj._cached_rlp = rlp
assert not isinstance(obj, Serializable) or not obj.is_mutable()
return obj
else:
return item
Expand Down
2 changes: 1 addition & 1 deletion rlp/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, message=None, obj=None, sedes=None, list_exception=None):
'("{}")'.format(str(list_exception)))
else:
assert sedes is not None
field = sedes.fields[list_exception.index][0]
field = sedes._meta.field_names[list_exception.index]
message = ('Serialization failed because of field {} '
'("{}")'.format(field, str(list_exception.element_exception)))
else:
Expand Down
3 changes: 2 additions & 1 deletion rlp/sedes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import raw # noqa: F401
from .binary import Binary, binary # noqa: F401
from .big_endian_int import BigEndianInt, big_endian_int # noqa: F401
from .lists import CountableList, List, Serializable, make_immutable, make_mutable # noqa: F401
from .lists import CountableList, List # noqa: F401
from .serializable import Serializable, make_immutable, make_mutable # noqa: F401
187 changes: 0 additions & 187 deletions rlp/sedes/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
from rlp.exceptions import (
SerializationError,
ListSerializationError,
ObjectSerializationError,
DeserializationError,
ListDeserializationError,
ObjectDeserializationError,
)

from .binary import (
Expand Down Expand Up @@ -140,188 +138,3 @@ def deserialize(self, serial):
yield self.element_sedes.deserialize(element)
except DeserializationError as e:
raise ListDeserializationError(serial=serial, element_exception=e, index=index)


class Serializable(object):

"""Base class for objects which can be serialized into RLP lists.
:attr:`fields` defines which attributes are serialized and how this is
done. It is expected to be an ordered sequence of 2-tuples
``(name, sedes)``. Here, ``name`` is the name of an attribute and ``sedes``
is the sedes object that will be used to serialize the corresponding
attribute. The object as a whole is then serialized as a list of those
fields.
:cvar fields: a list of 2-tuples ``(name, sedes)`` where ``name`` is a
string corresponding to an attribute and ``sedes`` is the
sedes object used for (de)serializing the attribute.
:param \*args: initial values for the first attributes defined via
:attr:`fields`
:param \*\*kwargs: initial values for all attributes not initialized via
positional arguments
:ivar _cached_rlp: can be used to store the object's RLP code (by default
`None`)
:ivar _mutable: if `False`, all attempts to set field values will fail (by
default `True`, unless created with :meth:`deserialize`)
"""

fields = tuple()
_cached_sedes = {}
_mutable = True
_cached_rlp = None

def __init__(self, *args, **kwargs):

# check keyword arguments are known
field_set = set(field for field, _ in self.fields)

# set positional arguments
for (field, _), arg in zip(self.fields, args):
setattr(self, field, arg)
field_set.remove(field)

# set keyword arguments, if not already set
for (field, value) in kwargs.items():
if field in field_set:
setattr(self, field, value)
field_set.remove(field)

if len(field_set) != 0:
raise TypeError('Not all fields initialized. Missing: %r' % field_set)

def __setattr__(self, attr, value):
try:
mutable = self.is_mutable()
except AttributeError:
mutable = True
self.__dict__['_mutable'] = True # don't call __setattr__ again
if mutable or attr not in set(field for field, _ in self.fields):
super(Serializable, self).__setattr__(attr, value)
else:
raise ValueError('Tried to mutate immutable object')

def __eq__(self, other):
"""Two objects are equal, if they are equal after serialization."""
if not hasattr(other.__class__, 'serialize'):
return False
return self.serialize(self) == other.serialize(other)

def __ne__(self, other):
return not self == other

def is_mutable(self):
"""Checks if the object is mutable"""
return self._mutable

def make_immutable(self):
"""Make it immutable to prevent accidental changes.
`obj.make_immutable` is equivalent to `make_immutable(obj)`, but doesn't return
anything.
"""
make_immutable(self)

def make_mutable(self):
"""Make it mutable.
`obj.make_mutable` is equivalent to `make_mutable(obj)`, but doesn't return
anything.
"""
make_mutable(self)

@classmethod
def get_sedes(cls):
if cls not in cls._cached_sedes:
cls._cached_sedes[cls] = List(sedes for _, sedes in cls.fields)
return cls._cached_sedes[cls]

@classmethod
def serialize(cls, obj):
try:
field_values = [getattr(obj, field) for field, _ in cls.fields]
except AttributeError:
raise ObjectSerializationError('Cannot serialize this object (missing attribute)', obj)
try:
result = cls.get_sedes().serialize(field_values)
except ListSerializationError as e:
raise ObjectSerializationError(obj=obj, sedes=cls, list_exception=e)
else:
return result

@classmethod
def deserialize(cls, serial, exclude=None, mutable=False, **kwargs):
try:
values = cls.get_sedes().deserialize(serial)
except ListDeserializationError as e:
raise ObjectDeserializationError(serial=serial, sedes=cls, list_exception=e)

params = {
field: value
for (field, _), value
in zip(cls.fields, values)
if not exclude or field not in exclude
}
obj = cls(**dict(list(params.items()) + list(kwargs.items())))
if mutable:
return make_mutable(obj)
else:
return make_immutable(obj)

@classmethod
def exclude(cls, excluded_fields):
"""Create a new sedes considering only a reduced set of fields."""
class SerializableExcluded(cls):
fields = [(field, sedes) for field, sedes in cls.fields
if field not in excluded_fields]
_sedes = None
return SerializableExcluded


def make_immutable(x):
"""Do your best to make `x` as immutable as possible.
If `x` is a sequence, apply this function recursively to all elements and return a tuple
containing them. If `x` is an instance of :class:`rlp.Serializable`, apply this function to its
fields, and set :attr:`_mutable` to `False`. If `x` is neither of the above, just return `x`.
:returns: `x` after making it immutable
"""
if isinstance(x, Serializable):
x._mutable = True
for field, _ in x.fields:
attr = getattr(x, field)
try:
setattr(x, field, make_immutable(attr))
except AttributeError:
pass # respect read only properties
x._mutable = False
return x
elif is_sequence(x):
return tuple(make_immutable(element) for element in x)
else:
return x


def make_mutable(x):
"""Do your best to make `x` as mutable as possible.
If `x` is a sequence, apply this function recursively to all elements and return a tuple
containing them. If `x` is an instance of :class:`rlp.Serializable`, apply this function to its
fields, and set :attr:`_mutable` to `False`. If `x` is neither of the above, just return `x`.
:returns: `x` after making it mutable
"""
if isinstance(x, Serializable):
x._mutable = True
for field, _ in x.fields:
attr = getattr(x, field)
try:
setattr(x, field, make_mutable(attr))
except AttributeError:
pass # respect read only properties
return x
elif is_sequence(x):
return list(make_mutable(element) for element in x)
else:
return x
Loading

0 comments on commit 738d9da

Please sign in to comment.