Skip to content

Commit

Permalink
Add python proto_api test to OSS
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 703303491
  • Loading branch information
anandolee authored and copybara-github committed Dec 12, 2024
1 parent 0765dd4 commit af4c862
Show file tree
Hide file tree
Showing 5 changed files with 286 additions and 5 deletions.
64 changes: 61 additions & 3 deletions python/build_targets.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ def build_targets(name):
"manual",
],
deps = [
":proto_api",
"//src/google/protobuf",
":proto_api",
"//src/google/protobuf:port",
"//src/google/protobuf:protobuf_lite",
"//src/google/protobuf/io",
Expand Down Expand Up @@ -448,6 +448,11 @@ def build_targets(name):
srcs = ["google/protobuf/internal/proto_json_test.py"],
)

internal_py_test(
name = "python_version_test",
srcs = ["python_version_test.py"],
)

native.cc_library(
name = "proto_api",
srcs = ["google/protobuf/proto_api.cc"],
Expand All @@ -459,13 +464,66 @@ def build_targets(name):
"//src/google/protobuf/io",
"@com_google_absl//absl/log:absl_check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/status:statusor",
"@system_python//:python_headers",
],
)

native.cc_binary(
name = "google/protobuf/internal/proto_api_example.so",
srcs = native.glob([
"google/protobuf/pyext/*.cc",
"google/protobuf/pyext/*.h",
"google/protobuf/internal/proto_api_example.cc",
]),
copts = COPTS + [
"-DGOOGLE_PROTOBUF_HAS_ONEOF=1",
# The Python API requires patterns that are ISO C incompatible, like
# casts between function pointers and object pointers.
"-Wno-pedantic",
],
linkopts = selects.with_or({
(
"//python/dist:osx_x86_64",
"//python/dist:osx_aarch64",
): ["-Wl,-undefined,dynamic_lookup"],
"//python/dist:windows_x86_32": ["-static-libgcc"],
"//conditions:default": [],
}),
includes = ["."],
linkshared = 1,
linkstatic = 1,
deps = [
":proto_api",
"//src/google/protobuf",
"//src/google/protobuf:port",
"//src/google/protobuf:protobuf_lite",
"//src/google/protobuf/io",
"//src/google/protobuf/io:tokenizer",
"//src/google/protobuf/stubs:lite",
"//src/google/protobuf/util:differencer",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/log:absl_check",
"@com_google_absl//absl/status",
"//src/google/protobuf:unittest_proto3_cc_proto",
"@com_google_absl//absl/log:absl_log",
"@com_google_absl//absl/strings",
] + select({
"//python:limited_api_3.9": ["@python-3.9.0//:python_headers"],
"//python:full_api_3.9_win32": ["@nuget_python_i686_3.9.0//:python_full_api"],
"//python:full_api_3.9_win64": ["@nuget_python_x86-64_3.9.0//:python_full_api"],
"//python:limited_api_3.10_win32": ["@nuget_python_i686_3.10.0//:python_limited_api"],
"//python:limited_api_3.10_win64": ["@nuget_python_x86-64_3.10.0//:python_limited_api"],
"//conditions:default": ["@system_python//:python_headers"],
}),
)

internal_py_test(
name = "python_version_test",
srcs = ["python_version_test.py"],
name = "proto_api_test",
srcs = ["google/protobuf/internal/proto_api_test.py"],
deps = [
":google/protobuf/internal/proto_api_example.so",
],
)

conformance_test(
Expand Down
141 changes: 141 additions & 0 deletions python/google/protobuf/internal/proto_api_example.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include <Python.h>

#include "absl/log/absl_log.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include "google/protobuf/descriptor.h"
#include "google/protobuf/text_format.h"
#include "google/protobuf/unittest_proto3.pb.h"
#include "google/protobuf/proto_api.h"

// We have to run the pure python import first, otherwise PyCapsule_Import will
// fail if this module is the very first import.
bool PrerequisitesSatisfied() {
return true;
}

const google::protobuf::python::PyProto_API* GetProtoApi() {
return google::protobuf::python::GetAPI();
}

PyObject* pyclear_message(PyObject* self, PyObject* args) {
PyObject* py_message;
if (!PyArg_ParseTuple(args, "O", &py_message)) return nullptr;
auto msg = GetProtoApi()->GetClearedMessageMutator(py_message);
if (!msg.ok()) {
PyErr_Format(PyExc_TypeError, std::string(msg.status().message()).c_str());
return nullptr;
}
Py_INCREF(Py_None);
return Py_None;
}

PyObject* pyparse_message(PyObject* self, PyObject* args) {
PyObject* py_message;
char* text_proto;
if (!PyArg_ParseTuple(args, "sO", &text_proto, &py_message)) return nullptr;
auto message = GetProtoApi()->GetClearedMessageMutator(py_message);
if (!message.ok()) {
PyErr_Format(PyExc_TypeError,
std::string(message.status().message()).c_str());
return nullptr;
}
if (message->get() == nullptr) {
return nullptr;
}
// No op. Test '->' operator
if ((*message)->ByteSizeLong()) {
return nullptr;
}

ABSL_LOG(ERROR) << "Try to parse message";
ABSL_LOG(ERROR) << message.value()->GetDescriptor()->file()->name();
// ABSL_LOG(ERROR) << message.value()->GetDescriptor()->DebugString();
google::protobuf::TextFormat::Parser parser;
parser.ParseFromString(text_proto, message.value().get());
Py_INCREF(Py_None);
return Py_None;
}

PyObject* pycheck_cpp_proto(PyObject* self, PyObject* args) {
PyObject* py_message;
if (!PyArg_ParseTuple(args, "O", &py_message)) return nullptr;
auto msg = GetProtoApi()->GetConstMessagePointer(py_message);
if (!msg.ok()) {
PyErr_Format(PyExc_TypeError, std::string(msg.status().message()).c_str());
return nullptr;
}
if (msg->get().GetDescriptor()->file()->pool() ==
google::protobuf::DescriptorPool::generated_pool()) {
Py_RETURN_TRUE;
}
ABSL_LOG(ERROR) << "file is not from generated pool";
ABSL_LOG(ERROR) << msg.value().get().GetDescriptor()->file()->name();
// ABSL_LOG(ERROR) << msg.value().get().GetDescriptor()->DebugString();
Py_RETURN_FALSE;
}

PyObject* pymessage_get(PyObject* self, PyObject* args) {
PyObject* py_message;
if (!PyArg_ParseTuple(args, "O", &py_message)) return nullptr;
auto message = GetProtoApi()->GetConstMessagePointer(py_message);
if (!message.ok()) {
PyErr_Format(PyExc_TypeError,
std::string(message.status().message()).c_str());
return nullptr;
}
// Test move constructor.
google::protobuf::python::PythonConstMessagePointer moved_msg(std::move(*message));
const proto3_unittest::TestAllTypes& msg_ptr =
static_cast<const proto3_unittest::TestAllTypes&>(moved_msg.get());
return PyLong_FromLong(msg_ptr.optional_int32());
}

PyObject* pymessage_mutate_const(PyObject* self, PyObject* args) {
PyObject* py_message;
if (!PyArg_ParseTuple(args, "O", &py_message)) return nullptr;
auto message = GetProtoApi()->GetConstMessagePointer(py_message);
PyObject_SetAttrString(py_message, "optional_bool", Py_True);
// The message has been changed. Returns false if detect not changed.
if (message->NotChanged()) {
Py_RETURN_FALSE;
}
// Change it back before returning.
PyErr_Clear();
PyObject* temp = PyObject_CallMethod(py_message, "Clear", nullptr);
Py_DECREF(temp);
Py_RETURN_TRUE;
}

PyMethodDef module_methods[] = {
{"ClearMessage", reinterpret_cast<PyCFunction>(pyclear_message),
METH_VARARGS, "Clear message."},
{"ParseMessage", reinterpret_cast<PyCFunction>(pyparse_message),
METH_VARARGS, "Parse message."},
{"IsCppProtoLinked", reinterpret_cast<PyCFunction>(pycheck_cpp_proto),
METH_VARARGS, "Check if the generated cpp proto is linked."},
{"GetOptionalInt32", reinterpret_cast<PyCFunction>(pymessage_get),
METH_VARARGS, "Get optional_int32 field."},
{"MutateConstAlive", reinterpret_cast<PyCFunction>(pymessage_mutate_const),
METH_VARARGS, "Mutate python message while keep a const pointer."},
{nullptr, nullptr}};

extern "C" {
PyMODINIT_FUNC PyInit_proto_api_example(void) {
if (!PrerequisitesSatisfied() || GetProtoApi() == nullptr) {
return nullptr;
}
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"proto_api_example", /* m_name */
"proto_api test", /* m_doc */
-1, /* m_size */
module_methods, /* m_methods */
nullptr, /* m_reload */
nullptr, /* m_traverse */
nullptr, /* m_clear */
nullptr, /* m_free */
};
return PyModule_Create(&moduledef);
}
}
68 changes: 68 additions & 0 deletions python/google/protobuf/internal/proto_api_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Protocol Buffers - Google's data interchange format
# Copyright 2008 Google Inc. All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

"""Unittest for proto_api."""

import unittest

from google.protobuf.internal import more_extensions_pb2
from google.protobuf.internal import proto_api_example
from google.protobuf.internal import testing_refleaks

from google.protobuf import unittest_proto3_pb2


@testing_refleaks.TestCase
class ProtoApiTest(unittest.TestCase):

def test_message_mutator_clear_generated_factory(self):
msg = unittest_proto3_pb2.TestAllTypes(
optional_int32=24, optional_string='optional_string'
)
self.assertEqual(24, msg.optional_int32)
self.assertEqual('optional_string', msg.optional_string)
self.assertTrue(proto_api_example.IsCppProtoLinked(msg))
proto_api_example.ClearMessage(msg)
self.assertEqual(0, msg.optional_int32)
self.assertEqual('', msg.optional_string)

def test_message_mutator_clear_dynamic_factory(self):
msg = more_extensions_pb2.ForeignMessage(foreign_message_int=15)
self.assertIn('foreign_message_int', msg)
self.assertFalse(proto_api_example.IsCppProtoLinked(msg))
proto_api_example.ClearMessage(msg)
self.assertNotIn('foreign_message_int', msg)

def test_not_a_message(self):
with self.assertRaises(TypeError):
proto_api_example.IsCppProtoLinked(112)
with self.assertRaises(TypeError):
proto_api_example.GetOptionalInt32(True)

def test_message_mutator_parse(self):
msg = more_extensions_pb2.ForeignMessage(foreign_message_int=123)
proto_api_example.ParseMessage('foreign_message_int: 321', msg)
self.assertEqual(321, msg.foreign_message_int)
return
msg = unittest_proto3_pb2.TestAllTypes(
optional_int32=24, optional_string='optional_string'
)
proto_api_example.ParseMessage('optional_string: "changed"', msg)
self.assertEqual(0, msg.optional_int32)
self.assertEqual(msg.optional_string, 'changed')

def test_message_const_pointer_get(self):
msg = unittest_proto3_pb2.TestAllTypes(optional_int32=123)
self.assertEqual(123, proto_api_example.GetOptionalInt32(msg))

def test_mutate_python_message_while_const_pinter_alive(self):
msg = unittest_proto3_pb2.TestAllTypes()
self.assertTrue(proto_api_example.MutateConstAlive(msg))


if __name__ == '__main__':
unittest.main()
6 changes: 5 additions & 1 deletion python/google/protobuf/proto_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <Python.h>

#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "google/protobuf/descriptor_database.h"
#include "google/protobuf/message.h"

Expand Down Expand Up @@ -200,10 +201,13 @@ class PythonConstMessagePointer {
};

inline const char* PyProtoAPICapsuleName() {
static const char kCapsuleName[] = "google.protobuf.pyext._message.proto_API";
static const char kCapsuleName[] =
"google.protobuf.internal.proto_api_example.proto_API";
return kCapsuleName;
}

PyProto_API* GetAPI();

} // namespace python
} // namespace protobuf
} // namespace google
Expand Down
12 changes: 11 additions & 1 deletion python/google/protobuf/pyext/message_module.cc
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@ struct ApiImplementation : google::protobuf::python::PyProto_API {
};

} // namespace
namespace google {
namespace protobuf {
namespace python {
PyProto_API* GetAPI() {
static auto api = new ApiImplementation();
return api;
}
} // namespace python
} // namespace protobuf
} // namespace google

static const char module_docstring[] =
"python-proto2 is a module that can be used to enhance proto2 Python API\n"
Expand Down Expand Up @@ -335,7 +345,7 @@ PyMODINIT_FUNC PyInit__message() {

// Adds the C++ API
if (PyObject* api = PyCapsule_New(
new ApiImplementation(), google::protobuf::python::PyProtoAPICapsuleName(),
google::protobuf::python::GetAPI(), google::protobuf::python::PyProtoAPICapsuleName(),
[](PyObject* o) {
delete (ApiImplementation*)PyCapsule_GetPointer(
o, google::protobuf::python::PyProtoAPICapsuleName());
Expand Down

0 comments on commit af4c862

Please sign in to comment.