Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit d045cbf

Browse files
authored
fix: accessing an unset struct_pb2.Value field does not raise (#140)
class Foo(proto.Message): value = proto.Field(struct_pb2.Value, number=1) f = Foo() assert f.value is None # This should not raise an exception. assert "value" not in f # The attribute has _not_ been set. f.value = None assert f.value is None assert "value" in f # The attribute _has_ been set. None of the above should raise an exception.
1 parent 6888d71 commit d045cbf

3 files changed

Lines changed: 22 additions & 9 deletions

File tree

proto/marshal/rules/struct.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,15 @@ def __init__(self, *, marshal):
2929
def to_python(self, value, *, absent: bool = None):
3030
"""Coerce the given value to the appropriate Python type.
3131
32-
Note that setting ``null_value`` is distinct from not setting
33-
a value, and the absent value will raise an exception.
32+
Note that both NullValue and absent fields return None.
33+
In order to disambiguate between these two options,
34+
use containment check,
35+
E.g.
36+
"value" in foo
37+
which is True for NullValue and False for an absent value.
3438
"""
3539
kind = value.WhichOneof("kind")
36-
if kind == "null_value":
40+
if kind == "null_value" or absent:
3741
return None
3842
if kind == "bool_value":
3943
return bool(value.bool_value)
@@ -49,7 +53,9 @@ def to_python(self, value, *, absent: bool = None):
4953
return self._marshal.to_python(
5054
struct_pb2.ListValue, value.list_value, absent=False,
5155
)
52-
raise AttributeError
56+
# If more variants are ever added, we want to fail loudly
57+
# instead of tacitly returning None.
58+
raise ValueError("Unexpected kind: %s" % kind) # pragma: NO COVER
5359

5460
def to_proto(self, value) -> struct_pb2.Value:
5561
"""Return a protobuf Value object representing this value."""

tests/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import imp
15+
import importlib
1616
from unittest import mock
1717

1818
from google.protobuf import descriptor_pool
@@ -76,11 +76,11 @@ def pytest_runtest_setup(item):
7676
# If the marshal had previously registered the old message classes,
7777
# then reload the appropriate modules so the marshal is using the new ones.
7878
if "wrappers_pb2" in reloaded:
79-
imp.reload(rules.wrappers)
79+
importlib.reload(rules.wrappers)
8080
if "struct_pb2" in reloaded:
81-
imp.reload(rules.struct)
81+
importlib.reload(rules.struct)
8282
if reloaded.intersection({"timestamp_pb2", "duration_pb2"}):
83-
imp.reload(rules.dates)
83+
importlib.reload(rules.dates)
8484

8585

8686
def pytest_runtest_teardown(item):

tests/test_marshal_types_struct.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class Foo(proto.Message):
3030
assert Foo(value=True).value is True
3131

3232

33+
def test_value_absent():
34+
class Foo(proto.Message):
35+
value = proto.Field(struct_pb2.Value, number=1)
36+
37+
assert Foo().value is None
38+
39+
3340
def test_value_primitives_rmw():
3441
class Foo(proto.Message):
3542
value = proto.Field(struct_pb2.Value, number=1)
@@ -158,7 +165,7 @@ class Foo(proto.Message):
158165
value = proto.Field(struct_pb2.Value, number=1)
159166

160167
foo = Foo()
161-
assert not hasattr(foo, "value")
168+
assert "value" not in foo
162169

163170

164171
def test_list_value_read():

0 commit comments

Comments
 (0)