Skip to content

Commit 316efac

Browse files
Add unwrap convenience function (#47)
* Add unwrap convenience function * Add test file * Remove unsupported Python syntax
1 parent 7e3e221 commit 316efac

File tree

5 files changed

+58
-2
lines changed

5 files changed

+58
-2
lines changed

docs/tutorial/messages.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,24 @@ On Python 3.10 and later, it is also possible to use a `match` statement to find
8686
>>> find(Message())
8787
'No field set'
8888
```
89+
90+
## Unwrapping optional values
91+
92+
In protobuf, fields are often marked as optional, either manually or because it is the default behavior of the protocol.
93+
If you care about type-checking, this can be tedious to handle as you need to make sure each field is not `None` before
94+
using, even when you know that the field will never be `None` is your application.
95+
96+
```python
97+
# typing error: item "None" of "Message | None" has no attribute "field"
98+
message.msg_field.field
99+
```
100+
101+
To simplify this, betterproto provides a convenience function: `unwrap`. This function takes an optional value, and
102+
returns the same value if it is not `None`. If the value is `None`, an error is raised.
103+
104+
```python
105+
from betterproto2 import unwrap
106+
107+
# no typing error!
108+
unwrap(message.msg_field).field
109+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "betterproto2"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
description = "A better Protobuf / gRPC generator & library"
55
authors = ["Adrien Vannson <[email protected]>", "Daniel G. Taylor <[email protected]>"]
66
readme = "README.md"

src/betterproto2/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
__all__ = ["__version__", "check_compiler_version"]
3+
__all__ = ["__version__", "check_compiler_version", "unwrap"]
44

55
import dataclasses
66
import enum as builtin_enum
@@ -43,6 +43,8 @@
4343
from dateutil.parser import isoparse
4444
from typing_extensions import Self
4545

46+
from betterproto2.utils import unwrap
47+
4648
from ._types import T
4749
from ._version import __version__, check_compiler_version
4850
from .casing import (

src/betterproto2/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,24 @@ def __init__(self, func: Callable[[TT_co], T_co]):
4949

5050
def __get__(self, instance: Any, type: TT_co) -> T_co:
5151
return self.__func__(type)
52+
53+
54+
T = TypeVar("T")
55+
56+
57+
def unwrap(x: T | None) -> T:
58+
"""
59+
Unwraps an optional value, returning the value if it exists, or raises a ValueError if the value is None.
60+
61+
Args:
62+
value (Optional[T]): The optional value to unwrap.
63+
64+
Returns:
65+
T: The unwrapped value if it exists.
66+
67+
Raises:
68+
ValueError: If the value is None.
69+
"""
70+
if x is None:
71+
raise ValueError("Can't unwrap a None value")
72+
return x

tests/test_unwrap.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import pytest
2+
3+
4+
def test_unwrap() -> None:
5+
from betterproto2 import unwrap
6+
from tests.output_betterproto.unwrap import Message, NestedMessage
7+
8+
with pytest.raises(ValueError):
9+
unwrap(Message().x)
10+
11+
msg = Message(x=NestedMessage())
12+
assert msg.x == unwrap(msg.x)

0 commit comments

Comments
 (0)