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