Skip to content

Commit

Permalink
renamed rlp_ to _cached_rlp and mutable_ to _mutable
Browse files Browse the repository at this point in the history
also introduced convenience function make_immutable
  • Loading branch information
jnnk committed Sep 24, 2015
1 parent 61fe8d3 commit 48e1aec
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 34 deletions.
2 changes: 1 addition & 1 deletion rlp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
from .exceptions import RLPException, EncodingError, DecodingError, \
SerializationError, DeserializationError
from .lazy import decode_lazy, peek, LazyList
from .sedes import Serializable
from .sedes import Serializable, make_immutable
48 changes: 32 additions & 16 deletions rlp/codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,53 @@
from itertools import imap as map


def encode(obj, sedes=None, infer_serializer=True):
def encode(obj, sedes=None, infer_serializer=True, cache=False):
"""Encode a Python object in RLP format.
By default, the object is serialized in a suitable way first (using :func:`rlp.infer_sedes`)
and then encoded. Serialization can be explicitly suppressed by setting `infer_serializer` to
``False`` and not passing an alternative as `sedes`.
If `obj` has an attribute :attr:`rlp_` (as, notably, :class:`rlp.Serializable`) and its value
is not `None`, this value is returned bypassing serialization and encoding, unless `sedes` is
given (as `rlp_` is assumed to refer to the standard serialization which can be replaced by
specifying `sedes`).
If `obj` has an attribute :attr:`_cached_rlp` (as, notably, :class:`rlp.Serializable`) and its
value is not `None`, this value is returned bypassing serialization and encoding, unless
`sedes` is given (as the cache is assumed to refer to the standard serialization which can be
replaced by specifying `sedes`).
If `obj` is a :class:`rlp.Serializable` and `cache` is true, the result of the encoding will be
stored in :attr:`_cached_rlp` if it is empty and :meth:`rlp.Serializable.make_immutable` will
be invoked on `obj`.
:param sedes: an object implementing a function ``serialize(obj)`` which will be used to
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`)
: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 hasattr(obj, 'rlp_') and obj.rlp_ and sedes is None:
return obj.rlp_
if isinstance(obj, Serializable):
if obj._cached_rlp:
return obj._cached_rlp
else:
really_cache = cache
else:
really_cache = False

if sedes:
item = sedes.serialize(obj)
elif infer_serializer:
item = infer_sedes(obj).serialize(obj)
else:
item = obj
return encode_raw(item)

result = encode_raw(item)
if really_cache:
obj._cached_rlp = result
obj.make_immutable()
return result


class RLPData(str):
Expand Down Expand Up @@ -168,10 +185,10 @@ def consume_item(rlp, start):
def decode(rlp, sedes=None, strict=True, **kwargs):
"""Decode an RLP encoded object.
If the deserialized result has an attribute :attr:`rlp_` (e.g. if `sedes` is a subclass of
:class:`rlp.sedes.Serializable`) it will be set to `rlp`, which will improve performance on
subsequent encode calls. Bear in mind however that `obj` needs to make sure that this value is
updated whenever one of its fields changes or prevent such changes entirely
If the deserialized result `obj` has an attribute :attr:`_cached_rlp` (e.g. if `sedes` is a
subclass of :class:`rlp.Serializable`) it will be set to `rlp`, which will improve performance
on subsequent :func:`rlp.encode` calls. Bear in mind however that `obj` needs to make sure that
this value is updated whenever one of its fields changes or prevent such changes entirely
(:class:`rlp.sedes.Serializable` does the latter).
:param sedes: an object implementing a function ``deserialize(code)`` which will be applied
Expand All @@ -193,10 +210,9 @@ def decode(rlp, sedes=None, strict=True, **kwargs):
raise DecodingError(msg, rlp)
if sedes:
obj = sedes.deserialize(item, **kwargs)
if hasattr(obj, 'rlp_'):
obj.rlp_ = rlp
if isinstance(obj, Serializable):
assert not obj.mutable_
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/sedes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import raw
from .binary import Binary, binary
from .big_endian_int import BigEndianInt, big_endian_int
from .lists import CountableList, List, Serializable
from .lists import CountableList, List, Serializable, make_immutable
53 changes: 45 additions & 8 deletions rlp/sedes/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,18 @@ class Serializable(object):
:attr:`fields`
:param \*\*kwargs: initial values for all attributes not initialized via
positional arguments
:ivar 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
: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()
_sedes = None

def __init__(self, *args, **kwargs):
# set mutable_ through __dict__ because it is used by __setattr__
self.mutable_ = True
self.rlp_ = None
self._mutable = True
self._cached_rlp = None

# check keyword arguments are known
field_set = set(field for field, _ in self.fields)
Expand All @@ -158,10 +158,10 @@ def __init__(self, *args, **kwargs):

def __setattr__(self, attr, value):
try:
mutable = self.mutable_
mutable = self.is_mutable()
except AttributeError:
mutable = True
self.__dict__['mutable_'] = True # don't call __setattr__ again
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:
Expand All @@ -176,6 +176,18 @@ def __eq__(self, 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)

@classmethod
def get_sedes(cls):
if not cls._sedes:
Expand Down Expand Up @@ -210,7 +222,7 @@ def deserialize(cls, serial, exclude=None, **kwargs):
for k in exclude:
del params[k]
obj = cls(**dict(list(params.items()) + list(kwargs.items())))
obj.mutable_ = False
obj._mutable = False
return obj

@classmethod
Expand All @@ -221,3 +233,28 @@ class SerializableExcluded(cls):
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
66 changes: 58 additions & 8 deletions tests/test_serializable.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
from rlp import SerializationError
from rlp import infer_sedes, Serializable, encode, decode
from rlp import infer_sedes, Serializable, encode, decode, make_immutable
from rlp.sedes import big_endian_int, binary, List


Expand Down Expand Up @@ -67,9 +67,9 @@ def test_serializable():
test1a_d = Test1.deserialize(serial_1a)
test1b_d = Test1.deserialize(serial_1b)
test2_d = Test2.deserialize(serial_2)
assert not test1a_d.mutable_
assert not test1b_d.mutable_
assert not test2_d.mutable_
assert not test1a_d.is_mutable()
assert not test1b_d.is_mutable()
assert not test2_d.is_mutable()
for obj in (test1a_d, test1b_d):
before1 = obj.field1
before2 = obj.field2
Expand All @@ -86,9 +86,59 @@ def test_serializable():
# encoding and decoding
for obj in (test1a, test1b, test2):
rlp_code = encode(obj)
assert obj.rlp_ is None
assert obj.mutable_
assert obj._cached_rlp is None
assert obj.is_mutable()

assert encode(obj, cache=True) == rlp_code
assert obj._cached_rlp == rlp_code
assert not obj.is_mutable()

assert encode(obj, cache=True) == rlp_code
assert obj._cached_rlp == rlp_code
assert not obj.is_mutable()

assert encode(obj) == rlp_code
assert obj._cached_rlp == rlp_code
assert not obj.is_mutable()

obj_decoded = decode(rlp_code, obj.__class__)
assert obj_decoded == obj
assert not obj_decoded.mutable_
assert obj_decoded.rlp_ == rlp_code
assert not obj_decoded.is_mutable()
assert obj_decoded._cached_rlp == rlp_code


def test_make_immutable():
assert make_immutable(1) == 1
assert make_immutable('a') == 'a'
assert make_immutable((1, 2, 3)) == (1, 2, 3)
assert make_immutable([1, 2, 'a']) == (1, 2, 'a')
assert make_immutable([[1], [2, [3], 4], 5, 6]) == ((1,), (2, (3,), 4), 5, 6)

t1a_data = (5, 'a', (0, ''))
t1b_data = (9, 'b', (2, ''))
test1a = Test1(*t1a_data)
test1b = Test1(*t1b_data)
test2 = Test2(test1a, [test1a, test1b])

assert test2.is_mutable()
assert test2.field1.is_mutable()
assert test2.field2[0].is_mutable()
assert test2.field2[1].is_mutable()
test2.make_immutable()
assert not test2.is_mutable()
assert not test1a.is_mutable()
assert not test1b.is_mutable()
assert test2.field1 == test1a
assert test2.field2 == (test1a, test1b)

test1a = Test1(*t1a_data)
test1b = Test1(*t1b_data)
test2 = Test2(test1a, [test1a, test1b])
assert test2.is_mutable()
assert test2.field1.is_mutable()
assert test2.field2[0].is_mutable()
assert test2.field2[1].is_mutable()
assert make_immutable([test1a, [test2, test1b]]) == (test1a, (test2, test1b))
assert not test2.is_mutable()
assert not test1a.is_mutable()
assert not test1b.is_mutable()

0 comments on commit 48e1aec

Please sign in to comment.