diff --git a/include/mg_procedure.h b/include/mg_procedure.h index 504fd9e73..c3908d56a 100644 --- a/include/mg_procedure.h +++ b/include/mg_procedure.h @@ -538,6 +538,9 @@ struct mgp_edge *mgp_edge_copy(const struct mgp_edge *e, /// Free the memory used by a mgp_edge. void mgp_edge_destroy(struct mgp_edge *e); +/// Return non-zero if given edges are equal, otherwise 0. +int mgp_edge_equal(const struct mgp_edge *e1, const struct mgp_edge *e2); + /// Return the type of the given edge. struct mgp_edge_type mgp_edge_get_type(const struct mgp_edge *e); diff --git a/include/mgp.py b/include/mgp.py index d3287e637..8209209dc 100644 --- a/include/mgp.py +++ b/include/mgp.py @@ -63,6 +63,9 @@ Property = namedtuple('Property', ('name', 'value')) class Properties: '''A collection of properties either on a Vertex or an Edge.''' + def __init__(self, obj): + raise NotImplementedError() + def get(self, property_name: str, default=None) -> object: '''Get the value of a property with the given name or return default. @@ -111,10 +114,14 @@ class Properties: class EdgeType: '''Type of an Edge.''' + __slots__ = ('_name',) + + def __init__(self, name): + self._name = name @property def name(self) -> str: - pass + return self._name class InvalidEdgeError(Exception): @@ -129,30 +136,50 @@ class Edge: a query. You should not globally store an instance of an Edge. Using an invalid Edge instance will raise InvalidEdgeError. ''' + __slots__ = ('_edge',) + + def __init__(self, edge): + if not isinstance(edge, _mgp.Edge): + raise TypeError("Expected '_mgp.Edge', got '{}'".fmt(type(edge))) + self._edge = edge + + def is_valid(self) -> bool: + '''Return True if `self` is in valid context and may be used.''' + return self._edge.is_valid() @property def type(self) -> EdgeType: '''Raise InvalidEdgeError.''' - pass + if not self.is_valid(): + raise InvalidEdgeError() + return EdgeType(self._edge.get_type_name()) @property def from_vertex(self): # -> Vertex: '''Raise InvalidEdgeError.''' - pass + if not self.is_valid(): + raise InvalidEdgeError() + return Vertex(self._edge.from_vertex()) @property def to_vertex(self): # -> Vertex: '''Raise InvalidEdgeError.''' - pass + if not self.is_valid(): + raise InvalidEdgeError() + return Vertex(self._edge.to_vertex()) @property def properties(self) -> Properties: '''Raise InvalidEdgeError.''' - pass + if not self.is_valid(): + raise InvalidEdgeError() + return Properties(self._edge) def __eq__(self, other) -> bool: '''Raise InvalidEdgeError.''' - pass + if not self.is_valid(): + raise InvalidEdgeError() + return self._edge == other._edge VertexId = typing.NewType('VertexId', int) diff --git a/src/query/procedure/mg_procedure_impl.cpp b/src/query/procedure/mg_procedure_impl.cpp index d3a5f9342..67736e1fa 100644 --- a/src/query/procedure/mg_procedure_impl.cpp +++ b/src/query/procedure/mg_procedure_impl.cpp @@ -1122,6 +1122,10 @@ mgp_edge *mgp_edge_copy(const mgp_edge *v, mgp_memory *memory) { void mgp_edge_destroy(mgp_edge *e) { delete_mgp_object(e); } +int mgp_edge_equal(const struct mgp_edge *e1, const struct mgp_edge *e2) { + return e1->impl == e2->impl ? 1 : 0; +} + mgp_edge_type mgp_edge_get_type(const mgp_edge *e) { const auto &name = e->from.graph->impl->EdgeTypeToName(e->impl.EdgeType()); static_assert( diff --git a/src/query/procedure/py_module.cpp b/src/query/procedure/py_module.cpp index 8cb8f02fd..32b8d7d54 100644 --- a/src/query/procedure/py_module.cpp +++ b/src/query/procedure/py_module.cpp @@ -6,183 +6,6 @@ namespace query::procedure { -py::Object MgpValueToPyObject(const mgp_value &value) { - switch (mgp_value_get_type(&value)) { - case MGP_VALUE_TYPE_NULL: - Py_INCREF(Py_None); - return py::Object(Py_None); - case MGP_VALUE_TYPE_BOOL: - return py::Object(PyBool_FromLong(mgp_value_get_bool(&value))); - case MGP_VALUE_TYPE_INT: - return py::Object(PyLong_FromLongLong(mgp_value_get_int(&value))); - case MGP_VALUE_TYPE_DOUBLE: - return py::Object(PyFloat_FromDouble(mgp_value_get_double(&value))); - case MGP_VALUE_TYPE_STRING: - return py::Object(PyUnicode_FromString(mgp_value_get_string(&value))); - case MGP_VALUE_TYPE_LIST: { - const auto *list = mgp_value_get_list(&value); - const size_t len = mgp_list_size(list); - py::Object py_list(PyList_New(len)); - CHECK(py_list); - for (size_t i = 0; i < len; ++i) { - auto elem = MgpValueToPyObject(*mgp_list_at(list, i)); - CHECK(elem); - // Explicitly convert `py_list`, which is `py::Object`, via static_cast. - // Then the macro will cast it to `PyList *`. - PyList_SET_ITEM(static_cast<PyObject *>(py_list), i, elem.Steal()); - } - return py_list; - } - case MGP_VALUE_TYPE_MAP: { - const auto *map = mgp_value_get_map(&value); - py::Object py_dict(PyDict_New()); - CHECK(py_dict); - for (const auto &[key, val] : map->items) { - auto py_val = MgpValueToPyObject(val); - CHECK(py_val); - // Unlike PyList_SET_ITEM, PyDict_SetItem does not steal the value. - CHECK(PyDict_SetItemString(py_dict, key.c_str(), py_val) == 0); - } - return py_dict; - } - case MGP_VALUE_TYPE_VERTEX: - case MGP_VALUE_TYPE_EDGE: - case MGP_VALUE_TYPE_PATH: - throw utils::NotYetImplemented("MgpValueToPyObject"); - } -} - -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 @@ -264,7 +87,6 @@ static PyTypeObject PyVerticesIteratorType = { .tp_dealloc = reinterpret_cast<destructor>(PyVerticesIteratorDealloc), }; - PyObject *PyGraphIsValid(PyGraph *self, PyObject *Py_UNUSED(ignored)) { return PyBool_FromLong(!!self->graph); } @@ -518,6 +340,118 @@ static PyModuleDef PyMgpModule = { .m_size = -1, }; +struct PyEdge { + PyObject_HEAD + mgp_edge *edge; + PyGraph *py_graph; +}; + +PyObject *PyEdgeGetTypeName(PyEdge *self, PyObject *Py_UNUSED(ignored)) { + CHECK(self); + CHECK(self->edge); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + return PyUnicode_FromString(mgp_edge_get_type(self->edge).name); +} + +PyObject *PyEdgeFromVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { + CHECK(self); + CHECK(self->edge); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + const auto *vertex = mgp_edge_get_from(self->edge); + CHECK(vertex); + // TODO: Wrap mgp_vertex_copy(vertex) into _mgp.Vertex and return it. + PyErr_SetString(PyExc_NotImplementedError, "from_vertex"); + return nullptr; +} + +PyObject *PyEdgeToVertex(PyEdge *self, PyObject *Py_UNUSED(ignored)) { + CHECK(self); + CHECK(self->edge); + CHECK(self->py_graph); + CHECK(self->py_graph->graph); + const auto *vertex = mgp_edge_get_to(self->edge); + CHECK(vertex); + // TODO: Wrap mgp_vertex_copy(vertex) into _mgp.Vertex and return it. + PyErr_SetString(PyExc_NotImplementedError, "to_vertex"); + return nullptr; +} + +void PyEdgeDealloc(PyEdge *self) { + CHECK(self->edge); + CHECK(self->py_graph); + // Avoid invoking `mgp_edge_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_edge_destroy(self->edge); + Py_DECREF(self->py_graph); + Py_TYPE(self)->tp_free(self); +} + +PyObject *PyEdgeIsValid(PyEdge *self, PyObject *Py_UNUSED(ignored)) { + return PyBool_FromLong(!!self->py_graph->graph); +} + +static PyMethodDef PyEdgeMethods[] = { + {"is_valid", reinterpret_cast<PyCFunction>(PyEdgeIsValid), METH_NOARGS, + "Return True if Edge is in valid context and may be used."}, + {"get_type_name", reinterpret_cast<PyCFunction>(PyEdgeGetTypeName), + METH_NOARGS, "Return the edge's type name."}, + {"from_vertex", reinterpret_cast<PyCFunction>(PyEdgeFromVertex), + METH_NOARGS, "Return the edge's source vertex."}, + {"to_vertex", reinterpret_cast<PyCFunction>(PyEdgeToVertex), METH_NOARGS, + "Return the edge's destination vertex."}, + {nullptr}}; + +PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op); + +static PyTypeObject PyEdgeType = { + PyVarObject_HEAD_INIT(nullptr, 0).tp_name = "_mgp.Edge", + .tp_doc = "Wraps struct mgp_edge.", + .tp_basicsize = sizeof(PyEdge), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_methods = PyEdgeMethods, + .tp_dealloc = reinterpret_cast<destructor>(PyEdgeDealloc), + .tp_richcompare = PyEdgeRichCompare, +}; + +/// Create an instance of `_mgp.Edge` class. +/// +/// The ownership of the edge is given to the created instance and will be +/// destroyed once the instance itself is destroyed, taking care that the +/// execution context is still valid. +/// +/// The created instance references an existing `_mgp.Graph` instance, which +/// marks the execution context. +PyObject *MakePyEdge(mgp_edge *edge, PyGraph *py_graph) { + CHECK(edge->GetMemoryResource() == py_graph->memory->impl); + auto *py_edge = PyObject_New(PyEdge, &PyEdgeType); + if (!py_edge) return nullptr; + py_edge->edge = edge; + py_edge->py_graph = py_graph; + Py_INCREF(py_graph); + return PyObject_Init(reinterpret_cast<PyObject *>(py_edge), &PyEdgeType); +} + +PyObject *PyEdgeRichCompare(PyObject *self, PyObject *other, int op) { + CHECK(self); + CHECK(other); + + if (Py_TYPE(self) != &PyEdgeType || Py_TYPE(other) != &PyEdgeType || + op != Py_EQ) { + Py_RETURN_NOTIMPLEMENTED; + } + + auto *e1 = reinterpret_cast<PyEdge *>(self); + auto *e2 = reinterpret_cast<PyEdge *>(other); + CHECK(e1->edge); + CHECK(e2->edge); + + return PyBool_FromLong(mgp_edge_equal(e1->edge, e2->edge)); +} + PyObject *PyInitMgpModule() { PyObject *mgp = PyModule_Create(&PyMgpModule); if (!mgp) return nullptr; @@ -537,6 +471,7 @@ PyObject *PyInitMgpModule() { if (!register_type(&PyVerticesIteratorType, "VerticesIterator")) return nullptr; if (!register_type(&PyGraphType, "Graph")) return nullptr; + if (!register_type(&PyEdgeType, "Edge")) return nullptr; if (!register_type(&PyQueryProcType, "Proc")) return nullptr; if (!register_type(&PyQueryModuleType, "Module")) return nullptr; Py_INCREF(Py_None); @@ -583,4 +518,198 @@ py::Object ReloadPyModule(PyObject *py_module, mgp_module *module_def) { }); } +py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph) { + switch (mgp_value_get_type(&value)) { + case MGP_VALUE_TYPE_NULL: + Py_INCREF(Py_None); + return py::Object(Py_None); + case MGP_VALUE_TYPE_BOOL: + return py::Object(PyBool_FromLong(mgp_value_get_bool(&value))); + case MGP_VALUE_TYPE_INT: + return py::Object(PyLong_FromLongLong(mgp_value_get_int(&value))); + case MGP_VALUE_TYPE_DOUBLE: + return py::Object(PyFloat_FromDouble(mgp_value_get_double(&value))); + case MGP_VALUE_TYPE_STRING: + return py::Object(PyUnicode_FromString(mgp_value_get_string(&value))); + case MGP_VALUE_TYPE_LIST: { + const auto *list = mgp_value_get_list(&value); + const size_t len = mgp_list_size(list); + py::Object py_list(PyList_New(len)); + CHECK(py_list); + for (size_t i = 0; i < len; ++i) { + auto elem = MgpValueToPyObject(*mgp_list_at(list, i), py_graph); + CHECK(elem); + // Explicitly convert `py_list`, which is `py::Object`, via static_cast. + // Then the macro will cast it to `PyList *`. + PyList_SET_ITEM(static_cast<PyObject *>(py_list), i, elem.Steal()); + } + return py_list; + } + case MGP_VALUE_TYPE_MAP: { + const auto *map = mgp_value_get_map(&value); + py::Object py_dict(PyDict_New()); + CHECK(py_dict); + for (const auto &[key, val] : map->items) { + auto py_val = MgpValueToPyObject(val, py_graph); + CHECK(py_val); + // Unlike PyList_SET_ITEM, PyDict_SetItem does not steal the value. + CHECK(PyDict_SetItemString(py_dict, key.c_str(), py_val) == 0); + } + return py_dict; + } + case MGP_VALUE_TYPE_VERTEX: + throw utils::NotYetImplemented("MgpValueToPyObject"); + case MGP_VALUE_TYPE_EDGE: { + // Copy the edge and pass the ownership to the created _mgp.Edge + // instance. + auto *e = mgp_edge_copy(mgp_value_get_edge(&value), py_graph->memory); + if (!e) { + PyErr_NoMemory(); + return py::Object(); + } + return py::Object(reinterpret_cast<PyObject *>(MakePyEdge(e, py_graph))); + } + case MGP_VALUE_TYPE_PATH: + throw utils::NotYetImplemented("MgpValueToPyObject"); + } +} + +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 if (Py_TYPE(o) == &PyEdgeType) { + // Copy the edge and pass the ownership to the created mgp_value. + auto *e = mgp_edge_copy(reinterpret_cast<PyEdge *>(o)->edge, memory); + if (!e) { + throw std::bad_alloc(); + } + mgp_v = mgp_value_make_edge(e); + } else { + // TODO: Check for Vertex and Path. Throw std::invalid_argument for + // everything else. + throw utils::NotYetImplemented("PyObjectToMgpValue"); + } + + if (!mgp_v) { + throw std::bad_alloc(); + } + + return mgp_v; +} + } // namespace query::procedure diff --git a/src/query/procedure/py_module.hpp b/src/query/procedure/py_module.hpp index f96266e9b..ff67b4f20 100644 --- a/src/query/procedure/py_module.hpp +++ b/src/query/procedure/py_module.hpp @@ -11,8 +11,18 @@ struct mgp_value; namespace query::procedure { -py::Object MgpValueToPyObject(const mgp_value &); +struct PyGraph; +/// Convert an `mgp_value` into a Python object, referencing the given `PyGraph` +/// instance and using the same allocator as the graph. +/// +/// Return a non-null `py::Object` instance on success. Otherwise, return a null +/// `py::Object` instance and set the appropriate Python exception. +py::Object MgpValueToPyObject(const mgp_value &value, PyGraph *py_graph); + +/// Convert a Python object into `mgp_value`, constructing it using the given +/// `mgp_memory` allocator. +/// /// @throw std::bad_alloc /// @throw std::overflow_error if attempting to convert a Python integer which /// too large to fit into int64_t. diff --git a/tests/unit/query_procedure_py_module.cpp b/tests/unit/query_procedure_py_module.cpp index 32e703d25..b49378300 100644 --- a/tests/unit/query_procedure_py_module.cpp +++ b/tests/unit/query_procedure_py_module.cpp @@ -27,7 +27,10 @@ TEST(PyModule, MgpValueToPyObject) { mgp_value_destroy(list_val); auto *map_val = mgp_value_make_map(map); auto gil = py::EnsureGIL(); - auto py_dict = query::procedure::MgpValueToPyObject(*map_val); + py::Object py_graph(query::procedure::MakePyGraph(nullptr, &memory)); + auto py_dict = query::procedure::MgpValueToPyObject( + *map_val, reinterpret_cast<query::procedure::PyGraph *>( + static_cast<PyObject *>(py_graph))); mgp_value_destroy(map_val); // We should now have in Python: // {"list": [None, False, True, 42, 0.1, "some text"]} @@ -101,12 +104,16 @@ int main(int argc, char **argv) { // Set program name, so Python can find its way to runtime libraries relative // to executable. Py_SetProgramName(program_name); + PyImport_AppendInittab("_mgp", &query::procedure::PyInitMgpModule); Py_InitializeEx(0 /* = initsigs */); PyEval_InitThreads(); int test_result; - Py_BEGIN_ALLOW_THREADS; - test_result = RUN_ALL_TESTS(); - Py_END_ALLOW_THREADS; + { + py::Object mgp(PyImport_ImportModule("_mgp")); + Py_BEGIN_ALLOW_THREADS; + test_result = RUN_ALL_TESTS(); + Py_END_ALLOW_THREADS; + } // Shutdown Python Py_Finalize(); PyMem_RawFree(program_name);