Skip to content

Commit fe2b856

Browse files
Support Pydantic validators (#101)
* Remove comment * Add validators * Test validators * Remove validation functions for numeric types * Test validation * Split test function in several functions
1 parent 385d5d4 commit fe2b856

File tree

4 files changed

+207
-2
lines changed

4 files changed

+207
-2
lines changed

src/betterproto2/__init__.py

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

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

55
import dataclasses
66
import enum as builtin_enum
@@ -20,6 +20,7 @@
2020

2121
from typing_extensions import Self
2222

23+
import betterproto2.validators as validators
2324
from betterproto2.message_pool import MessagePool
2425
from betterproto2.utils import unwrap
2526

@@ -1068,7 +1069,6 @@ def to_dict(
10681069

10691070
@classmethod
10701071
def _from_dict_init(cls, mapping: Mapping[str, Any] | Any) -> Mapping[str, Any]:
1071-
# TODO restructure using other function
10721072
init_kwargs: dict[str, Any] = {}
10731073
for key, value in mapping.items():
10741074
field_name = safe_snake_case(key)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .proto_types import validate_float32, validate_string
2+
3+
__all__ = ["validate_float32", "validate_string"]
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Pydantic validators for Protocol Buffer standard types.
3+
4+
This module provides validator functions that can be used with Pydantic
5+
to validate Protocol Buffer standard types (int32, int64, sfixed32, etc.)
6+
to ensure they conform to their respective constraints.
7+
8+
These validators are designed to be used as "after validators", meaning the value
9+
will already be of the correct type and only bounds checking is needed.
10+
"""
11+
12+
import struct
13+
14+
15+
def validate_float32(v: float) -> float:
16+
try:
17+
packed = struct.pack("!f", v)
18+
struct.unpack("!f", packed)
19+
except (struct.error, OverflowError):
20+
raise ValueError(f"Value cannot be encoded as a float: {v}")
21+
22+
return v
23+
24+
25+
def validate_string(v: str) -> str:
26+
try:
27+
v.encode("utf-8").decode("utf-8")
28+
except UnicodeError:
29+
raise ValueError("String contains invalid UTF-8 characters")
30+
return v

tests/test_validation.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import pydantic
2+
import pytest
3+
4+
5+
def test_int32_validation():
6+
from .output_betterproto_pydantic.validation import Message
7+
8+
# Test valid values
9+
Message(int32_value=1)
10+
Message(int32_value=-(2**31))
11+
Message(int32_value=(2**31 - 1))
12+
13+
# Test invalid values
14+
with pytest.raises(pydantic.ValidationError):
15+
Message(int32_value=2**31)
16+
with pytest.raises(pydantic.ValidationError):
17+
Message(int32_value=-(2**31) - 1)
18+
19+
20+
def test_int64_validation():
21+
from .output_betterproto_pydantic.validation import Message
22+
23+
# Test valid values
24+
Message(int64_value=1)
25+
Message(int64_value=-(2**63))
26+
Message(int64_value=(2**63 - 1))
27+
28+
# Test invalid values
29+
with pytest.raises(pydantic.ValidationError):
30+
Message(int64_value=2**63)
31+
with pytest.raises(pydantic.ValidationError):
32+
Message(int64_value=-(2**63) - 1)
33+
34+
35+
def test_uint32_validation():
36+
from .output_betterproto_pydantic.validation import Message
37+
38+
# Test valid values
39+
Message(uint32_value=0)
40+
Message(uint32_value=2**32 - 1)
41+
42+
# Test invalid values
43+
with pytest.raises(pydantic.ValidationError):
44+
Message(uint32_value=-1)
45+
with pytest.raises(pydantic.ValidationError):
46+
Message(uint32_value=2**32)
47+
48+
49+
def test_uint64_validation():
50+
from .output_betterproto_pydantic.validation import Message
51+
52+
# Test valid values
53+
Message(uint64_value=0)
54+
Message(uint64_value=2**64 - 1)
55+
56+
# Test invalid values
57+
with pytest.raises(pydantic.ValidationError):
58+
Message(uint64_value=-1)
59+
with pytest.raises(pydantic.ValidationError):
60+
Message(uint64_value=2**64)
61+
62+
63+
def test_sint32_validation():
64+
from .output_betterproto_pydantic.validation import Message
65+
66+
# Test valid values
67+
Message(sint32_value=1)
68+
Message(sint32_value=-(2**31))
69+
Message(sint32_value=(2**31 - 1))
70+
71+
# Test invalid values
72+
with pytest.raises(pydantic.ValidationError):
73+
Message(sint32_value=2**31)
74+
with pytest.raises(pydantic.ValidationError):
75+
Message(sint32_value=-(2**31) - 1)
76+
77+
78+
def test_sint64_validation():
79+
from .output_betterproto_pydantic.validation import Message
80+
81+
# Test valid values
82+
Message(sint64_value=1)
83+
Message(sint64_value=-(2**63))
84+
Message(sint64_value=(2**63 - 1))
85+
86+
# Test invalid values
87+
with pytest.raises(pydantic.ValidationError):
88+
Message(sint64_value=2**63)
89+
with pytest.raises(pydantic.ValidationError):
90+
Message(sint64_value=-(2**63) - 1)
91+
92+
93+
def test_fixed32_validation():
94+
from .output_betterproto_pydantic.validation import Message
95+
96+
# Test valid values
97+
Message(fixed32_value=0)
98+
Message(fixed32_value=2**32 - 1)
99+
100+
# Test invalid values
101+
with pytest.raises(pydantic.ValidationError):
102+
Message(fixed32_value=-1)
103+
with pytest.raises(pydantic.ValidationError):
104+
Message(fixed32_value=2**32)
105+
106+
107+
def test_fixed64_validation():
108+
from .output_betterproto_pydantic.validation import Message
109+
110+
# Test valid values
111+
Message(fixed64_value=0)
112+
Message(fixed64_value=2**64 - 1)
113+
114+
# Test invalid values
115+
with pytest.raises(pydantic.ValidationError):
116+
Message(fixed64_value=-1)
117+
with pytest.raises(pydantic.ValidationError):
118+
Message(fixed64_value=2**64)
119+
120+
121+
def test_sfixed32_validation():
122+
from .output_betterproto_pydantic.validation import Message
123+
124+
# Test valid values
125+
Message(sfixed32_value=1)
126+
Message(sfixed32_value=-(2**31))
127+
Message(sfixed32_value=(2**31 - 1))
128+
129+
# Test invalid values
130+
with pytest.raises(pydantic.ValidationError):
131+
Message(sfixed32_value=2**31)
132+
with pytest.raises(pydantic.ValidationError):
133+
Message(sfixed32_value=-(2**31) - 1)
134+
135+
136+
def test_sfixed64_validation():
137+
from .output_betterproto_pydantic.validation import Message
138+
139+
# Test valid values
140+
Message(sfixed64_value=1)
141+
Message(sfixed64_value=-(2**63))
142+
Message(sfixed64_value=(2**63 - 1))
143+
144+
# Test invalid values
145+
with pytest.raises(pydantic.ValidationError):
146+
Message(sfixed64_value=2**63)
147+
with pytest.raises(pydantic.ValidationError):
148+
Message(sfixed64_value=-(2**63) - 1)
149+
150+
151+
def test_float_validation():
152+
from .output_betterproto_pydantic.validation import Message
153+
154+
# Test valid values
155+
Message(float_value=0.0)
156+
Message(float_value=3.14)
157+
158+
# Test invalid values
159+
with pytest.raises(pydantic.ValidationError):
160+
Message(float_value=3.5e38)
161+
162+
163+
def test_string_validation():
164+
from .output_betterproto_pydantic.validation import Message
165+
166+
# Test valid values
167+
Message(string_value="")
168+
Message(string_value="Hello World")
169+
170+
# Test invalid values
171+
with pytest.raises(pydantic.ValidationError):
172+
Message(string_value="Hello \udc00 World")

0 commit comments

Comments
 (0)