Allow for easier construction of serializable objects#37
Allow for easier construction of serializable objects#37LivInTheLookingGlass wants to merge 13 commits intovsergeev:masterfrom
Conversation
|
Is there any interest in merging this feature? It would be fairly useful. |
|
I'll take a look in more detail soon and might incorporate it in upcoming v2.6.0. |
Say you have a bit of code like this:
```python
class Message(Ext):
type = 0 # for MessagePack serialization purposes
def __new__(cls, msg_type: MessageType, *args, **kwargs):
if cls is Message:
return msg_type.associated_class(msg_type, *args, **kwargs)
return object.__new__(cls)
```
This is designed for things to inherit from Message, but if anything does, then you get
conflicts in auto_handlers as originally designed. This patch fixes that.
|
Any news on this? I'm working on a project that would probably benefit from not having to maintain this patch in my own codebase. (More than willing to help maintain, it's just a pain to do downstream) |
|
I'll take a look this week. It's been on my todo list (for some time admittedly) to get this feature into v2.6.0. |
|
I like the Ext subclassing, the dynamic lookup without Ideally, there would be some sort of registration interface at the class definition level that's only called during class creation. This way umsgpack can build a dictionary once with all the classes associated with Ext types. I looked into a metaclass approach that took keyword arguments to try to do something like: class MyType(metaclass=umsgpack.ExtInterface, ext_type=123):
def pack(self):
pass
@classmethod
def unpack(cls, data):
passand while this is doable, it appears that passing the arguments to the metaclass like that is Python 3 only. Also, any metaclass approach interacts very poorly with the After a few other dead ends, it seems like a class decorator might be the easiest solution: import typing
# umsgpack
ext_classes = {}
def ext_serializable(ext_type):
def wrapper(cls):
# assert ext_type is unique
# assert cls has pack() and unpack()
ext_classes[ext_type] = cls
ext_classes[cls] = ext_type
return cls
return wrapper
def pack(obj):
if obj.__class__ in ext_classes:
return obj.pack()
def unpack(ext_type, data):
return ext_classes[ext_type].unpack(data)
# user code
@ext_serializable(5)
class MySerializableTuple(typing.NamedTuple):
foo: str
bar: str
def pack(self):
return self.foo + "," + self.bar
@classmethod
def unpack(cls, data):
return cls(*data.split(","))
# example
x = MySerializableTuple("abc", "def")
print(x) # -> MySerializableTuple(foo='abc', bar='def')
print(pack(x)) # -> abc,def
print(unpack(5, pack(x))) # -> MySerializableTuple(foo='abc', bar='def')This has the advantages of being simple, interacting well with |
|
I'm not a huge fan of the decorator approach, though I can see why it would be useful here. If we were wanting to support python 3 only, my instinct would be to use init_subclass to handle this, but it seems like you want to support 2.* well past its end of life, which I mostly get. Just to plot out my pro/cons here: Pros:
Cons:
A middle of the road approach might be to change how it handles the ext_handlers instead, so instead of parsing through that each time it would:
As for the packing part, the other way I have been doing that is with a property for the data field. It would be trivial to set up a template that caches data and clears it when a property is mutated. Would that address your concerns? |
|
Mostly I don't see the need of visiting/caching the subclass list if we can just register the ext type at definition time and do a simple lookup during unpacking. I agree that inheritance semantics would be less confusing and more intuitive, but I haven't found a clean way to handle NamedTuples with them, and I think the approach will generally require a special # umsgpack
ext_classes = {}
class ExtInterfaceMeta(type):
def __new__(cls, name, bases, dct):
new_cls = type.__new__(cls, name, bases, dct)
if 'ext_type' in dct:
ext_classes[dct['ext_type']] = new_cls
return new_cls
class ExtInterface(metaclass=ExtInterfaceMeta):
def pack(self):
raise NotImplementedError('Pack not implemented')
@classmethod
def unpack(cls, data):
raise NotImplementedError('Unpack not implemented')
def pack(obj):
if isinstance(obj, ExtInterface):
return obj.pack()
def unpack(ext_type, data):
return ext_classes[ext_type].unpack(data)
# user code
class MySerializableTuple(ExtInterface):
ext_type = 5
def __init__(self, foo, bar):
self.foo = foo
self.bar = bar
def pack(self):
return self.foo + "," + self.bar
@classmethod
def unpack(cls, data):
return cls(*data.split(","))
def __repr__(self):
return "MySerializableTuple(foo='{}', bar='{}')".format(self.foo, self.bar)
# example
x = MySerializableTuple("abc", "def")
print(x) # -> MySerializableTuple(foo='abc', bar='def')
print(pack(x)) # -> abc,def
print(unpack(5, pack(x))) # -> MySerializableTuple(foo='abc', bar='def') |
|
Okay, so I've sat on this for a while now, and I think I have a way to make it work. We do a two-stage thing.
This would basically end up being something like from abc import ABCMeta
from itertools import chain
try:
from typing import Any, Dict, List, Tuple, Type, Union, TYPE_CHECKING
except ImportError:
TYPE_CHECKING = False
class MessagePackableMeta(ABCMeta):
def __instancecheck__(self, instance):
if isinstance(instance, (int, float, bool, str, bytes, type(None))):
return True
if isinstance(instance, (list, tuple)):
return all(isinstance(x, self) for x in instance)
if isinstance(instance, dict):
return all(isinstance(x, self) for x in chain(instance, instance.values()))
return super().__instancecheck__(instance)
class _MessagePackable(metaclass=MessagePackableMeta):
pass
for type_ in (int, float, bool, str, bytes, type(None)):
_MessagePackable.register(type_)
del type_
if TYPE_CHECKING:
MessagePackable = Union[_MessagePackable, float, str, bytes, None, List[Any], Tuple[Any], Dict[Any, Any]]
else:
MessagePackable = _MessagePackable
ext_classes = {}
def pack(obj):
if obj.__class__ in ext_classes:
return obj.pack()
def unpack(ext_type, data):
return ext_classes[ext_type].unpack(data)
def ext_serializable(ext_type):
def wrapper(cls):
# assert ext_type is unique
# assert cls has pack() and unpack()
MessagePackable.register(cls)
ext_classes[ext_type] = cls
ext_classes[cls] = ext_type
return cls
return wrapper |
|
This doesn't work perfectly on the type-hinting front, but it would at least take care of primitive and custom types, and I think this even works on python 2, to some extent. If not, it would be easy to simply not export it in python 2. The only errors this should give are some false positives on type hinting, since mypy doesn't support cyclic definitions yet. |
|
Okay, after digging into it more, the approach I posted will fail for type hinting. I no longer think it's realistically achievable to get type hinting to work here. The fact that |
|
We also can't do something clever like give them a common base class as a dummy, as the following produces incorrect type errors on mypy class Shim:
pass
def modify_class_def(cls):
class ret(cls, Shim):
pass
return ret
@modify_class_def
class Test:
pass
def test(a: Shim):
pass
test(Test()) # error |
|
I've opted to go with the decorator approach to get a more user friendly API out there for now, and we can revisit the metaclass approach and type hinting down the line. |
This PR adds the ability to usefully inherit from Ext. It changes four things:
In python3, you can have the pretty NamedTuple version by inheriting from an initially defined class. In python2, you can inherit from a function call to namedtuple(). In both cases, all that needs to happen is for the class to define a
type/codevariable, to define adatavariable/property, and to define an_unpackbmethod for umsgpack to hook onto.This PR enables the following in Python 3
And the following in Python 2 or 3:
This also enables for more flexible serialization through the use of dynamically calculated data: