diff --git a/include/mgp.py b/include/mgp.py index 8ce6d5232..982ae329f 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -128,9 +128,16 @@ class Edge: def __init__(self, edge): if not isinstance(edge, _mgp.Edge): - raise TypeError("Expected '_mgp.Edge', got '{}'".fmt(type(edge))) + raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) self._edge = edge + def __deepcopy__(self, memo): + # This is the same as the shallow copy, because we want to share the + # underlying C struct. Besides, it doesn't make much sense to actually + # copy _mgp.Edge as that is actually a reference to a graph element + # and not a proper value. + return Edge(self._edge) + def is_valid(self) -> bool: '''Return True if `self` is in valid context and may be used.''' return self._edge.is_valid() @@ -192,9 +199,16 @@ class Vertex: def __init__(self, vertex): if not isinstance(vertex, _mgp.Vertex): - raise TypeError("Expected '_mgp.Vertex', got '{}'".fmt(type(vertex))) + raise TypeError("Expected '_mgp.Vertex', got '{}'".format(type(vertex))) self._vertex = vertex + def __deepcopy__(self, memo): + # This is the same as the shallow copy, because we want to share the + # underlying C struct. Besides, it doesn't make much sense to actually + # copy _mgp.Vertex as that is actually a reference to a graph element + # and not a proper value. + return Vertex(self._vertex) + def is_valid(self) -> bool: '''Return True if `self` is in valid context and may be used''' return self._vertex.is_valid() @@ -254,15 +268,60 @@ class Vertex: return self._vertex == other._vertex +class InvalidPathError(Exception): + '''Signals using a Path instance not part of the procedure context.''' + pass + + class Path: '''Path containing Vertex and Edge instances.''' + __slots__ = ('_path', '_vertices', '_edges') - def __init__(self, starting_vertex: Vertex): + def __init__(self, starting_vertex_or_path: typing.Union[_mgp.Path, Vertex]): '''Initialize with a starting Vertex. Raise InvalidVertexError if passed in Vertex is invalid. ''' - pass + # We cache calls to `vertices` and `edges`, so as to avoid needless + # allocations at the C level. + self._vertices = None + self._edges = None + # Accepting _mgp.Path is just for internal usage. + if isinstance(starting_vertex_or_path, _mgp.Path): + self._path = starting_vertex_or_path + elif isinstance(starting_vertex_or_path, Vertex): + vertex = starting_vertex_or_path._vertex + if not vertex.is_valid(): + raise InvalidVertexError() + self._path = _mgp.Path.make_with_start(vertex) + else: + raise TypeError("Expected '_mgp.Vertex' or '_mgp.Path', got '{}'" + .format(type(starting_vertex_or_path))) + + def __copy__(self): + if not self.is_valid(): + raise InvalidPathError() + assert len(self.vertices) >= 1 + path = Path(self.vertices[0]) + for e in self.edges: + path.expand(e) + return path + + def __deepcopy__(self, memo): + try: + return Path(memo[id(self._path)]) + except KeyError: + pass + # This is the same as the shallow copy, as the underlying C API should + # not support deepcopy. Besides, it doesn't make much sense to actually + # copy Edge and Vertex types as they are actually references to graph + # elements and not proper values. + path = self.__copy__() + memo[id(self._path)] = path._path + return path + + def is_valid(self) -> bool: + return self._path.is_valid() def expand(self, edge: Edge): '''Append an edge continuing from the last vertex on the path. @@ -273,18 +332,44 @@ class Path: Raise ValueError if the current last vertex in the path is not part of the given edge. Raise InvalidEdgeError if passed in edge is invalid. + Raise InvalidPathError if using an invalid Path instance. ''' - pass + if not isinstance(edge, Edge): + raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) + if not self.is_valid(): + raise InvalidPathError() + if not edge.is_valid(): + raise InvalidEdgeError() + self._path.expand(edge._edge) + # Invalidate our cached tuples + self._vertices = None + self._edges = None @property def vertices(self) -> typing.Tuple[Vertex, ...]: - '''Vertices ordered from the start to the end of the path.''' - pass + '''Vertices ordered from the start to the end of the path. + + Raise InvalidPathError if using an invalid Path instance.''' + if not self.is_valid(): + raise InvalidPathError() + if self._vertices is None: + num_vertices = self._path.size() + 1 + self._vertices = tuple(Vertex(self._path.vertex_at(i)) + for i in range(num_vertices)) + return self._vertices @property def edges(self) -> typing.Tuple[Edge, ...]: - '''Edges ordered from the start to the end of the path.''' - pass + '''Edges ordered from the start to the end of the path. + + Raise InvalidPathError if using an invalid Path instance.''' + if not self.is_valid(): + raise InvalidPathError() + if self._edges is None: + num_edges = self._path.size() + self._edges = tuple(Edge(self._path.edge_at(i)) + for i in range(num_edges)) + return self._edges class Record: @@ -297,7 +382,7 @@ class Record: class InvalidProcCtxError(Exception): - '''Signals using an ProcCtx instance outside of the registered procedure.''' + '''Signals using a ProcCtx instance outside of the registered procedure.''' pass @@ -307,9 +392,15 @@ class Vertices: def __init__(self, graph): if not isinstance(graph, _mgp.Graph): - raise TypeError("Expected '_mgp.Graph', got '{}'".fmt(type(graph))) + raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) self._graph = graph + def __deepcopy__(self, memo): + # This is the same as the shallow copy, because we want to share the + # underlying C struct. Besides, it doesn't make much sense to actually + # copy _mgp.Graph as that always references the whole graph state. + return Vertices(self._graph) + def is_valid(self) -> bool: '''Return True if `self` is in valid context and may be used.''' return self._graph.is_valid() @@ -336,6 +427,12 @@ class Graph: raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) self._graph = graph + def __deepcopy__(self, memo): + # This is the same as the shallow copy, because we want to share the + # underlying C struct. Besides, it doesn't make much sense to actually + # copy _mgp.Graph as that always references the whole graph state. + return Graph(self._graph) + def is_valid(self) -> bool: '''Return True if `self` is in valid context and may be used.''' return self._graph.is_valid() diff --git a/src/py/py.hpp b/src/py/py.hpp index 8460fe5a1..d88631575 100644 --- a/src/py/py.hpp +++ b/src/py/py.hpp @@ -43,6 +43,7 @@ class Object final { public: Object() = default; + Object(std::nullptr_t) {} explicit Object(PyObject *ptr) noexcept : ptr_(ptr) {} ~Object() noexcept { Py_XDECREF(ptr_); } diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index a52819632..f635721c4 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -1,11 +1,24 @@ #include "query/procedure/py_module.hpp" +#include #include +#include #include "query/procedure/mg_procedure_impl.hpp" namespace query::procedure { +// Set this as a __reduce__ special method on our types to prevent `pickle` and +// `copy` module operations on our types. +PyObject *DisallowPickleAndCopy(PyObject *self, PyObject *Py_UNUSED(ignored)) { + auto *type = Py_TYPE(self); + std::stringstream ss; + ss << "cannot pickle nor copy '" << type->tp_name << "' object"; + const auto &msg = ss.str(); + PyErr_SetString(PyExc_TypeError, msg.c_str()); + return nullptr; +} + // Definitions of types wrapping C API types // // These should all be in the private `_mgp` Python module, which will be used @@ -68,6 +81,8 @@ PyObject *PyVerticesIteratorNext(PyVerticesIterator *self, } static PyMethodDef PyVerticesIteratorMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"get", reinterpret_cast(PyVerticesIteratorGet), METH_NOARGS, "Get the current vertex pointed to by the iterator or return None."}, {"next", reinterpret_cast(PyVerticesIteratorNext), METH_NOARGS, @@ -126,6 +141,8 @@ PyObject *PyEdgesIteratorNext(PyEdgesIterator *self, } static PyMethodDef PyEdgesIteratorMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"get", reinterpret_cast(PyEdgesIteratorGet), METH_NOARGS, "Get the current edge pointed to by the iterator or return None."}, {"next", reinterpret_cast(PyEdgesIteratorNext), METH_NOARGS, @@ -189,6 +206,8 @@ PyObject *PyGraphIterVertices(PyGraph *self, PyObject *Py_UNUSED(ignored)) { } static PyMethodDef PyGraphMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"is_valid", reinterpret_cast(PyGraphIsValid), METH_NOARGS, "Return True if Graph is in valid context and may be used."}, {"get_vertex_by_id", reinterpret_cast(PyGraphGetVertexById), @@ -337,6 +356,8 @@ PyObject *PyQueryProcAddDeprecatedResult(PyQueryProc *self, PyObject *args) { } static PyMethodDef PyQueryProcMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"add_arg", reinterpret_cast(PyQueryProcAddArg), METH_VARARGS, "Add a required argument to a procedure."}, {"add_opt_arg", reinterpret_cast(PyQueryProcAddOptArg), @@ -399,6 +420,8 @@ PyObject *PyQueryModuleAddReadProcedure(PyQueryModule *self, PyObject *cb) { } static PyMethodDef PyQueryModuleMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"add_read_procedure", reinterpret_cast(PyQueryModuleAddReadProcedure), METH_O, "Register a read-only procedure with this module."}, @@ -566,10 +589,12 @@ void PyEdgeDealloc(PyEdge *self) { } PyObject *PyEdgeIsValid(PyEdge *self, PyObject *Py_UNUSED(ignored)) { - return PyBool_FromLong(!!self->py_graph->graph); + return PyBool_FromLong(self->py_graph && self->py_graph->graph); } static PyMethodDef PyEdgeMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"is_valid", reinterpret_cast(PyEdgeIsValid), METH_NOARGS, "Return True if Edge is in valid context and may be used."}, {"get_type_name", reinterpret_cast(PyEdgeGetTypeName), @@ -578,7 +603,8 @@ static PyMethodDef PyEdgeMethods[] = { METH_NOARGS, "Return the edge's source vertex."}, {"to_vertex", reinterpret_cast(PyEdgeToVertex), METH_NOARGS, "Return the edge's destination vertex."}, - {nullptr}}; + {nullptr}, +}; PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op); @@ -652,7 +678,7 @@ void PyVertexDealloc(PyVertex *self) { } PyObject *PyVertexIsValid(PyVertex *self, PyObject *Py_UNUSED(ignored)) { - return PyBool_FromLong(!!self->py_graph->graph); + return PyBool_FromLong(self->py_graph && self->py_graph->graph); } PyObject *PyVertexGetId(PyVertex *self, PyObject *Py_UNUSED(ignored)) { @@ -742,6 +768,8 @@ PyObject *PyVertexIterOutEdges(PyVertex *self, PyObject *Py_UNUSED(ignored)) { } static PyMethodDef PyVertexMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, {"is_valid", reinterpret_cast(PyVertexIsValid), METH_NOARGS, "Return True if Vertex is in valid context and may be used."}, {"get_id", reinterpret_cast(PyVertexGetId), METH_NOARGS, @@ -754,7 +782,8 @@ static PyMethodDef PyVertexMethods[] = { METH_NOARGS, "Return _mgp.EdgesIterator for in edges"}, {"iter_out_edges", reinterpret_cast(PyVertexIterOutEdges), METH_NOARGS, "Return _mgp.EdgesIterator for out edges"}, - {nullptr}}; + {nullptr}, +}; PyObject *PyVertexRichCompare(PyObject *self, PyObject *other, int op); @@ -805,6 +834,170 @@ PyObject *PyVertexRichCompare(PyObject *self, PyObject *other, int op) { return PyBool_FromLong(mgp_vertex_equal(v1->vertex, v2->vertex)); } +struct PyPath { + PyObject_HEAD + mgp_path *path; + PyGraph *py_graph; +}; + +void PyPathDealloc(PyPath *self) { + CHECK(self->path); + CHECK(self->py_graph); + // Avoid invoking `mgp_path_destroy` if we are not in valid execution + // context. The query execution should free all memory used during + // execution, so we may cause a double free issue. + if (self->py_graph->graph) mgp_path_destroy(self->path); + Py_DECREF(self->py_graph); + Py_TYPE(self)->tp_free(self); +} + +PyObject *PyPathIsValid(PyPath *self, PyObject *Py_UNUSED(ignored)) { + return PyBool_FromLong(self->py_graph && self->py_graph->graph); +} + +PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex); + +PyObject *PyPathExpand(PyPath *self, PyObject *edge) { + CHECK(self->path); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + if (Py_TYPE(edge) != &PyEdgeType) { + PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Edge."); + return nullptr; + } + auto *py_edge = reinterpret_cast(edge); + const auto *to = mgp_edge_get_to(py_edge->edge); + const auto *from = mgp_edge_get_from(py_edge->edge); + const auto *last_vertex = + mgp_path_vertex_at(self->path, mgp_path_size(self->path)); + if (!mgp_vertex_equal(last_vertex, to) && + !mgp_vertex_equal(last_vertex, from)) { + PyErr_SetString(PyExc_ValueError, + "Edge is not a continuation of the path."); + return nullptr; + } + if (!mgp_path_expand(self->path, py_edge->edge)) { + PyErr_SetString(PyExc_MemoryError, "Unable to expand mgp_path."); + return nullptr; + } + Py_RETURN_NONE; +} + +PyObject *PyPathSize(PyPath *self, PyObject *Py_UNUSED(ignored)) { + CHECK(self->path); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + return PyLong_FromSize_t(mgp_path_size(self->path)); +} + +PyObject *PyPathVertexAt(PyPath *self, PyObject *args) { + CHECK(self->path); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + static_assert(std::numeric_limits::max() <= + std::numeric_limits::max()); + Py_ssize_t i; + if (!PyArg_ParseTuple(args, "n", &i)) return nullptr; + const auto *vertex = mgp_path_vertex_at(self->path, i); + if (!vertex) { + PyErr_SetString(PyExc_IndexError, "Index is out of range."); + return nullptr; + } + return MakePyVertex(*vertex, self->py_graph); +} + +PyObject *PyPathEdgeAt(PyPath *self, PyObject *args) { + CHECK(self->path); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + static_assert(std::numeric_limits::max() <= + std::numeric_limits::max()); + Py_ssize_t i; + if (!PyArg_ParseTuple(args, "n", &i)) return nullptr; + const auto *edge = mgp_path_edge_at(self->path, i); + if (!edge) { + PyErr_SetString(PyExc_IndexError, "Index is out of range."); + return nullptr; + } + return MakePyEdge(*edge, self->py_graph); +} + +static PyMethodDef PyPathMethods[] = { + {"__reduce__", reinterpret_cast(DisallowPickleAndCopy), + METH_NOARGS, "__reduce__ is not supported"}, + {"is_valid", reinterpret_cast(PyPathIsValid), METH_NOARGS, + "Return True if Path is in valid context and may be used."}, + {"make_with_start", reinterpret_cast(PyPathMakeWithStart), + METH_O | METH_CLASS, "Create a path with a starting vertex."}, + {"expand", reinterpret_cast(PyPathExpand), METH_O, + "Append an edge continuing from the last vertex on the path."}, + {"size", reinterpret_cast(PyPathSize), METH_NOARGS, + "Return the number of edges in a mgp_path."}, + {"vertex_at", reinterpret_cast(PyPathVertexAt), METH_VARARGS, + "Return the vertex from a path at given index."}, + {"edge_at", reinterpret_cast(PyPathEdgeAt), METH_VARARGS, + "Return the edge from a path at given index."}, + {nullptr}, +}; + +static PyTypeObject PyPathType = { + PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "_mgp.Path", + .tp_doc = "Wraps struct mgp_path.", + .tp_basicsize = sizeof(PyPath), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_methods = PyPathMethods, + .tp_dealloc = reinterpret_cast(PyPathDealloc), +}; + +PyObject *MakePyPath(mgp_path *path, PyGraph *py_graph) { + CHECK(path); + CHECK(py_graph->graph && py_graph->memory); + CHECK(path->GetMemoryResource() == py_graph->memory->impl); + auto *py_path = PyObject_New(PyPath, &PyPathType); + if (!py_path) return nullptr; + py_path->path = path; + py_path->py_graph = py_graph; + Py_INCREF(py_graph); + return PyObject_Init(reinterpret_cast(py_path), &PyPathType); +} + +PyObject *MakePyPath(const mgp_path &path, PyGraph *py_graph) { + CHECK(py_graph); + CHECK(py_graph->graph && py_graph->memory); + auto *path_copy = mgp_path_copy(&path, py_graph->memory); + if (!path_copy) { + PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_path."); + return nullptr; + } + auto *py_path = MakePyPath(path_copy, py_graph); + if (!py_path) mgp_path_destroy(path_copy); + return py_path; +} + +PyObject *PyPathMakeWithStart(PyTypeObject *type, PyObject *vertex) { + if (type != &PyPathType) { + PyErr_SetString(PyExc_TypeError, + "Expected '' as the first argument."); + return nullptr; + } + if (Py_TYPE(vertex) != &PyVertexType) { + PyErr_SetString(PyExc_TypeError, + "Expected a '_mgp.Vertex' as the second argument."); + return nullptr; + } + auto *py_vertex = reinterpret_cast(vertex); + auto *path = + mgp_path_make_with_start(py_vertex->vertex, py_vertex->py_graph->memory); + if (!path) { + PyErr_SetString(PyExc_MemoryError, "Unable to allocate mgp_path."); + return nullptr; + } + auto *py_path = MakePyPath(path, py_vertex->py_graph); + if (!py_path) mgp_path_destroy(path); + return py_path; +} + PyObject *PyInitMgpModule() { PyObject *mgp = PyModule_Create(&PyMgpModule); if (!mgp) return nullptr; @@ -829,6 +1022,7 @@ PyObject *PyInitMgpModule() { if (!register_type(&PyQueryProcType, "Proc")) return nullptr; if (!register_type(&PyQueryModuleType, "Module")) return nullptr; if (!register_type(&PyVertexType, "Vertex")) return nullptr; + if (!register_type(&PyPathType, "Path")) return nullptr; if (!register_type(&PyCypherTypeType, "Type")) return nullptr; Py_INCREF(Py_None); if (PyModule_AddObject(mgp, "_MODULE", Py_None) < 0) { @@ -874,6 +1068,14 @@ py::Object ReloadPyModule(PyObject *py_module, mgp_module *module_def) { }); } +py::Object MgpValueToPyObject(const mgp_value &value, PyObject *py_graph) { + if (Py_TYPE(py_graph) != &PyGraphType) { + PyErr_SetString(PyExc_TypeError, "Expected a _mgp.Graph."); + return nullptr; + } + return MgpValueToPyObject(value, reinterpret_cast(py_graph)); +} + py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph) { switch (mgp_value_get_type(&value)) { case MGP_VALUE_TYPE_NULL: @@ -919,8 +1121,10 @@ py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph) { const auto *e = mgp_value_get_edge(&value); return py::Object(reinterpret_cast(MakePyEdge(*e, py_graph))); } - case MGP_VALUE_TYPE_PATH: - throw utils::NotYetImplemented("MgpValueToPyObject"); + case MGP_VALUE_TYPE_PATH: { + const auto *p = mgp_value_get_path(&value); + return py::Object(reinterpret_cast(MakePyPath(*p, py_graph))); + } } } @@ -1037,10 +1241,20 @@ mgp_value *PyObjectToMgpValue(PyObject *o, mgp_memory *memory) { mgp_edge_destroy(e); throw std::bad_alloc(); } - } else { - // TODO: Check for Vertex and Path. Throw std::invalid_argument for - // everything else. + } else if (Py_TYPE(o) == &PyPathType) { + auto *p = mgp_path_copy(reinterpret_cast(o)->path, memory); + if (!p) { + throw std::bad_alloc(); + } + mgp_v = mgp_value_make_path(p); + if (!mgp_v) { + mgp_path_destroy(p); + throw std::bad_alloc(); + } + } else if (Py_TYPE(o) == &PyVertexType) { throw utils::NotYetImplemented("PyObjectToMgpValue"); + } else { + throw std::invalid_argument("Unsupported PyObject conversion"); } if (!mgp_v) { diff --git a/src/query/procedure/py_module.hpp b/src/query/procedure/py_module.hpp index ff67b4f20..bd9e53130 100644 --- a/src/query/procedure/py_module.hpp +++ b/src/query/procedure/py_module.hpp @@ -20,6 +20,8 @@ struct PyGraph; /// `py::Object` instance and set the appropriate Python exception. py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph); +py::Object MgpValueToPyObject(const mgp_value &value, PyObject *py_graph); + /// Convert a Python object into `mgp_value`, constructing it using the given /// `mgp_memory` allocator. /// diff --git a/tests/unit/query_procedure_py_module.cpp b/tests/unit/query_procedure_py_module.cpp index b49378300..0d933aac9 100644 --- a/tests/unit/query_procedure_py_module.cpp +++ b/tests/unit/query_procedure_py_module.cpp @@ -57,7 +57,78 @@ TEST(PyModule, MgpValueToPyObject) { ASSERT_TRUE(PyUnicode_Check(py_str)); EXPECT_EQ(std::string(PyUnicode_AsUTF8(py_str)), "some text"); } - // TODO: Vertex, Edge and Path values +} + +// Our _mgp types should not support (by default) pickling and copying. +static void AssertPickleAndCopyAreNotSupported(PyObject *py_obj) { + py::Object pickle_mod(PyImport_ImportModule("pickle")); + ASSERT_TRUE(pickle_mod); + ASSERT_FALSE(py::FetchError()); + py::Object dumps_res(pickle_mod.CallMethod("dumps", py_obj)); + ASSERT_FALSE(dumps_res); + ASSERT_TRUE(py::FetchError()); + py::Object copy_mod(PyImport_ImportModule("copy")); + ASSERT_TRUE(pickle_mod); + ASSERT_FALSE(py::FetchError()); + py::Object copy_res(pickle_mod.CallMethod("copy", py_obj)); + ASSERT_FALSE(copy_res); + ASSERT_TRUE(py::FetchError()); + // We should have cleared the error state. + ASSERT_FALSE(py::FetchError()); + py::Object deepcopy_res(pickle_mod.CallMethod("deepcopy", py_obj)); + ASSERT_FALSE(deepcopy_res); + ASSERT_TRUE(py::FetchError()); +} + +// TODO: Test Vertex and Edge values +TEST(PyModule, PyPath) { + storage::Storage db; + { + auto dba = db.Access(); + auto v1 = dba.CreateVertex(); + auto v2 = dba.CreateVertex(); + ASSERT_TRUE( + dba.CreateEdge(&v1, &v2, dba.NameToEdgeType("type")).HasValue()); + ASSERT_FALSE(dba.Commit().HasError()); + } + auto storage_dba = db.Access(); + query::DbAccessor dba(&storage_dba); + mgp_memory memory{utils::NewDeleteResource()}; + mgp_graph graph{&dba, storage::View::OLD}; + auto *start_v = mgp_graph_get_vertex_by_id(&graph, mgp_vertex_id{0}, &memory); + ASSERT_TRUE(start_v); + auto *path = mgp_path_make_with_start(start_v, &memory); + ASSERT_TRUE(path); + auto *edges_it = mgp_vertex_iter_out_edges(start_v, &memory); + ASSERT_TRUE(edges_it); + for (const auto *edge = mgp_edges_iterator_get(edges_it); edge; + edge = mgp_edges_iterator_next(edges_it)) { + ASSERT_TRUE(mgp_path_expand(path, edge)); + } + ASSERT_EQ(mgp_path_size(path), 1); + mgp_edges_iterator_destroy(edges_it); + mgp_vertex_destroy(start_v); + auto *path_value = mgp_value_make_path(path); + ASSERT_TRUE(path_value); + auto gil = py::EnsureGIL(); + py::Object py_graph(query::procedure::MakePyGraph(&graph, &memory)); + ASSERT_TRUE(py_graph); + // We have setup the C structs, so create convert to PyObject. + py::Object py_path_value( + query::procedure::MgpValueToPyObject(*path_value, py_graph)); + ASSERT_TRUE(py_path_value); + AssertPickleAndCopyAreNotSupported(py_path_value); + // Convert back to C struct and check equality. + auto *new_path_value = + query::procedure::PyObjectToMgpValue(py_path_value, &memory); + ASSERT_TRUE(new_path_value); + ASSERT_NE(new_path_value, path_value); // Pointer compare. + ASSERT_TRUE(mgp_value_is_path(new_path_value)); + ASSERT_TRUE(mgp_path_equal(mgp_value_get_path(path_value), + mgp_value_get_path(new_path_value))); + mgp_value_destroy(new_path_value); + mgp_value_destroy(path_value); + ASSERT_FALSE(dba.Commit().HasError()); } TEST(PyModule, PyObjectToMgpValue) {