Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
daae380
Convert inited values to corresponding Enum members
Gobot1234 Jul 11, 2020
6ad7f62
Revert enum-value changes
Gobot1234 Jul 11, 2020
d72189b
Enum speedups and bug fixes
Gobot1234 Jul 11, 2020
5fdc6e9
Casing hasn't been initialized yet
Gobot1234 Jul 11, 2020
5fc5f2e
Rename enums.py -> enum.py
Gobot1234 Jul 12, 2020
b6d9b35
Use Enum.__getitem__ instead of from_string
Gobot1234 Jul 12, 2020
1a8988f
Type hinting things
Gobot1234 Jul 12, 2020
533ab26
Blacken
Gobot1234 Jul 12, 2020
d9ac28f
Improve Enum coverage to be more inline with stdlib
Gobot1234 Jul 12, 2020
d091156
Remove doc string
Gobot1234 Jul 12, 2020
a1833a6
Fix Casing and rebase
Gobot1234 Jul 12, 2020
ce4e854
Fix recursion errors
Gobot1234 Jul 12, 2020
09d7bff
Return the EnumMember if it is used for __call__
Gobot1234 Jul 12, 2020
83e47b1
Remove unnecessary function
Gobot1234 Jul 12, 2020
907c91b
Delete enums.py
Gobot1234 Jul 12, 2020
cfbd632
Remove debugging Enum
Gobot1234 Jul 12, 2020
26b0d71
Add test_enum.py
Gobot1234 Jul 12, 2020
e4205bb
Blacken test
Gobot1234 Jul 12, 2020
4ee7641
Fix name space error
Gobot1234 Jul 12, 2020
966ac29
Fix failed tests
Gobot1234 Jul 12, 2020
0f631d7
Blacken code
Gobot1234 Jul 12, 2020
9eb4a80
Fix more tests
Gobot1234 Jul 12, 2020
dab320d
Fix more tests
Gobot1234 Jul 12, 2020
8c0d08c
Fix more tests
Gobot1234 Jul 12, 2020
16977c8
Final fix?
Gobot1234 Jul 12, 2020
bafd884
Final final fix
Gobot1234 Jul 12, 2020
f9b9f7a
Improve __eq__ check
Gobot1234 Jul 12, 2020
0f0325f
Final __eq__ check fix
Gobot1234 Jul 12, 2020
a4c94fb
Checks are done?
Gobot1234 Jul 12, 2020
3d98db1
Improve IntEnumMember.__eq__
Gobot1234 Jul 12, 2020
94e14c4
Revert "Improve IntEnumMember.__eq__", it's pointless
Gobot1234 Jul 12, 2020
d70b805
Fix test
Gobot1234 Jul 12, 2020
848f6b2
Slight fixes
Gobot1234 Jul 13, 2020
8021d21
Blacken code
Gobot1234 Jul 13, 2020
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
25 changes: 7 additions & 18 deletions src/betterproto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import dataclasses
import enum
import inspect
import json
import struct
Expand All @@ -24,6 +23,7 @@

import typing

from .enum import IntEnum as Enum, Enum as _Enum
from ._types import T
from .casing import camel_case, safe_snake_case, snake_case
from .grpc.grpclib_client import ServiceStub
Expand Down Expand Up @@ -119,7 +119,7 @@ def datetime_default_gen():
DATETIME_ZERO = datetime_default_gen()


class Casing(enum.Enum):
class Casing(_Enum):
"""Casing constants for serialization."""

CAMEL = camel_case
Expand Down Expand Up @@ -254,18 +254,6 @@ def map_field(
)


class Enum(enum.IntEnum):
"""Protocol buffers enumeration base class. Acts like `enum.IntEnum`."""

@classmethod
def from_string(cls, name: str) -> int:
"""Return the value which corresponds to the string name."""
try:
return cls.__members__[name]
except KeyError as e:
raise ValueError(f"Unknown value {name} for enum {cls.__name__}") from e


def _pack_fmt(proto_type: str) -> str:
"""Returns a little-endian format string for reading/writing binary."""
return {
Expand Down Expand Up @@ -783,7 +771,7 @@ def FromString(cls: Type[T], data: bytes) -> T:
return cls().parse(data)

def to_dict(
self, casing: Casing = Casing.CAMEL, include_default_values: bool = False
self, casing: Optional[Casing] = None, include_default_values: bool = False
) -> Dict[str, Any]:
"""
Returns a dict representation of this message instance which can be
Expand All @@ -795,6 +783,7 @@ def to_dict(
not be in returned dict if `include_default_values` is set to
`False`.
"""
casing = casing or Casing.CAMEL
output: Dict[str, Any] = {}
field_types = self._type_hints()
for field_name, meta in self._betterproto.meta_by_field_name.items():
Expand Down Expand Up @@ -909,9 +898,9 @@ def from_dict(self: T, value: dict) -> T:
elif meta.proto_type == TYPE_ENUM:
enum_cls = self._betterproto.cls_by_field[field_name]
if isinstance(v, list):
v = [enum_cls.from_string(e) for e in v]
v = [enum_cls[e] for e in v]
elif isinstance(v, str):
v = enum_cls.from_string(v)
v = enum_cls[v]

if v is not None:
setattr(self, field_name, v)
Expand Down Expand Up @@ -1007,7 +996,7 @@ class _WrappedMessage(Message):

value: Any

def to_dict(self, casing: Casing = Casing.CAMEL) -> Any:
def to_dict(self, casing: Optional[Casing] = None) -> Any:
return self.value

def from_dict(self: T, value: Any) -> T:
Expand Down
201 changes: 201 additions & 0 deletions src/betterproto/enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
from enum import EnumMeta as _EnumMeta, _is_dunder, _is_descriptor
from types import MappingProxyType
from typing import Any, Dict, Iterable, List, Mapping, NoReturn, Tuple

from .casing import camel_case, snake_case


class EnumMember:
_enum_cls_: "Enum"
name: str
value: Any

def __new__(cls, **kwargs: Dict[str, Any]) -> "EnumMember":
self = super().__new__(cls)
try:
self.name = kwargs["name"]
self.value = kwargs["value"]
except KeyError:
pass
finally:
return self

def __repr__(self):
return f"<{self._enum_cls_.__name__}.{self.name}: {self.value!r}>"

def __str__(self):
return f"{self._enum_cls_.__name__}.{self.name}"

@classmethod
def __call__(cls, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> Any:
try:
kwargs["_is_enum__call__"]
except KeyError:
return cls.value(*args, **kwargs)
else:
return cls.__new__(cls, name=kwargs["name"], value=kwargs["value"])

def __hash__(self):
return hash((self.name, self.value))

def __eq__(self, other: Any):
try:
if other._enum_cls_ is self._enum_cls_:
return self.value == other.value
return False
except AttributeError:
return NotImplemented


class IntEnumMember(int, EnumMember):
_enum_cls_: "IntEnum"
value: int

def __new__(cls, **kwargs: Dict[str, Any]) -> "IntEnumMember":
try:
value = kwargs["value"]
self = super().__new__(cls, value)
self.name = kwargs["name"]
self.value = value
return self
except KeyError:
return super().__new__(cls)


class EnumMeta(type):
_enum_value_map_: Dict[Any, EnumMember]
_enum_member_map_: Dict[str, EnumMember]
_enum_member_names_: List[str]

def __new__(
mcs, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]
) -> "EnumMeta":
value_mapping: Dict[Any, EnumMember] = {}
member_mapping: Dict[str, EnumMember] = {}
member_names: List[str] = []
try:
value_cls = (
IntEnumMember(name=name) if IntEnum in bases else EnumMember(name=name)
)
except NameError:
value_cls = EnumMember(name=name)

for key, value in tuple(attrs.items()):
is_descriptor = _is_descriptor(value)
if key[0] == "_" and not is_descriptor:
continue

if is_descriptor:
if value not in (camel_case, snake_case):
setattr(value_cls, key, value)
del attrs[key]
continue

try:
new_value = value_mapping[value]
except KeyError:
new_value = value_cls(name=key, value=value, _is_enum__call__=True)
value_mapping[value] = new_value
member_names.append(key)

member_mapping[key] = new_value
attrs[key] = new_value

attrs["_enum_value_map_"] = value_mapping
attrs["_enum_member_map_"] = member_mapping
attrs["_enum_member_names_"] = member_names
enum_class: "EnumMeta" = super().__new__(mcs, name, bases, attrs)
for member in member_mapping.values():
member._enum_cls_ = enum_class
return enum_class

def __call__(cls, value: Any) -> "EnumMember":
if isinstance(value, cls):
return value
try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
raise ValueError(f"{value!r} is not a valid {cls.__name__}")

def __repr__(cls):
return f"<enum {cls.__name__!r}>"

def __iter__(cls) -> Iterable["EnumMember"]:
return (cls._enum_member_map_[name] for name in cls._enum_member_names_)

def __reversed__(cls) -> Iterable["EnumMember"]:
return (
cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_)
)

def __len__(cls):
return len(cls._enum_member_names_)

def __getitem__(cls, key: Any) -> "EnumMember":
return cls._enum_member_map_[key]

def __getattr__(cls, name: str):
if _is_dunder(name):
raise AttributeError(name)
try:
return cls._enum_value_map_[name]
except KeyError:
raise AttributeError(name) from None

def __setattr__(cls, name: str, value: Any) -> NoReturn:
if name in cls._enum_member_map_:
raise AttributeError("Cannot reassign members.")
super().__setattr__(name, value)

def __delattr__(cls, attr: Any) -> NoReturn:
if attr in cls._enum_member_map_:
raise AttributeError(f"{cls.__name__}: cannot delete Enum member.")
super().__delattr__(attr)

def __instancecheck__(self, instance: Any):
try:
cls = instance._enum_cls_
return cls is self or issubclass(cls, self)
except AttributeError:
return False

def __dir__(cls):
return [
"__class__",
"__doc__",
"__members__",
"__module__",
] + cls._enum_member_names_

def __contains__(cls, member: "EnumMember"):
if not isinstance(member, EnumMember):
raise TypeError(
"unsupported operand type(s) for 'in':"
f" '{member.__class__.__qualname__}' and '{cls.__class__.__qualname__}'"
)
return member.name in cls._enum_member_map_

def __bool__(self):
return True

@property
def __members__(cls) -> Mapping[str, "EnumMember"]:
return MappingProxyType(cls._enum_member_map_)


class Enum(metaclass=EnumMeta):
"""Protocol buffers enumeration base base class. Acts like `enum.Enum`."""


class IntEnum(int, Enum):
"""Protocol buffers enumeration base class. Acts like `enum.IntEnum`."""


def patched_instance_check(self: _EnumMeta, instance: Any) -> bool:
if isinstance(instance, (EnumMeta, EnumMember)):
return True

return type.__instancecheck__(self, instance)


_EnumMeta.__instancecheck__ = patched_instance_check # fake it till you make it
Loading