-
-
Save e2thenegpii/19dd5df64227e27639dac16828098e92 to your computer and use it in GitHub Desktop.
| """ | |
| I often find myself parsing buffers that contain typed buffers of data. | |
| Python enumerations are great. I love to use them to map symantic names | |
| onto a collection of values. However they often are lacking in performing | |
| actions based on the enumerant. Typically I have to do something like | |
| the following | |
| class foo(enum.Enum): | |
| a = 1 | |
| b = 2 | |
| c = 3 | |
| def do_foo(self): | |
| if self is foo.a: | |
| pass # do a foo | |
| elif self is foo.b: | |
| pass # do b foo | |
| elif self is foo.c: | |
| pass # do c foo | |
| else: | |
| raise ValueError() # how did we even get here? | |
| The foo.do_foo function is essentially a dispatch method typically calling | |
| constructors of various types. | |
| Python 3.7 added support for singledispatch methods which are fantastic | |
| for implementing a function like foo.do_foo, however out of the box they | |
| don't work with enums. This is because type(foo.a) == foo for standard | |
| python enums. | |
| The code below is my kluge/elegant attempt to make python enums work with | |
| the singledispatch decorator. | |
| Python enumerations allow us to define a custom type for the enumerants | |
| by inheriting a class that implements the __new__ method. This is the case | |
| for enum.IntEnum. It's definition is basicaly the following: | |
| class IntEnum(int, Enum): | |
| pass | |
| In order to support singledispatch each enumerant must have a unique type. | |
| Taking a page from IntEnum I created a class UniqueEnumType that dynamically | |
| generates a type in its __new__ method based on the enumerant value and | |
| class name so the type of each enumerant is unique. | |
| Most of the implementation of UniqueEnumType as found below is related to | |
| making our enumerants look and feel like standard python enumerants except | |
| for the fact that foo.enumerant.__class__ is unique to the enumerant. | |
| We use a python metaclass in UniqueEnumType so that the types will | |
| compare and hash equal. | |
| """ | |
| import enum | |
| from types import DynamicClassAttribute | |
| from functools import singledispatch | |
| class UniqueEnumMeta(type): | |
| """ | |
| Metaclass that makes all produced classes compare equal and hash equal | |
| if their respective names match. | |
| """ | |
| def __eq__(self, other): | |
| return self.__name__ == other.__name__ | |
| def __hash__(self): | |
| return hash(self.__name__) | |
| class UniqueEnumType: | |
| @classmethod | |
| def create_type(cls, name, value, **kwargs): | |
| """ | |
| Produce a new type whose name is a concatenation of the name and value | |
| Unfortunately because we're using metaclasses pickling these enumerants | |
| becomes impossible. | |
| """ | |
| cls_name = "%s_%r" % (name, value) | |
| bases = (UniqueEnumType,) | |
| attributes = { | |
| **kwargs, | |
| "_orig_class_name": name, | |
| "__module__": None, # makes printing nicer | |
| } | |
| return UniqueEnumMeta(cls_name, bases, attributes) | |
| def __new__(cls, *args, **kwargs): | |
| """ | |
| Dynamically create an enumerant specific class | |
| """ | |
| if not issubclass(cls, enum.Enum): | |
| cls._value_ = args[0] | |
| return object.__new__(cls) | |
| custom_type = UniqueEnumType.create_type(cls.__name__, args[0], **cls.__dict__) | |
| return custom_type.__new__(custom_type, *args, **kwargs) | |
| @DynamicClassAttribute | |
| def name(self): | |
| """ | |
| Give ourselves a name attribute like normal python enumerants | |
| """ | |
| return self._name_ | |
| @DynamicClassAttribute | |
| def value(self): | |
| """ | |
| Give ourselves a value attribute like normal python enumerants | |
| """ | |
| return self._value_ | |
| def __repr__(self): | |
| """ | |
| Make our repr just like a normal python enumerant | |
| """ | |
| return "<%s.%s: %r>" % (self._orig_class_name, self.name, self.value) | |
| class foo(UniqueEnumType, enum.Enum): | |
| """ | |
| Example enumeration | |
| Notice type(foo.a) == <class 'foo_1'> instead of <class 'foo'> | |
| """ | |
| a = 1 | |
| b = 2 | |
| c = 3 | |
| @classmethod | |
| def parse(cls, view): | |
| """ | |
| Normal enum class method parses a new type and calls the registerd | |
| enumerant handler | |
| """ | |
| return cls(view[0]).parse_type(view[1:]) | |
| @singledispatch | |
| def parse_type(self, view): | |
| """ | |
| Default value handler will be called if no enumerant handler is registered | |
| """ | |
| raise NotImplementedError() | |
| @parse_type.register(UniqueEnumType.create_type("foo", 2)) | |
| def _(self, view): | |
| """ | |
| Parser function for parsing foo.b | |
| The syntax for registering a enum method is kind of a kludge. | |
| We essentially create an enum that hashes the same type | |
| that will be produced for the enumerant. | |
| Unfortunately calling @parse_type.register(b) wouldn't work | |
| because at this point b == int(2) and hasn't been mangled | |
| by the enum.EnumMeta metaclass. | |
| """ | |
| print("Parsing type related to b") | |
| return two(), view | |
| @parse_type.register(UniqueEnumType.create_type("foo", 3)) | |
| def _(self, view): | |
| """ | |
| Same as above different type | |
| """ | |
| print("parsing type related to c") | |
| return three(), view | |
| class one: | |
| @foo.parse_type.register(type(foo.a)) | |
| def _(enumerant, view): | |
| """ | |
| Parsing methods don't have to be members of the enumeration class | |
| Notice the registration syntax changes a little bit. | |
| The registration syntax for in enum functions is a little cleaner | |
| type(foo.a) returns the type specific to foo.a | |
| The parameters are a little querky in that the first parameter is the | |
| enumerant that is responsable for calling the dispatcher | |
| """ | |
| print(enumerant) | |
| print(view) | |
| print("Parsing type related to a") | |
| return one(), view | |
| class two: | |
| pass | |
| class three: | |
| pass | |
| def main(): | |
| view = memoryview(b"\x01\x02\x03") | |
| while view: | |
| val, view = foo.parse(view) | |
| print(val) | |
| if __name__ == "__main__": | |
| main() |
Hi, nice use of the singledispatch decorator. Someone at StackOverflow shared your gist in a comment to a question that's just about identical to the problem you're trying to solve here, and I thought you'd be interested in checking out my solution using a decorator class instead: https://stackoverflow.com/a/73105260/6890912
When I wrote this I mostly wanted to play around with the singledispatch decorator, and I feel like a custom __new__ method on enums is a vastly underused feature.
I definitely like that your solution doesn't involve metaclasses and dynamic typing, but it doesn't register a default (easily rectified by using a defaultdict in the bind decorator though adding complexity). Also when you forget to register a callable for a type you'll get a key error which I feel like is a little bit confusing as I'd expect a NotImplemented exception would be better (also easily fixed).
Licensed CC BY-SA
https://creativecommons.org/licenses/by-sa/4.0/