From 5953f07be371a42a6492ab72db84125a797da4f2 Mon Sep 17 00:00:00 2001 From: Lovro Lugovic <lovro.lugovic@memgraph.io> Date: Wed, 12 Feb 2020 14:29:29 +0100 Subject: [PATCH] Add conversion of py::Object to mgp_value Reviewers: teon.banek, ipaljak, mferencevic Reviewed By: teon.banek, ipaljak Subscribers: pullbot Differential Revision: https://phabricator.memgraph.io/D2667 --- src/py/py.hpp | 2 +- src/query/procedure/py_module.cpp | 133 +++++++++++++++++++++++ src/query/procedure/py_module.hpp | 8 ++ tests/unit/query_procedure_py_module.cpp | 36 ++++++ 4 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/py/py.hpp b/src/py/py.hpp index 1839d1de7..8460fe5a1 100644 --- a/src/py/py.hpp +++ b/src/py/py.hpp @@ -2,9 +2,9 @@ /// Provides a C++ API for working with Python's original C API. #pragma once +#include <optional> #include <ostream> #include <string_view> -#include <optional> // Define to use Py_ssize_t for API returning length of something. Some future // Python version will only support Py_ssize_t, so it's best to always define diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 402091377..8fbfffd55 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -1,5 +1,7 @@ #include "query/procedure/py_module.hpp" +#include <stdexcept> + #include "query/procedure/mg_procedure_impl.hpp" namespace query::procedure { @@ -50,6 +52,137 @@ py::Object MgpValueToPyObject(const mgp_value &value) { } } +mgp_value *PyObjectToMgpValue(PyObject *o, mgp_memory *memory) { + mgp_value *mgp_v{nullptr}; + + if (o == Py_None) { + mgp_v = mgp_value_make_null(memory); + } else if (PyBool_Check(o)) { + mgp_v = mgp_value_make_bool(static_cast<int>(o == Py_True), memory); + } else if (PyLong_Check(o)) { + int64_t value = PyLong_AsLong(o); + if (PyErr_Occurred()) { + PyErr_Clear(); + throw std::overflow_error("Python integer is out of range"); + } + mgp_v = mgp_value_make_int(value, memory); + } else if (PyFloat_Check(o)) { + mgp_v = mgp_value_make_double(PyFloat_AsDouble(o), memory); + } else if (PyUnicode_Check(o)) { + mgp_v = mgp_value_make_string(PyUnicode_AsUTF8(o), memory); + } else if (PyList_Check(o)) { + Py_ssize_t len = PyList_Size(o); + mgp_list *list = mgp_list_make_empty(len, memory); + + if (!list) { + throw std::bad_alloc(); + } + + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject *e = PyList_GET_ITEM(o, i); + mgp_value *v{nullptr}; + + try { + v = PyObjectToMgpValue(e, memory); + } catch (...) { + mgp_list_destroy(list); + throw; + } + + if (!mgp_list_append(list, v)) { + mgp_value_destroy(v); + mgp_list_destroy(list); + throw std::bad_alloc(); + } + + mgp_value_destroy(v); + } + + mgp_v = mgp_value_make_list(list); + } else if (PyTuple_Check(o)) { + Py_ssize_t len = PyTuple_Size(o); + mgp_list *list = mgp_list_make_empty(len, memory); + + if (!list) { + throw std::bad_alloc(); + } + + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject *e = PyTuple_GET_ITEM(o, i); + mgp_value *v{nullptr}; + + try { + v = PyObjectToMgpValue(e, memory); + } catch (...) { + mgp_list_destroy(list); + throw; + } + + if (!mgp_list_append(list, v)) { + mgp_value_destroy(v); + mgp_list_destroy(list); + throw std::bad_alloc(); + } + + mgp_value_destroy(v); + } + + mgp_v = mgp_value_make_list(list); + } else if (PyDict_Check(o)) { + mgp_map *map = mgp_map_make_empty(memory); + + if (!map) { + throw std::bad_alloc(); + } + + PyObject *key{nullptr}; + PyObject *value{nullptr}; + Py_ssize_t pos{0}; + while (PyDict_Next(o, &pos, &key, &value)) { + if (!PyUnicode_Check(key)) { + mgp_map_destroy(map); + throw std::invalid_argument("Dictionary keys must be strings"); + } + + const char *k = PyUnicode_AsUTF8(key); + mgp_value *v{nullptr}; + + if (!k) { + PyErr_Clear(); + mgp_map_destroy(map); + throw std::bad_alloc(); + } + + try { + v = PyObjectToMgpValue(value, memory); + } catch (...) { + mgp_map_destroy(map); + throw; + } + + if (!mgp_map_insert(map, k, v)) { + mgp_value_destroy(v); + mgp_map_destroy(map); + throw std::bad_alloc(); + } + + mgp_value_destroy(v); + } + + mgp_v = mgp_value_make_map(map); + } else { + // TODO: Check for Vertex, Edge and Path. Throw std::invalid_argument for + // everything else. + throw utils::NotYetImplemented("PyObjectToMgpValue"); + } + + if (!mgp_v) { + throw std::bad_alloc(); + } + + return mgp_v; +} + // Definitions of types wrapping C API types // // These should all be in the private `_mgp` Python module, which will be used diff --git a/src/query/procedure/py_module.hpp b/src/query/procedure/py_module.hpp index 12ebb2fe9..c32a3448c 100644 --- a/src/query/procedure/py_module.hpp +++ b/src/query/procedure/py_module.hpp @@ -12,6 +12,14 @@ namespace query::procedure { py::Object MgpValueToPyObject(const mgp_value &); +/// @throw std::bad_alloc +/// @throw std::overflow_error if attempting to convert a Python integer which +/// too large to fit into int64_t. +/// @throw std::invalid_argument if the given Python object cannot be converted +/// to an mgp_value (e.g. a dictionary whose keys aren't strings or an object +/// of unsupported type). +mgp_value *PyObjectToMgpValue(PyObject *, mgp_memory *); + /// Create the _mgp module for use in embedded Python. /// /// The function is to be used before Py_Initialize via the following code. diff --git a/tests/unit/query_procedure_py_module.cpp b/tests/unit/query_procedure_py_module.cpp index 120164127..32e703d25 100644 --- a/tests/unit/query_procedure_py_module.cpp +++ b/tests/unit/query_procedure_py_module.cpp @@ -57,6 +57,42 @@ TEST(PyModule, MgpValueToPyObject) { // TODO: Vertex, Edge and Path values } +TEST(PyModule, PyObjectToMgpValue) { + mgp_memory memory{utils::NewDeleteResource()}; + auto gil = py::EnsureGIL(); + py::Object py_value{Py_BuildValue("[i f s (i f s) {s i s f}]", 1, 1.0, "one", + 2, 2.0, "two", "three", 3, "four", 4.0)}; + mgp_value *value = query::procedure::PyObjectToMgpValue(py_value, &memory); + + ASSERT_TRUE(mgp_value_is_list(value)); + const mgp_list *list1 = mgp_value_get_list(value); + EXPECT_EQ(mgp_list_size(list1), 5); + ASSERT_TRUE(mgp_value_is_int(mgp_list_at(list1, 0))); + EXPECT_EQ(mgp_value_get_int(mgp_list_at(list1, 0)), 1); + ASSERT_TRUE(mgp_value_is_double(mgp_list_at(list1, 1))); + EXPECT_EQ(mgp_value_get_double(mgp_list_at(list1, 1)), 1.0); + ASSERT_TRUE(mgp_value_is_string(mgp_list_at(list1, 2))); + EXPECT_STREQ(mgp_value_get_string(mgp_list_at(list1, 2)), "one"); + ASSERT_TRUE(mgp_value_is_list(mgp_list_at(list1, 3))); + const mgp_list *list2 = mgp_value_get_list(mgp_list_at(list1, 3)); + EXPECT_EQ(mgp_list_size(list2), 3); + ASSERT_TRUE(mgp_value_is_int(mgp_list_at(list2, 0))); + EXPECT_EQ(mgp_value_get_int(mgp_list_at(list2, 0)), 2); + ASSERT_TRUE(mgp_value_is_double(mgp_list_at(list2, 1))); + EXPECT_EQ(mgp_value_get_double(mgp_list_at(list2, 1)), 2.0); + ASSERT_TRUE(mgp_value_is_string(mgp_list_at(list2, 2))); + EXPECT_STREQ(mgp_value_get_string(mgp_list_at(list2, 2)), "two"); + ASSERT_TRUE(mgp_value_is_map(mgp_list_at(list1, 4))); + const mgp_map *map = mgp_value_get_map(mgp_list_at(list1, 4)); + EXPECT_EQ(mgp_map_size(map), 2); + const mgp_value *v1 = mgp_map_at(map, "three"); + ASSERT_TRUE(mgp_value_is_int(v1)); + EXPECT_EQ(mgp_value_get_int(v1), 3); + const mgp_value *v2 = mgp_map_at(map, "four"); + ASSERT_TRUE(mgp_value_is_double(v2)); + EXPECT_EQ(mgp_value_get_double(v2), 4.0); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); // Initialize Python