Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Serializable class. #62

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions rlp/codec.py
Original file line number Diff line number Diff line change
@@ -11,11 +11,12 @@
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


def encode(obj, sedes=None, infer_serializer=True, cache=False):
def encode(obj, sedes=None, infer_serializer=True, cache=True):
"""Encode a Python object in RLP format.

By default, the object is serialized in a suitable way first (using
@@ -37,19 +38,22 @@ def encode(obj, sedes=None, infer_serializer=True, cache=False):
serialize ``obj`` before encoding, or ``None`` to use the infered one (if any)
:param infer_serializer: if ``True`` an appropriate serializer will be selected using
:func:`rlp.infer_sedes` to serialize `obj` before encoding
:param cache: cache the return value in `obj._cached_rlp` if possible and
make `obj` immutable
(default `False`)
:param cache: cache the return value in `obj._cached_rlp` if possible
(default `True`)
:returns: the RLP encoded item
:raises: :exc:`rlp.EncodingError` in the rather unlikely case that the item is too big to
encode (will not happen)
: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

@@ -63,7 +67,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


@@ -218,7 +221,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()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was removed in favor of having mutable Serializable instances automatically invalidate their cache.

return obj
else:
return item
2 changes: 1 addition & 1 deletion rlp/exceptions.py
Original file line number Diff line number Diff line change
@@ -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:
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
@@ -11,10 +11,8 @@
from rlp.exceptions import (
SerializationError,
ListSerializationError,
ObjectSerializationError,
DeserializationError,
ListDeserializationError,
ObjectDeserializationError,
)

from .binary import (
@@ -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