Skip to content

Commit

Permalink
starting to look real
Browse files Browse the repository at this point in the history
  • Loading branch information
pipermerriam committed Apr 12, 2018
1 parent 3b7f04b commit 29cc832
Show file tree
Hide file tree
Showing 4 changed files with 375 additions and 131 deletions.
10 changes: 6 additions & 4 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 @@ -49,7 +50,10 @@ def encode(obj, sedes=None, infer_serializer=True, cache=False):
if obj._cached_rlp and sedes is None:
return obj._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 +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


Expand Down Expand Up @@ -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()
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
245 changes: 221 additions & 24 deletions rlp/sedes/serializable.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import abc
import collections

from eth_utils import (
to_tuple,
)

from rlp.exceptions import (
ListSerializationError,
ObjectSerializationError,
Expand Down Expand Up @@ -33,36 +37,49 @@ def get_sedes(cls):
return List(sedes)


class SerializableMeta(abc.ABCMeta):
def __new__(cls, name, bases, attrs):
fields = attrs.pop('fields', tuple())
@to_tuple
def merge_args_and_kwargs(args, kwargs, arg_name_ordering):
if len(arg_name_ordering) != len(set(arg_name_ordering)):
raise TypeError("duplicate argument names")

fields_meta = type('FieldsMeta', (FieldsMeta,), {'fields': fields})
attrs['_meta'] = fields_meta
return super(SerializableMeta, cls).__new__(cls, name, bases, attrs)
needed_kwargs = arg_name_ordering[len(args):]
used_kwargs = set(arg_name_ordering[:len(args)])

duplicate_kwargs = used_kwargs.intersection(kwargs.keys())
if duplicate_kwargs:
raise TypeError("Duplicate kwargs: {0}".format(sorted(duplicate_kwargs)))

class Serializable(collections.Sequence, metaclass=SerializableMeta):
def __init__(self, *args, **kwargs):
unknown_kwargs = set(kwargs.keys()).difference(arg_name_ordering)
if unknown_kwargs:
raise TypeError("Unknown kwargs: {0}".format(sorted(unknown_kwargs)))

# check keyword arguments are known
field_set = set(self._meta.get_field_names())
missing_kwargs = set(needed_kwargs).difference(kwargs.keys())
if missing_kwargs:
raise TypeError("Missing kwargs: {0}".format(sorted(missing_kwargs)))

# set positional arguments
for field, arg in zip(self._meta.get_field_names(), args):
setattr(self, field, arg)
field_set.remove(field)
yield from args
for arg_name in needed_kwargs:
yield kwargs[arg_name]

# 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)
else:
raise AttributeError("Attempt to set unknown field: {0}".format(field))

if len(field_set) != 0:
raise TypeError('Not all fields initialized. Missing: %r' % field_set)
class BaseSerializable(collections.Sequence):
@property
@abc.abstractmethod
def _cached_rlp(self):
pass

def __init__(self, *args, **kwargs):
field_values = merge_args_and_kwargs(args, kwargs, self._meta.field_names)
if field_values:
field_attrs = self._get_field_attrs()

if len(field_values) != len(field_attrs):
raise TypeError('Argument count mismatch. expected {0} - got {1}'.format(
len(field_attrs),
len(field_values),
))
for value, attr in zip(field_values, field_attrs):
setattr(self, attr, value)

def __iter__(self):
for field in self._meta.get_field_names():
Expand All @@ -78,6 +95,11 @@ def __getitem__(self, idx):
def __len__(self):
return len(self._meta.fields)

def __eq__(self, other):
if not hasattr(other, 'serialize'):
return False
return self.serialize(self) == other.serialize(other)

@classmethod
def get_sedes(cls):
return cls._meta.get_sedes()
Expand Down Expand Up @@ -107,9 +129,184 @@ def deserialize(cls, serial, exclude=None, mutable=False, **kwargs):
obj = cls(**dict(list(params.items()) + list(kwargs.items())))

# TODO: mutability
return obj
if mutable:
return obj.as_mutable()
else:
return obj.as_immutable()

@classmethod
def exclude(cls, excluded_fields):
"""Create a new sedes considering only a reduced set of fields."""
raise NotImplementedError("Not yet implemented")

@property
def is_immutable(self):
return not self.is_mutable

#
# Abstract methods
#
@abc.abstractmethod
def as_immutable(self):
pass

@abc.abstractmethod
def as_mutable(self):
pass

@abc.abstractmethod
def _get_field_attrs(self):
pass


def make_immutable(value):
if hasattr(value, 'as_immutable'):
return value.as_immutable()
elif not isinstance(value, (bytes, str)) and isinstance(value, collections.Sequence):
return tuple(make_immutable(item) for item in value)
else:
return value


def make_mutable(value):
if hasattr(value, 'as_mutable'):
return value.as_mutable()
elif not isinstance(value, (bytes, str)) and isinstance(value, collections.Sequence):
return list(make_mutable(item) for item in value)
else:
return value


class ReadOnlySerializable:
is_mutable = False

_cached_rlp_value = None

@property
def _cached_rlp(self):
return self._cached_rlp_value

@_cached_rlp.setter
def _cached_rlp(self, value):
self._cached_rlp_value = value

def as_immutable(self):
return self

def as_mutable(self):
return self._meta.mutable_cls(*(
make_mutable(arg)
for arg
in self
))

def _get_field_attrs(self):
return self._meta.field_aliases


class WritableSerializable(BaseSerializable):
is_mutable = True

_cached_rlp_value = None
_cached_rlp_key = None

def _mk_rlp_cache_key(self):
return tuple(id(v) for v in self)

@property
def _cached_rlp(self):
cache_key = self._mk_rlp_cache_key()
if cache_key == self._cached_rlp_key:
return self._cached_rlp_value
else:
return None

@_cached_rlp.setter
def _cached_rlp(self, value):
self._cached_rlp_value = value
self._cached_rlp_key = self._mk_rlp_cache_key()

def as_immutable(self):
return self._meta.immutable_cls(*(
make_immutable(arg)
for arg
in self
))

def as_mutable(self):
return self

def _get_field_attrs(self):
return self._meta.field_names


@to_tuple
def mk_field_aliases(field_names):
for field in field_names:
while True:
field = '_' + field
if field not in field_names:
yield field
break


def mk_field_property(field, attr):
def field_fn(self):
return getattr(self, attr)
field_fn.__name__ = field
return field_fn


class SerializableMeta(abc.ABCMeta):
def __new__(cls, name, bases, attrs):
fields = attrs.pop('fields', tuple())
if fields:
field_names, sedes = zip(*fields)
else:
field_names = tuple()
sedes = tuple()

field_aliases = mk_field_aliases(field_names)

meta_namespace = {
'fields': fields,
'field_names': field_names,
'field_aliases': field_aliases,
'sedes': sedes,
}

meta = type(
'Meta',
(FieldsMeta,),
meta_namespace,
)
attrs['_meta'] = meta

read_only_props = {
field: property(mk_field_property(field, attr))
for field, attr
in zip(field_names, field_aliases)
}
meta.immutable_cls = super(SerializableMeta, cls).__new__(
cls,
name,
(ReadOnlySerializable, ) + bases,
dict(
tuple(attrs.items()) +
tuple(read_only_props.items()) +
(('__slots__', field_aliases),)
)
)

slots = meta.get_field_names()
meta.mutable_cls = super(SerializableMeta, cls).__new__(
cls,
name,
bases,
dict(tuple(attrs.items()) + (('__slots__', slots),)),
)
return meta.mutable_cls


class Serializable(WritableSerializable, metaclass=SerializableMeta):
pass
Loading

0 comments on commit 29cc832

Please sign in to comment.