Coverage for apio/common/proto_util.py: 89%
70 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-07 05:08 +0000
1# This file is part of the Apio project
2# (C) 2016-present FPGAwars
3# Project authors:
4# * Jesús Arroyo (Jesus89) (2016-2019)
5# * Juan Gonzalez (obijuan) (2019-2024)
6# * The Apio Team (2024-present)
7# License: GPLv2
9"""Utilities related to the Apio Protocol Buffers objects."""
11import re
12from typing import Any, Dict, TypeVar
13from google.protobuf.json_format import ParseDict
14from google.protobuf.message import Message
15from google.protobuf.unknown_fields import UnknownFieldSet
16from google.protobuf.json_format import MessageToDict
17from apio.common.apio_console import fatal_error, cerror
19# Placeholder for a concrete protobuf message *class* (e.g. MyProto),
20# not an instance. bound=Message restricts it to subclasses of
21# google.protobuf.message.Message. A TypeVar (vs plain type[Message])
22# lets the type checker keep the specific class: pass MyProto, get MyProto.
23MessageClass = TypeVar("MessageClass", bound=Message)
26def is_initialized(proto_msg: Message, use_json_naming: bool = False) -> bool:
27 """Test if the protocol buffer message is fully initializes. If not,
28 print an error message and return False, otherwise return True.
29 """
31 assert isinstance(proto_msg, Message), type(proto_msg)
33 # -- Check 1: All required fields should present.
34 if not proto_msg.IsInitialized():
35 # -- Report the first missing required field.
36 # missing_field: str = proto_msg.FindInitializationErrors()[0]
37 find = getattr(proto_msg, "FindInitializationErrors", None)
38 assert callable(find)
39 missing_field = str(find()[0])
40 if use_json_naming: 40 ↛ 42line 40 didn't jump to line 42 because the condition on line 40 was always true
41 missing_field = missing_field.replace("_", "-")
42 cerror(f"Missing required field '{missing_field}'")
43 return False
45 # -- Check 2: Should not carry unknown fields.
46 unknown_fields = list(UnknownFieldSet(proto_msg))
47 if len(unknown_fields) > 0: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 cerror(f'Unknown fields: {", ".join(unknown_fields)}')
49 return False
51 # -- All is good.
52 return True
55def check_is_initialized(
56 proto_msg: Message, error_msg: str, *, use_json_naming: bool = False
57) -> None:
58 """Check that a proto message is fully populated"""
59 if not is_initialized(proto_msg, use_json_naming=use_json_naming): 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true
60 fatal_error(error_msg)
63def check_is_required(proto_msg: Message, *fields_names: str) -> None:
64 """Check that all the names are of required fields of the proto
65 object proto_msg. Names may be nested, e.g. "field1.field2".
66 """
67 assert isinstance(proto_msg, Message), type(proto_msg)
69 for name in fields_names:
70 descriptor = proto_msg.DESCRIPTOR
71 for segment in name.split("."):
72 field = descriptor.fields_by_name.get(segment)
73 if field is None:
74 fatal_error(
75 f"Field '{name}' is not a field of "
76 "protocol buffer message "
77 f"'{proto_msg.DESCRIPTOR.full_name}'"
78 )
79 if not field.is_required:
80 fatal_error(
81 f"Field '{name}' of '{proto_msg.DESCRIPTOR.full_name}' "
82 f"is not required"
83 )
84 descriptor = field.message_type
87def check_not_required(proto_msg: Message, *fields_names: str) -> None:
88 """Check that none of the names is a not fully required path of proto_msg.
89 Names may be nested, e.g. "field1.field2". A path is not required if
90 any segment along it is not required. So if "b" is optional, "a.b.c"
91 is not required. Repeated fields are not required since they can have
92 zero members.
93 """
94 assert isinstance(proto_msg, Message), type(proto_msg)
96 for name in fields_names:
97 descriptor = proto_msg.DESCRIPTOR
98 all_required = True
99 for segment in name.split("."):
100 if descriptor is None: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 fatal_error(
102 f"Field '{name}' is not a field of "
103 "protocol buffer message "
104 f"'{proto_msg.DESCRIPTOR.full_name}'"
105 )
106 field = descriptor.fields_by_name.get(segment)
107 if field is None: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 fatal_error(
109 f"Field '{name}' is not a field of "
110 "protocol buffer message "
111 f"'{proto_msg.DESCRIPTOR.full_name}'"
112 )
113 if not field.is_required:
114 all_required = False
115 descriptor = field.message_type
116 if all_required:
117 fatal_error(
118 f"Field '{name}' of '{proto_msg.DESCRIPTOR.full_name}' "
119 f"is required"
120 )
123def proto_from_json_dict(
124 json_dict: Dict[str, Any],
125 proto_class: type[MessageClass],
126 *,
127 use_json_naming: bool = True,
128) -> MessageClass | None:
129 """Create and return an object of proto message class 'proto_class'
130 populated with values from json dict json_dict. If error, print an error
131 message and return None.
132 """
133 # pylint: disable=broad-exception-caught
135 try:
136 proto_msg = ParseDict(json_dict, proto_class())
138 except Exception as e:
139 parsing_error = str(e)
141 # -- Try to improve the error message.
142 pattern = re.compile(r'has no field named "([^"]+)" at')
143 match = pattern.search(parsing_error)
144 if match: 144 ↛ 148line 144 didn't jump to line 148 because the condition on line 144 was always true
145 parsing_error = f"Unknown field '{match.group(1)}'"
147 # -- Print an error message and exit.
148 cerror(parsing_error)
149 return None
151 if not is_initialized(proto_msg, use_json_naming=use_json_naming):
152 return None
154 return proto_msg
157def proto_to_json_dict(proto_msg: Message) -> Dict[str, Any]:
158 """Given a proto object, convert it to a json dict."""
159 assert isinstance(proto_msg, Message), type(proto_msg)
160 return MessageToDict(proto_msg)