Skip to content

Commit 2888361

Browse files
Greatly simplify enums implementation (#72)
* Greatly simplify enums implementation * Remove try_value * Fix documentation * Update version
1 parent 654423e commit 2888361

File tree

5 files changed

+51
-181
lines changed

5 files changed

+51
-181
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "betterproto2"
3-
version = "0.2.3"
3+
version = "0.3.0"
44
description = "A better Protobuf / gRPC generator & library"
55
authors = [
66
{name = "Adrien Vannson", email = "[email protected]"},

src/betterproto2/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ def _postprocess_single(self, wire_type: int, meta: FieldMetadata, field_name: s
823823
value = value > 0
824824
elif meta.proto_type == TYPE_ENUM:
825825
# Convert enum ints to python enum instances
826-
value = self._betterproto.cls_by_field[field_name].try_value(value)
826+
value = self._betterproto.cls_by_field[field_name](value)
827827
elif wire_type in (WIRE_FIXED_32, WIRE_FIXED_64):
828828
fmt = _pack_fmt(meta.proto_type)
829829
value = struct.unpack(fmt, value)[0]

src/betterproto2/enum.py

Lines changed: 30 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,177 +1,45 @@
1-
from __future__ import annotations
1+
from enum import IntEnum
22

3-
from enum import (
4-
EnumMeta,
5-
IntEnum,
6-
)
7-
from types import MappingProxyType
8-
from typing import (
9-
TYPE_CHECKING,
10-
Any,
11-
)
3+
from typing_extensions import Self
124

13-
if TYPE_CHECKING:
14-
from collections.abc import (
15-
Generator,
16-
Mapping,
17-
)
18-
19-
from typing_extensions import (
20-
Never,
21-
Self,
22-
)
23-
24-
25-
def _is_descriptor(obj: object) -> bool:
26-
return hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__")
27-
28-
29-
class EnumType(EnumMeta if TYPE_CHECKING else type):
30-
_value_map_: Mapping[int, Enum]
31-
_member_map_: Mapping[str, Enum]
32-
33-
def __new__(mcs, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> Self:
34-
value_map = {}
35-
member_map = {}
36-
37-
new_mcs = type(
38-
f"{name}Type",
39-
tuple(
40-
dict.fromkeys([base.__class__ for base in bases if base.__class__ is not type] + [EnumType, type])
41-
), # reorder the bases so EnumType and type are last to avoid conflicts
42-
{"_value_map_": value_map, "_member_map_": member_map},
43-
)
44-
45-
members = {
46-
name: value for name, value in namespace.items() if not _is_descriptor(value) and not name.startswith("__")
47-
}
48-
49-
cls = type.__new__(
50-
new_mcs,
51-
name,
52-
bases,
53-
{key: value for key, value in namespace.items() if key not in members},
54-
)
55-
# this allows us to disallow member access from other members as
56-
# members become proper class variables
57-
58-
for name, value in members.items():
59-
member = value_map.get(value)
60-
if member is None:
61-
member = cls.__new__(cls, name=name, value=value) # type: ignore
62-
value_map[value] = member
63-
member_map[name] = member
64-
type.__setattr__(new_mcs, name, member)
65-
66-
return cls
67-
68-
if not TYPE_CHECKING:
69-
70-
def __call__(cls, value: int) -> Enum:
71-
try:
72-
return cls._value_map_[value]
73-
except (KeyError, TypeError):
74-
raise ValueError(f"{value!r} is not a valid {cls.__name__}") from None
75-
76-
def __iter__(cls) -> Generator[Enum, None, None]:
77-
yield from cls._member_map_.values()
78-
79-
def __reversed__(cls) -> Generator[Enum, None, None]:
80-
yield from reversed(cls._member_map_.values())
81-
82-
def __getitem__(cls, key: str) -> Enum:
83-
return cls._member_map_[key]
84-
85-
@property
86-
def __members__(cls) -> MappingProxyType[str, Enum]:
87-
return MappingProxyType(cls._member_map_)
88-
89-
def __repr__(cls) -> str:
90-
return f"<enum {cls.__name__!r}>"
91-
92-
def __len__(cls) -> int:
93-
return len(cls._member_map_)
94-
95-
def __setattr__(cls, name: str, value: Any) -> Never:
96-
raise AttributeError(f"{cls.__name__}: cannot reassign Enum members.")
97-
98-
def __delattr__(cls, name: str) -> Never:
99-
raise AttributeError(f"{cls.__name__}: cannot delete Enum members.")
100-
101-
def __contains__(cls, member: object) -> bool:
102-
return isinstance(member, cls) and member.name in cls._member_map_
103-
104-
105-
class Enum(IntEnum if TYPE_CHECKING else int, metaclass=EnumType):
106-
"""
107-
The base class for protobuf enumerations, all generated enumerations will
108-
inherit from this. Emulates `enum.IntEnum`.
109-
"""
110-
111-
name: str | None
112-
value: int
113-
114-
if not TYPE_CHECKING:
115-
116-
def __new__(cls, *, name: str | None, value: int) -> Self:
117-
self = super().__new__(cls, value)
118-
super().__setattr__(self, "name", name)
119-
super().__setattr__(self, "value", value)
120-
return self
121-
122-
def __str__(self) -> str:
123-
return self.name or "None"
124-
125-
def __repr__(self) -> str:
126-
return f"{self.__class__.__name__}.{self.name}"
127-
128-
def __setattr__(self, key: str, value: Any) -> Never:
129-
raise AttributeError(f"{self.__class__.__name__} Cannot reassign a member's attributes.")
130-
131-
def __delattr__(self, item: Any) -> Never:
132-
raise AttributeError(f"{self.__class__.__name__} Cannot delete a member's attributes.")
133-
134-
def __copy__(self) -> Self:
135-
return self
136-
137-
def __deepcopy__(self, memo: Any) -> Self:
138-
return self
1395

6+
class Enum(IntEnum):
1407
@classmethod
141-
def try_value(cls, value: int = 0) -> Self:
142-
"""Return the value which corresponds to the value.
143-
144-
Parameters
145-
-----------
146-
value: :class:`int`
147-
The value of the enum member to get.
8+
def _missing_(cls, value):
9+
# If the given value is not an integer, let the standard enum implementation raise an error
10+
if not isinstance(value, int):
11+
return None
12+
13+
# Create a new "unknown" instance with the given value.
14+
obj = int.__new__(cls, value)
15+
obj._value_ = value
16+
obj._name_ = ""
17+
return obj
18+
19+
def __str__(self):
20+
if not self.name:
21+
return f"{self.__class__.__name__}.~UNKNOWN({self.value})"
22+
return f"{self.__class__.__name__}.{self.name}"
14823

149-
Returns
150-
-------
151-
:class:`Enum`
152-
The corresponding member or a new instance of the enum if
153-
``value`` isn't actually a member.
154-
"""
155-
try:
156-
return cls._value_map_[value]
157-
except (KeyError, TypeError):
158-
return cls.__new__(cls, name=None, value=value)
24+
def __repr__(self):
25+
if not self.name:
26+
return f"<{self.__class__.__name__}.~UNKNOWN: {self.value}>"
27+
return super().__repr__()
15928

16029
@classmethod
16130
def from_string(cls, name: str) -> Self:
16231
"""Return the value which corresponds to the string name.
16332
164-
Parameters
165-
-----------
166-
name: :class:`str`
167-
The name of the enum member to get.
33+
Parameters:
34+
name: The name of the enum member to get.
35+
36+
Raises:
37+
ValueError: The member was not found in the Enum.
16838
169-
Raises
170-
-------
171-
:exc:`ValueError`
172-
The member was not found in the Enum.
39+
Returns:
40+
The corresponding value
17341
"""
17442
try:
175-
return cls._member_map_[name]
43+
return cls[name]
17644
except KeyError as e:
17745
raise ValueError(f"Unknown value {name} for enum {cls.__name__}") from e

src/betterproto2/internal_lib/google/protobuf/__init__.py

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test_enum.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ class Colour(betterproto2.Enum):
99
BLUE = 3
1010

1111

12-
PURPLE = Colour.__new__(Colour, name=None, value=4)
12+
PURPLE = Colour(4)
1313

1414

1515
@pytest.mark.parametrize(
1616
"member, str_value",
1717
[
18-
(Colour.RED, "RED"),
19-
(Colour.GREEN, "GREEN"),
20-
(Colour.BLUE, "BLUE"),
18+
(Colour.RED, "Colour.RED"),
19+
(Colour.GREEN, "Colour.GREEN"),
20+
(Colour.BLUE, "Colour.BLUE"),
21+
(PURPLE, "Colour.~UNKNOWN(4)"),
2122
],
2223
)
2324
def test_str(member: Colour, str_value: str) -> None:
@@ -27,9 +28,10 @@ def test_str(member: Colour, str_value: str) -> None:
2728
@pytest.mark.parametrize(
2829
"member, repr_value",
2930
[
30-
(Colour.RED, "Colour.RED"),
31-
(Colour.GREEN, "Colour.GREEN"),
32-
(Colour.BLUE, "Colour.BLUE"),
31+
(Colour.RED, "<Colour.RED: 1>"),
32+
(Colour.GREEN, "<Colour.GREEN: 2>"),
33+
(Colour.BLUE, "<Colour.BLUE: 3>"),
34+
(PURPLE, "<Colour.~UNKNOWN: 4>"),
3335
],
3436
)
3537
def test_repr(member: Colour, repr_value: str) -> None:
@@ -42,7 +44,7 @@ def test_repr(member: Colour, repr_value: str) -> None:
4244
(Colour.RED, ("RED", 1)),
4345
(Colour.GREEN, ("GREEN", 2)),
4446
(Colour.BLUE, ("BLUE", 3)),
45-
(PURPLE, (None, 4)),
47+
(PURPLE, ("", 4)),
4648
],
4749
)
4850
def test_name_values(member: Colour, values: tuple[str | None, int]) -> None:
@@ -70,5 +72,5 @@ def test_from_string(member: Colour, input_str: str) -> None:
7072
(PURPLE, 4),
7173
],
7274
)
73-
def test_try_value(member: Colour, input_int: int) -> None:
74-
assert Colour.try_value(input_int) == member
75+
def test_construction(member: Colour, input_int: int) -> None:
76+
assert Colour(input_int) == member

0 commit comments

Comments
 (0)