Add Python class for mgp_edge

Reviewers: teon.banek, ipaljak, mferencevic

Reviewed By: teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D2679
This commit is contained in:
Teon Banek 2020-02-25 10:24:16 +01:00
parent 34db077cbd
commit e7f363ecbe
6 changed files with 369 additions and 189 deletions

View File

@ -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);

View File

@ -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)

View File

@ -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(

View File

@ -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

View File

@ -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.

View File

@ -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);