Skip to content

Commit a4508d4

Browse files
[numpy] Fix breakage in users of Python protobufs under NumPy 2.3rc1. (#22172)
* [numpy] Fix breakage in users of Python protobufs under NumPy 2.3rc1. As of NumPy 2.3.0rc1, numpy.bool scalars can no longer be interpreted as index values (https://github.com/numpy/numpy/releases/tag/v2.3.0rc1). This causes protobuf no longer to accept a np.bool scalar as a legal value for a boolean field. We have two options: a) either we can change protobuf so that it continues to accept NumPy boolean scalars (this change), or b) decide that protobuf should reject NumPy boolean scalars and that users must update their code to cast to a Python bool explicitly. I have no strong opinion as to which, but option (a) seems less disruptive. No test updates are needed: the existing tests fail under NumPy 2.3. PiperOrigin-RevId: 766629310 * Bump PHP to 8.3 to fix non-hermetic macos failure * Bump setup-php pin instead of PHP version --------- Co-authored-by: Peter Hawkins <phawkins@google.com>
1 parent ea5369e commit a4508d4

File tree

4 files changed

+56
-8
lines changed

4 files changed

+56
-8
lines changed

.github/workflows/test_php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ jobs:
166166
run: brew install coreutils gd
167167

168168
- name: Pin PHP version
169-
uses: shivammathur/setup-php@a4e22b60bbb9c1021113f2860347b0759f66fe5d # 2.30.0
169+
uses: shivammathur/setup-php@8872c784b04a1420e81191df5d64fbd59d3d3033 # 2.30.2
170170
with:
171171
php-version: ${{ matrix.version }}
172172

python/convert.c

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,35 @@ bool PyUpb_IsNumpyNdarray(PyObject* obj, const upb_FieldDef* f) {
226226
return is_ndarray;
227227
}
228228

229+
bool PyUpb_IsNumpyBoolScalar(PyObject* obj) {
230+
PyObject* type_module_obj =
231+
PyObject_GetAttrString((PyObject*)Py_TYPE(obj), "__module__");
232+
bool is_numpy = !strcmp(PyUpb_GetStrData(type_module_obj), "numpy");
233+
Py_DECREF(type_module_obj);
234+
if (!is_numpy) {
235+
return false;
236+
}
237+
238+
PyObject* type_name_obj =
239+
PyObject_GetAttrString((PyObject*)Py_TYPE(obj), "__name__");
240+
bool is_bool = !strcmp(PyUpb_GetStrData(type_name_obj), "bool");
241+
Py_DECREF(type_name_obj);
242+
if (!is_bool) {
243+
return false;
244+
}
245+
return true;
246+
}
247+
248+
static bool PyUpb_GetBool(PyObject* obj, const upb_FieldDef* f, bool* val) {
249+
if (PyUpb_IsNumpyNdarray(obj, f)) return false;
250+
if (PyUpb_IsNumpyBoolScalar(obj)) {
251+
*val = PyObject_IsTrue(obj);
252+
return !PyErr_Occurred();
253+
}
254+
*val = PyLong_AsLong(obj);
255+
return !PyErr_Occurred();
256+
}
257+
229258
bool PyUpb_PyToUpb(PyObject* obj, const upb_FieldDef* f, upb_MessageValue* val,
230259
upb_Arena* arena) {
231260
switch (upb_FieldDef_CType(f)) {
@@ -248,9 +277,7 @@ bool PyUpb_PyToUpb(PyObject* obj, const upb_FieldDef* f, upb_MessageValue* val,
248277
val->double_val = PyFloat_AsDouble(obj);
249278
return !PyErr_Occurred();
250279
case kUpb_CType_Bool:
251-
if (PyUpb_IsNumpyNdarray(obj, f)) return false;
252-
val->bool_val = PyLong_AsLong(obj);
253-
return !PyErr_Occurred();
280+
return PyUpb_GetBool(obj, f, &val->bool_val);
254281
case kUpb_CType_Bytes: {
255282
char* ptr;
256283
Py_ssize_t size;

python/google/protobuf/internal/type_checkers.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,21 @@ class BoolValueChecker(object):
113113
"""Type checker used for bool fields."""
114114

115115
def CheckValue(self, proposed_value):
116-
if not hasattr(proposed_value, '__index__') or (
117-
type(proposed_value).__module__ == 'numpy' and
116+
if not hasattr(proposed_value, '__index__'):
117+
# Under NumPy 2.3, numpy.bool does not have an __index__ method.
118+
if (type(proposed_value).__module__ == 'numpy' and
119+
type(proposed_value).__name__ == 'bool'):
120+
return bool(proposed_value)
121+
message = ('%.1024r has type %s, but expected one of: %s' %
122+
(proposed_value, type(proposed_value), (bool, int)))
123+
raise TypeError(message)
124+
125+
if (type(proposed_value).__module__ == 'numpy' and
118126
type(proposed_value).__name__ == 'ndarray'):
119127
message = ('%.1024r has type %s, but expected one of: %s' %
120128
(proposed_value, type(proposed_value), (bool, int)))
121129
raise TypeError(message)
130+
122131
return bool(proposed_value)
123132

124133
def DefaultValue(self):

python/google/protobuf/pyext/message.cc

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,20 @@ bool CheckAndGetFloat(PyObject* arg, float* value) {
558558

559559
bool CheckAndGetBool(PyObject* arg, bool* value) {
560560
long long_value = PyLong_AsLong(arg); // NOLINT
561-
if (!strcmp(Py_TYPE(arg)->tp_name, "numpy.ndarray") ||
562-
(long_value == -1 && PyErr_Occurred())) {
561+
if (long_value == -1 && PyErr_Occurred()) {
562+
// In NumPy 2.3, numpy.bool does not have an __index__ method and cannot
563+
// be converted to a long using PyLong_AsLong.
564+
if (!strcmp(Py_TYPE(arg)->tp_name, "numpy.bool")) {
565+
PyErr_Clear();
566+
int is_true = PyObject_IsTrue(arg);
567+
if (is_true >= 0) {
568+
*value = static_cast<bool>(is_true);
569+
return true;
570+
}
571+
}
572+
FormatTypeError(arg, "int, bool");
573+
return false;
574+
} else if (!strcmp(Py_TYPE(arg)->tp_name, "numpy.ndarray")) {
563575
FormatTypeError(arg, "int, bool");
564576
return false;
565577
}

0 commit comments

Comments
 (0)