From 15c665e525c82a55c782b5b3630d9fe76366eb4d Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Sun, 11 Aug 2024 12:51:26 +0200 Subject: [PATCH 01/11] Add spatial data types --- src/glue.c | 2 ++ src/types.c | 2 ++ src/types.h | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/glue.c b/src/glue.c index 40999f87..2f3c8446 100644 --- a/src/glue.c +++ b/src/glue.c @@ -379,6 +379,7 @@ PyObject *mg_value_to_py_object(const mg_value *value) { return mg_local_date_time_to_py_datetime(mg_value_local_date_time(value)); case MG_VALUE_TYPE_DURATION: return mg_duration_to_py_delta(mg_value_duration(value)); + // TODO(gitbuda): Add Point2&3D. default: PyErr_SetString(PyExc_RuntimeError, "encountered a mg_value of unknown type"); @@ -648,6 +649,7 @@ mg_value *py_object_to_mg_value(PyObject *object) { return NULL; } ret = mg_value_make_duration(dur); + // TODO(gitbuda): Add Point2&3D. } else { PyErr_Format(PyExc_ValueError, "value of type '%s' can't be used as query parameter", diff --git a/src/types.c b/src/types.c index b51e87fd..da2b1d9f 100644 --- a/src/types.c +++ b/src/types.c @@ -572,5 +572,7 @@ PyTypeObject PathType = { .tp_new = PyType_GenericNew }; +// TODO(gitbuda): Add Point2&3D equivalent to e.g. node. + #undef CHECK_ATTRIBUTE // clang-format on diff --git a/src/types.h b/src/types.h index 74863bc5..a20adbfe 100644 --- a/src/types.h +++ b/src/types.h @@ -42,10 +42,29 @@ typedef struct { PyObject *nodes; PyObject *relationships; } PathObject; + +typedef struct { + PyObject_HEAD + + uint16_t srid; + double x_longitude; + double y_latitude; +} Point2DObject; + +typedef struct { + PyObject_HEAD + + uint16_t srid; + double x_longitude; + double y_latitude; + double z_height; +} Point3DObject; // clang-format on extern PyTypeObject NodeType; extern PyTypeObject RelationshipType; extern PyTypeObject PathType; +extern PyTypeObject Point2DType; +extern PyTypeObject Point3DType; #endif From 36d5921b2121a852653c0fe1a66a7edad5e9bb4b Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Sun, 11 Aug 2024 12:59:24 +0200 Subject: [PATCH 02/11] Update mgclient to the latest version --- mgclient | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mgclient b/mgclient index 6f59c8a4..19998932 160000 --- a/mgclient +++ b/mgclient @@ -1 +1 @@ -Subproject commit 6f59c8a4216d20e42e0943cf506d0ddb6241c29b +Subproject commit 19998932beb0f54dee0347b515cd9c9da60a83b4 From 6be7515dcd4c934af9e0fe6e7d538d95486a834e Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Sun, 11 Aug 2024 13:17:17 +0200 Subject: [PATCH 03/11] Add a few more TODOs --- test/test_glue.py | 3 +++ test/test_types.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/test/test_glue.py b/test/test_glue.py index eafa2e15..2f829426 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -263,3 +263,6 @@ def test_duration(memgraph_connection): cursor.execute("RETURN $value", {"value": datetime.timedelta(64, 7, 11, 1)}) result = cursor.fetchall() assert result == [(datetime.timedelta(64, 7, 1011),)] + + +# TODO(gitbuda): Add spatial tests equivalent to temporal. diff --git a/test/test_types.py b/test/test_types.py index e1da3d4d..b9e9ac72 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -47,3 +47,6 @@ def test_path(): path = mgclient.Path([n1, n2, n3], [e1, e2]) assert str(path) == "(:Label1)-[:Edge1]->(:Label2)<-[:Edge2]-(:Label3)" + + +# TODO(gitbuda): Add Point2&3D test. From e435f30327a2471904144117d2df90c9a3cf9b0d Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Thu, 15 Aug 2024 22:25:34 +0200 Subject: [PATCH 04/11] Fix the flag, some temporal test is failing --- .gitignore | 3 ++- test/common.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 683a57a1..c2aa9b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ pymgclient.egg-info/ .venv *.pyc *.pyo -__pycache__/ \ No newline at end of file +__pycache__/ +.pytest_cache/ diff --git a/test/common.py b/test/common.py index f3c481c5..e67590c7 100644 --- a/test/common.py +++ b/test/common.py @@ -88,7 +88,7 @@ def start_memgraph(cert_file="", key_file=""): "--storage-properties-on-edges=true", "--storage-snapshot-interval-sec=0", "--storage-wal-enabled=false", - "--storage-recover-on-startup=false", + "--data-recovery-on-startup=false", "--storage-snapshot-on-exit=false", "--telemetry-enabled=false", "--log-file", From 7eae436f55c3feac509712f7f02c7688890cdc0f Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Fri, 16 Aug 2024 11:40:34 +0200 Subject: [PATCH 05/11] Add v1 of Point2D (still broken) --- src/mgclientmodule.c | 1 + src/types.c | 121 ++++++++++++++++++++++++++++++++++++++++++- test/test_types.py | 5 ++ 3 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/mgclientmodule.c b/src/mgclientmodule.c index 4c97106d..0e08afd8 100644 --- a/src/mgclientmodule.c +++ b/src/mgclientmodule.c @@ -171,6 +171,7 @@ static struct { {"Node", &NodeType}, {"Relationship", &RelationshipType}, {"Path", &PathType}, + {"Point2D", &Point2DType}, {NULL, NULL}}; static int add_module_types(PyObject *module) { diff --git a/src/types.c b/src/types.c index da2b1d9f..737696e6 100644 --- a/src/types.c +++ b/src/types.c @@ -572,7 +572,126 @@ PyTypeObject PathType = { .tp_new = PyType_GenericNew }; -// TODO(gitbuda): Add Point2&3D equivalent to e.g. node. +static void point2d_dealloc(Point2DObject *point2d) { + Py_TYPE(point2d)->tp_free(point2d); +} + +static PyObject *point2d_repr(Point2DObject *point2d) { + return PyUnicode_FromFormat("<%s(srid=%u, x_longitude=%d, y_latitude=%d) at %p>", + Py_TYPE(point2d)->tp_name, point2d->x_longitude, point2d->y_latitude, + point2d); +} + +static PyObject *point2d_str(Point2DObject *point2d) { + return PyUnicode_FromFormat("Point2D({ srid=%u, x_longitude=%d, y_latitude=%d })", + point2d->srid, point2d->x_longitude, point2d->y_latitude); +} + +// Helper function for implementing richcompare. +static PyObject *point2d_astuple(Point2DObject *point2d) { + PyObject *tuple = NULL; + PyObject *srid = NULL; + PyObject *x_longitude = NULL; + PyObject *y_latitude = NULL; + + if (!(srid = PyLong_FromUnsignedLong(point2d->srid))) { + goto cleanup; + } + if (!(x_longitude = PyFloat_FromDouble(point2d->x_longitude))) { + goto cleanup; + } + if (!(y_latitude = PyFloat_FromDouble(point2d->y_latitude))) { + goto cleanup; + } + if (!(tuple = PyTuple_New(3))) { + goto cleanup; + } + + PyTuple_SET_ITEM(tuple, 0, srid); + PyTuple_SET_ITEM(tuple, 1, x_longitude); + PyTuple_SET_ITEM(tuple, 2, y_latitude); + return tuple; + +cleanup: + Py_XDECREF(tuple); + Py_XDECREF(srid); + Py_XDECREF(x_longitude); + Py_XDECREF(y_latitude); + return NULL; +} + +// TODO(gitbuda): Verify that the richcompare is correct. +static PyObject *point2d_richcompare(Point2DObject *lhs, PyObject *rhs, int op) { + PyObject *tlhs = NULL; + PyObject *trhs = NULL; + PyObject *ret = NULL; + + if (Py_TYPE(rhs) == &Point2DType) { + if (!(tlhs = point2d_astuple(lhs))) { + goto exit; + } + if (!(trhs = point2d_astuple((Point2DObject *)rhs))) { + goto exit; + } + ret = PyObject_RichCompare(tlhs, trhs, op); + } else { + Py_INCREF(Py_False); + ret = Py_False; + } + +exit: + Py_XDECREF(tlhs); + Py_XDECREF(trhs); + return ret; +} + +int point2d_init(Point2DObject *point2d, PyObject *args, PyObject *kwargs) { + uint16_t srid = 0; + double x_longitude = 0; + double y_latitude = 0; + static char *kwlist[] = {"", "", "", NULL}; + // TODO(gitbuda): https://docs.python.org/3/c-api/arg.html#numbers + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Idd", kwlist, &srid, &x_longitude, &y_latitude)) { + return -1; + } + + point2d->srid = srid; + point2d->x_longitude = x_longitude; + point2d->y_latitude = y_latitude; + return 0; +} + +PyDoc_STRVAR(Point2DType_srid_doc, + "Point2D srid (a unique identifier associated with a specific coordinate system, tolerance, and resolution)."); +PyDoc_STRVAR(Point2DType_x_longitude_doc, "Point2D x or longitude value."); +PyDoc_STRVAR(Point2DType_y_latitude_doc, "Point2D y or latitude value."); +static PyMemberDef point2d_members[] = { + {"srid", T_USHORT, offsetof(Point2DObject, srid), READONLY, Point2DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point2DObject, x_longitude), READONLY, Point2DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point2DObject, y_latitude), READONLY, Point2DType_y_latitude_doc}, + {NULL}}; + +PyDoc_STRVAR(Point2DType_doc, + "A Point2D object."); +// clang-format off +PyTypeObject Point2DType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mgclient.Point2D", + .tp_basicsize = sizeof(Point2DObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)point2d_dealloc, + .tp_repr = (reprfunc)point2d_repr, + .tp_str = (reprfunc)point2d_str, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = Point2DType_doc, + .tp_richcompare = (richcmpfunc)point2d_richcompare, + .tp_members = point2d_members, + .tp_init = (initproc)point2d_init, + .tp_new = PyType_GenericNew +}; +// clang-format on + +// TODO(gitbuda): Add Point3D #undef CHECK_ATTRIBUTE // clang-format on diff --git a/test/test_types.py b/test/test_types.py index b9e9ac72..57ef0041 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -50,3 +50,8 @@ def test_path(): # TODO(gitbuda): Add Point2&3D test. +def test_point2d(): + p1 = mgclient.Point2D(0, 1, 2.1); + p2 = mgclient.Point2D(0, 1.2, 2.1); + assert p1 == p2 + assert str(p1) == "Point2D" and repr(p1) == "Point2D" From 1405e1180edd3ece2ca2b3c6c73847c3d781d265 Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Fri, 16 Aug 2024 16:01:57 +0200 Subject: [PATCH 06/11] Make Point2D type to work --- src/types.c | 18 ++++++++++-------- test/test_types.py | 24 +++++++++++++++++++----- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/types.c b/src/types.c index 737696e6..4cf9c201 100644 --- a/src/types.c +++ b/src/types.c @@ -577,14 +577,17 @@ static void point2d_dealloc(Point2DObject *point2d) { } static PyObject *point2d_repr(Point2DObject *point2d) { - return PyUnicode_FromFormat("<%s(srid=%u, x_longitude=%d, y_latitude=%d) at %p>", - Py_TYPE(point2d)->tp_name, point2d->x_longitude, point2d->y_latitude, - point2d); + char buffer[256]; + snprintf(buffer, sizeof(buffer), "<%s(srid=%u, x_longitude=%f, y_latitude=%f) at %p>", Py_TYPE(point2d)->tp_name, point2d->srid, point2d->x_longitude, point2d->y_latitude, point2d); + return PyUnicode_FromFormat("%s", buffer); } static PyObject *point2d_str(Point2DObject *point2d) { - return PyUnicode_FromFormat("Point2D({ srid=%u, x_longitude=%d, y_latitude=%d })", - point2d->srid, point2d->x_longitude, point2d->y_latitude); + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double values. + // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value + char buffer[256]; + snprintf(buffer, sizeof(buffer), "Point2D({ srid=%u, x_longitude=%f, y_latitude=%f })", point2d->srid, point2d->x_longitude, point2d->y_latitude); + return PyUnicode_FromFormat("%s", buffer); } // Helper function for implementing richcompare. @@ -620,7 +623,6 @@ static PyObject *point2d_astuple(Point2DObject *point2d) { return NULL; } -// TODO(gitbuda): Verify that the richcompare is correct. static PyObject *point2d_richcompare(Point2DObject *lhs, PyObject *rhs, int op) { PyObject *tlhs = NULL; PyObject *trhs = NULL; @@ -650,8 +652,8 @@ int point2d_init(Point2DObject *point2d, PyObject *args, PyObject *kwargs) { double x_longitude = 0; double y_latitude = 0; static char *kwlist[] = {"", "", "", NULL}; - // TODO(gitbuda): https://docs.python.org/3/c-api/arg.html#numbers - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Idd", kwlist, &srid, &x_longitude, &y_latitude)) { + // https://docs.python.org/3/c-api/arg.html#numbers + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hdd", kwlist, &srid, &x_longitude, &y_latitude)) { return -1; } diff --git a/test/test_types.py b/test/test_types.py index 57ef0041..104f668a 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -49,9 +49,23 @@ def test_path(): assert str(path) == "(:Label1)-[:Edge1]->(:Label2)<-[:Edge2]-(:Label3)" -# TODO(gitbuda): Add Point2&3D test. def test_point2d(): - p1 = mgclient.Point2D(0, 1, 2.1); - p2 = mgclient.Point2D(0, 1.2, 2.1); - assert p1 == p2 - assert str(p1) == "Point2D" and repr(p1) == "Point2D" + p1 = mgclient.Point2D(0, 1, 2); + assert p1.srid == 0 + assert p1.x_longitude == 1 + assert p1.y_latitude == 2 + assert str(p1) == "Point2D({ srid=0, x_longitude=1.000000, y_latitude=2.000000 })" + assert repr(p1).startswith(" Date: Fri, 16 Aug 2024 16:22:14 +0200 Subject: [PATCH 07/11] Add mg -> py, tests WIP --- src/glue.c | 11 ++++++++++- test/test_glue.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/glue.c b/src/glue.c index 2f3c8446..a546f084 100644 --- a/src/glue.c +++ b/src/glue.c @@ -342,6 +342,13 @@ PyObject *mg_duration_to_py_delta(const mg_duration *dur) { return make_py_delta(days, seconds, (nanoseconds / 1000)); } +PyObject *mg_point_2d_to_py_point2d(const mg_point_2d *point2d) { + PyObject *ret = PyObject_CallFunction( + (PyObject *)&Point2DType, "Hdd", mg_point_2d_srid(point2d), + mg_point_2d_x(point2d), mg_point_2d_y(point2d)); + return ret; +} + PyObject *mg_value_to_py_object(const mg_value *value) { switch (mg_value_get_type(value)) { case MG_VALUE_TYPE_NULL: @@ -379,7 +386,9 @@ PyObject *mg_value_to_py_object(const mg_value *value) { return mg_local_date_time_to_py_datetime(mg_value_local_date_time(value)); case MG_VALUE_TYPE_DURATION: return mg_duration_to_py_delta(mg_value_duration(value)); - // TODO(gitbuda): Add Point2&3D. + case MG_VALUE_TYPE_POINT_2D: + return mg_point_2d_to_py_point2d(mg_value_point_2d(value)); + // TODO(gitbuda): Add Point3D. default: PyErr_SetString(PyExc_RuntimeError, "encountered a mg_value of unknown type"); diff --git a/test/test_glue.py b/test/test_glue.py index 2f829426..54e13b60 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -265,4 +265,14 @@ def test_duration(memgraph_connection): assert result == [(datetime.timedelta(64, 7, 1011),)] +def test_point2d(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + + cursor.execute("RETURN point({x:0, y:1}) AS point;") + result = cursor.fetchall() + # TODO(gitbuda): Pointer to the object is also take into account; Point2D == doesn't work + assert result == [(mgclient.Point2D(7203, 0, 1))] + + # TODO(gitbuda): Add spatial tests equivalent to temporal. From 2d491c2aeaa53ca9cefb2a2088073d70ca6ac760 Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Fri, 16 Aug 2024 21:23:37 +0200 Subject: [PATCH 08/11] Add send/encoder path for Point2D --- mgclient | 2 +- src/glue.c | 14 +++++++++++++- src/types.c | 2 +- test/test_glue.py | 11 ++++++++++- test/test_types.py | 2 +- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/mgclient b/mgclient index 19998932..df0aeb34 160000 --- a/mgclient +++ b/mgclient @@ -1 +1 @@ -Subproject commit 19998932beb0f54dee0347b515cd9c9da60a83b4 +Subproject commit df0aeb3439813eb08d541c3855b312a90f54cb25 diff --git a/src/glue.c b/src/glue.c index a546f084..e8425383 100644 --- a/src/glue.c +++ b/src/glue.c @@ -597,6 +597,12 @@ mg_duration *py_delta_to_mg_duration(PyObject *obj) { return mg_duration_make(0, days, seconds, microseconds * 1000); } +mg_point_2d *py_point2d_to_mg_point_2d(PyObject *point_object) { + assert(Py_TYPE(point_object) == &Point2DType); + Point2DObject *py_point2d = (Point2DObject *)point_object; + return mg_point_2d_make(py_point2d->srid, py_point2d->x_longitude, py_point2d->y_latitude); +} + mg_value *py_object_to_mg_value(PyObject *object) { mg_value *ret = NULL; @@ -658,7 +664,13 @@ mg_value *py_object_to_mg_value(PyObject *object) { return NULL; } ret = mg_value_make_duration(dur); - // TODO(gitbuda): Add Point2&3D. + } else if (Py_TYPE(object) == &Point2DType) { + mg_point_2d *point = py_point2d_to_mg_point_2d(object); + if (!point) { + return NULL; + } + ret = mg_value_make_point_2d(point); + // TODO(gitbuda): Add Point3D. } else { PyErr_Format(PyExc_ValueError, "value of type '%s' can't be used as query parameter", diff --git a/src/types.c b/src/types.c index 4cf9c201..50fc02e7 100644 --- a/src/types.c +++ b/src/types.c @@ -614,7 +614,7 @@ static PyObject *point2d_astuple(Point2DObject *point2d) { PyTuple_SET_ITEM(tuple, 1, x_longitude); PyTuple_SET_ITEM(tuple, 2, y_latitude); return tuple; - + cleanup: Py_XDECREF(tuple); Py_XDECREF(srid); diff --git a/test/test_glue.py b/test/test_glue.py index 54e13b60..6ef4a21d 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -265,7 +265,7 @@ def test_duration(memgraph_connection): assert result == [(datetime.timedelta(64, 7, 1011),)] -def test_point2d(memgraph_connection): +def test_point2d_receive(memgraph_connection): conn = memgraph_connection cursor = conn.cursor() @@ -275,4 +275,13 @@ def test_point2d(memgraph_connection): assert result == [(mgclient.Point2D(7203, 0, 1))] +def test_point2d_send(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + + cursor.execute("RETURN $value", {"value": mgclient.Point2D(7203, 0, 1)}) + result = cursor.fetchall() + # TODO(gitbuda): Pointer to the object is also take into account; Point2D == doesn't work + assert result == [(mgclient.Point2D(7203, 0, 1))] + # TODO(gitbuda): Add spatial tests equivalent to temporal. diff --git a/test/test_types.py b/test/test_types.py index 104f668a..8dd4e967 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -51,7 +51,7 @@ def test_path(): def test_point2d(): p1 = mgclient.Point2D(0, 1, 2); - assert p1.srid == 0 + assert p1.srid == 0 assert p1.x_longitude == 1 assert p1.y_latitude == 2 assert str(p1) == "Point2D({ srid=0, x_longitude=1.000000, y_latitude=2.000000 })" From 7ad2bf26aecd8d32e801e22b764b2c1f52ae9d7f Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Sat, 17 Aug 2024 10:30:49 +0200 Subject: [PATCH 09/11] Fix the point2d asserts --- test/test_glue.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/test_glue.py b/test/test_glue.py index 6ef4a21d..b5691581 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -268,20 +268,17 @@ def test_duration(memgraph_connection): def test_point2d_receive(memgraph_connection): conn = memgraph_connection cursor = conn.cursor() - cursor.execute("RETURN point({x:0, y:1}) AS point;") result = cursor.fetchall() - # TODO(gitbuda): Pointer to the object is also take into account; Point2D == doesn't work - assert result == [(mgclient.Point2D(7203, 0, 1))] + assert result == [(mgclient.Point2D(7203, 0, 1),)] def test_point2d_send(memgraph_connection): conn = memgraph_connection cursor = conn.cursor() - cursor.execute("RETURN $value", {"value": mgclient.Point2D(7203, 0, 1)}) result = cursor.fetchall() - # TODO(gitbuda): Pointer to the object is also take into account; Point2D == doesn't work - assert result == [(mgclient.Point2D(7203, 0, 1))] + assert result == [(mgclient.Point2D(7203, 0, 1),)] + -# TODO(gitbuda): Add spatial tests equivalent to temporal. +# TODO(gitbuda): Add Point3D test. From a3ac4de72139b31b9e26739268eeb7c59f2dab74 Mon Sep 17 00:00:00 2001 From: Marko Budiselic Date: Sat, 17 Aug 2024 10:58:32 +0200 Subject: [PATCH 10/11] Make Point3D work --- src/glue.c | 23 +++++++- src/mgclientmodule.c | 1 + src/types.c | 133 ++++++++++++++++++++++++++++++++++++++++++- test/test_glue.py | 15 ++++- test/test_types.py | 18 +++++- 5 files changed, 184 insertions(+), 6 deletions(-) diff --git a/src/glue.c b/src/glue.c index e8425383..d8f15e84 100644 --- a/src/glue.c +++ b/src/glue.c @@ -349,6 +349,13 @@ PyObject *mg_point_2d_to_py_point2d(const mg_point_2d *point2d) { return ret; } +PyObject *mg_point_3d_to_py_point3d(const mg_point_3d *point3d) { + PyObject *ret = PyObject_CallFunction( + (PyObject *)&Point3DType, "Hddd", mg_point_3d_srid(point3d), + mg_point_3d_x(point3d), mg_point_3d_y(point3d), mg_point_3d_z(point3d)); + return ret; +} + PyObject *mg_value_to_py_object(const mg_value *value) { switch (mg_value_get_type(value)) { case MG_VALUE_TYPE_NULL: @@ -388,7 +395,8 @@ PyObject *mg_value_to_py_object(const mg_value *value) { return mg_duration_to_py_delta(mg_value_duration(value)); case MG_VALUE_TYPE_POINT_2D: return mg_point_2d_to_py_point2d(mg_value_point_2d(value)); - // TODO(gitbuda): Add Point3D. + case MG_VALUE_TYPE_POINT_3D: + return mg_point_3d_to_py_point3d(mg_value_point_3d(value)); default: PyErr_SetString(PyExc_RuntimeError, "encountered a mg_value of unknown type"); @@ -603,6 +611,12 @@ mg_point_2d *py_point2d_to_mg_point_2d(PyObject *point_object) { return mg_point_2d_make(py_point2d->srid, py_point2d->x_longitude, py_point2d->y_latitude); } +mg_point_3d *py_point3d_to_mg_point_3d(PyObject *point_object) { + assert(Py_TYPE(point_object) == &Point3DType); + Point3DObject *py_point3d = (Point3DObject *)point_object; + return mg_point_3d_make(py_point3d->srid, py_point3d->x_longitude, py_point3d->y_latitude, py_point3d->z_height); +} + mg_value *py_object_to_mg_value(PyObject *object) { mg_value *ret = NULL; @@ -670,7 +684,12 @@ mg_value *py_object_to_mg_value(PyObject *object) { return NULL; } ret = mg_value_make_point_2d(point); - // TODO(gitbuda): Add Point3D. + } else if (Py_TYPE(object) == &Point3DType) { + mg_point_3d *point = py_point3d_to_mg_point_3d(object); + if (!point) { + return NULL; + } + ret = mg_value_make_point_3d(point); } else { PyErr_Format(PyExc_ValueError, "value of type '%s' can't be used as query parameter", diff --git a/src/mgclientmodule.c b/src/mgclientmodule.c index 0e08afd8..080a049b 100644 --- a/src/mgclientmodule.c +++ b/src/mgclientmodule.c @@ -172,6 +172,7 @@ static struct { {"Relationship", &RelationshipType}, {"Path", &PathType}, {"Point2D", &Point2DType}, + {"Point3D", &Point3DType}, {NULL, NULL}}; static int add_module_types(PyObject *module) { diff --git a/src/types.c b/src/types.c index 50fc02e7..a018b178 100644 --- a/src/types.c +++ b/src/types.c @@ -571,6 +571,7 @@ PyTypeObject PathType = { .tp_init = (initproc)path_init, .tp_new = PyType_GenericNew }; +// clang-format on static void point2d_dealloc(Point2DObject *point2d) { Py_TYPE(point2d)->tp_free(point2d); @@ -693,7 +694,135 @@ PyTypeObject Point2DType = { }; // clang-format on -// TODO(gitbuda): Add Point3D +static void point3d_dealloc(Point3DObject *point3d) { + Py_TYPE(point3d)->tp_free(point3d); +} -#undef CHECK_ATTRIBUTE +static PyObject *point3d_repr(Point3DObject *point3d) { + char buffer[256]; + snprintf(buffer, sizeof(buffer), "<%s(srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f) at %p>", Py_TYPE(point3d)->tp_name, point3d->srid, point3d->x_longitude, point3d->y_latitude, point3d->z_height, point3d); + return PyUnicode_FromFormat("%s", buffer); +} + +static PyObject *point3d_str(Point3DObject *point3d) { + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double values. + // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value + char buffer[256]; + snprintf(buffer, sizeof(buffer), "Point3D({ srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f })", point3d->srid, point3d->x_longitude, point3d->y_latitude, point3d->z_height); + return PyUnicode_FromFormat("%s", buffer); +} + +// Helper function for implementing richcompare. +static PyObject *point3d_astuple(Point3DObject *point3d) { + PyObject *tuple = NULL; + PyObject *srid = NULL; + PyObject *x_longitude = NULL; + PyObject *y_latitude = NULL; + PyObject *z_height = NULL; + + if (!(srid = PyLong_FromUnsignedLong(point3d->srid))) { + goto cleanup; + } + if (!(x_longitude = PyFloat_FromDouble(point3d->x_longitude))) { + goto cleanup; + } + if (!(y_latitude = PyFloat_FromDouble(point3d->y_latitude))) { + goto cleanup; + } + if (!(z_height = PyFloat_FromDouble(point3d->z_height))) { + goto cleanup; + } + if (!(tuple = PyTuple_New(4))) { + goto cleanup; + } + + PyTuple_SET_ITEM(tuple, 0, srid); + PyTuple_SET_ITEM(tuple, 1, x_longitude); + PyTuple_SET_ITEM(tuple, 2, y_latitude); + PyTuple_SET_ITEM(tuple, 3, z_height); + return tuple; + +cleanup: + Py_XDECREF(tuple); + Py_XDECREF(srid); + Py_XDECREF(x_longitude); + Py_XDECREF(y_latitude); + Py_XDECREF(z_height); + return NULL; +} + +static PyObject *point3d_richcompare(Point3DObject *lhs, PyObject *rhs, int op) { + PyObject *tlhs = NULL; + PyObject *trhs = NULL; + PyObject *ret = NULL; + + if (Py_TYPE(rhs) == &Point3DType) { + if (!(tlhs = point3d_astuple(lhs))) { + goto exit; + } + if (!(trhs = point3d_astuple((Point3DObject *)rhs))) { + goto exit; + } + ret = PyObject_RichCompare(tlhs, trhs, op); + } else { + Py_INCREF(Py_False); + ret = Py_False; + } + +exit: + Py_XDECREF(tlhs); + Py_XDECREF(trhs); + return ret; +} + +int point3d_init(Point3DObject *point3d, PyObject *args, PyObject *kwargs) { + uint16_t srid = 0; + double x_longitude = 0; + double y_latitude = 0; + double z_height = 0; + static char *kwlist[] = {"", "", "", "", NULL}; + // https://docs.python.org/3/c-api/arg.html#numbers + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hddd", kwlist, &srid, &x_longitude, &y_latitude, &z_height)) { + return -1; + } + + point3d->srid = srid; + point3d->x_longitude = x_longitude; + point3d->y_latitude = y_latitude; + point3d->z_height = z_height; + return 0; +} + +PyDoc_STRVAR(Point3DType_srid_doc, + "Point3D srid (a unique identifier associated with a specific coordinate system, tolerance, and resolution)."); +PyDoc_STRVAR(Point3DType_x_longitude_doc, "Point3D x or longitude value."); +PyDoc_STRVAR(Point3DType_y_latitude_doc, "Point3D y or latitude value."); +PyDoc_STRVAR(Point3DType_z_height_doc, "Point3D z or height value."); +static PyMemberDef point3d_members[] = { + {"srid", T_USHORT, offsetof(Point3DObject, srid), READONLY, Point3DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point3DObject, x_longitude), READONLY, Point3DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point3DObject, y_latitude), READONLY, Point3DType_y_latitude_doc}, + {"z_height", T_DOUBLE, offsetof(Point3DObject, z_height), READONLY, Point3DType_z_height_doc}, + {NULL}}; + +PyDoc_STRVAR(Point3DType_doc, + "A Point3D object."); +// clang-format off +PyTypeObject Point3DType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "mgclient.Point3D", + .tp_basicsize = sizeof(Point3DObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)point3d_dealloc, + .tp_repr = (reprfunc)point3d_repr, + .tp_str = (reprfunc)point3d_str, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = Point3DType_doc, + .tp_richcompare = (richcmpfunc)point3d_richcompare, + .tp_members = point3d_members, + .tp_init = (initproc)point3d_init, + .tp_new = PyType_GenericNew +}; // clang-format on + +#undef CHECK_ATTRIBUTE diff --git a/test/test_glue.py b/test/test_glue.py index b5691581..d7b89513 100644 --- a/test/test_glue.py +++ b/test/test_glue.py @@ -281,4 +281,17 @@ def test_point2d_send(memgraph_connection): assert result == [(mgclient.Point2D(7203, 0, 1),)] -# TODO(gitbuda): Add Point3D test. +def test_point3d_receive(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN point({x:0, y:1, z:2}) AS point;") + result = cursor.fetchall() + assert result == [(mgclient.Point3D(9757, 0, 1, 2),)] + + +def test_point3d_send(memgraph_connection): + conn = memgraph_connection + cursor = conn.cursor() + cursor.execute("RETURN $value", {"value": mgclient.Point3D(9757, 0, 1, 2)}) + result = cursor.fetchall() + assert result == [(mgclient.Point3D(9757, 0, 1, 2),)] diff --git a/test/test_types.py b/test/test_types.py index 8dd4e967..6d94a2c4 100644 --- a/test/test_types.py +++ b/test/test_types.py @@ -68,4 +68,20 @@ def test_point2d(): def test_point3d(): - assert False + p1 = mgclient.Point3D(0, 1, 2, 3); + assert p1.srid == 0 + assert p1.x_longitude == 1 + assert p1.y_latitude == 2 + assert p1.z_height == 3 + assert str(p1) == "Point3D({ srid=0, x_longitude=1.000000, y_latitude=2.000000, z_height=3.000000 })" + assert repr(p1).startswith(" Date: Sat, 17 Aug 2024 11:00:44 +0200 Subject: [PATCH 11/11] Fix formatting --- src/glue.c | 6 +++-- src/types.c | 70 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/glue.c b/src/glue.c index d8f15e84..166fc615 100644 --- a/src/glue.c +++ b/src/glue.c @@ -608,13 +608,15 @@ mg_duration *py_delta_to_mg_duration(PyObject *obj) { mg_point_2d *py_point2d_to_mg_point_2d(PyObject *point_object) { assert(Py_TYPE(point_object) == &Point2DType); Point2DObject *py_point2d = (Point2DObject *)point_object; - return mg_point_2d_make(py_point2d->srid, py_point2d->x_longitude, py_point2d->y_latitude); + return mg_point_2d_make(py_point2d->srid, py_point2d->x_longitude, + py_point2d->y_latitude); } mg_point_3d *py_point3d_to_mg_point_3d(PyObject *point_object) { assert(Py_TYPE(point_object) == &Point3DType); Point3DObject *py_point3d = (Point3DObject *)point_object; - return mg_point_3d_make(py_point3d->srid, py_point3d->x_longitude, py_point3d->y_latitude, py_point3d->z_height); + return mg_point_3d_make(py_point3d->srid, py_point3d->x_longitude, + py_point3d->y_latitude, py_point3d->z_height); } mg_value *py_object_to_mg_value(PyObject *object) { diff --git a/src/types.c b/src/types.c index a018b178..c7d515f2 100644 --- a/src/types.c +++ b/src/types.c @@ -579,15 +579,21 @@ static void point2d_dealloc(Point2DObject *point2d) { static PyObject *point2d_repr(Point2DObject *point2d) { char buffer[256]; - snprintf(buffer, sizeof(buffer), "<%s(srid=%u, x_longitude=%f, y_latitude=%f) at %p>", Py_TYPE(point2d)->tp_name, point2d->srid, point2d->x_longitude, point2d->y_latitude, point2d); + snprintf(buffer, sizeof(buffer), + "<%s(srid=%u, x_longitude=%f, y_latitude=%f) at %p>", + Py_TYPE(point2d)->tp_name, point2d->srid, point2d->x_longitude, + point2d->y_latitude, point2d); return PyUnicode_FromFormat("%s", buffer); } static PyObject *point2d_str(Point2DObject *point2d) { - // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double values. + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double + // values. // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value char buffer[256]; - snprintf(buffer, sizeof(buffer), "Point2D({ srid=%u, x_longitude=%f, y_latitude=%f })", point2d->srid, point2d->x_longitude, point2d->y_latitude); + snprintf(buffer, sizeof(buffer), + "Point2D({ srid=%u, x_longitude=%f, y_latitude=%f })", point2d->srid, + point2d->x_longitude, point2d->y_latitude); return PyUnicode_FromFormat("%s", buffer); } @@ -624,7 +630,8 @@ static PyObject *point2d_astuple(Point2DObject *point2d) { return NULL; } -static PyObject *point2d_richcompare(Point2DObject *lhs, PyObject *rhs, int op) { +static PyObject *point2d_richcompare(Point2DObject *lhs, PyObject *rhs, + int op) { PyObject *tlhs = NULL; PyObject *trhs = NULL; PyObject *ret = NULL; @@ -654,7 +661,8 @@ int point2d_init(Point2DObject *point2d, PyObject *args, PyObject *kwargs) { double y_latitude = 0; static char *kwlist[] = {"", "", "", NULL}; // https://docs.python.org/3/c-api/arg.html#numbers - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hdd", kwlist, &srid, &x_longitude, &y_latitude)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hdd", kwlist, &srid, + &x_longitude, &y_latitude)) { return -1; } @@ -665,17 +673,20 @@ int point2d_init(Point2DObject *point2d, PyObject *args, PyObject *kwargs) { } PyDoc_STRVAR(Point2DType_srid_doc, - "Point2D srid (a unique identifier associated with a specific coordinate system, tolerance, and resolution)."); + "Point2D srid (a unique identifier associated with a specific " + "coordinate system, tolerance, and resolution)."); PyDoc_STRVAR(Point2DType_x_longitude_doc, "Point2D x or longitude value."); PyDoc_STRVAR(Point2DType_y_latitude_doc, "Point2D y or latitude value."); static PyMemberDef point2d_members[] = { - {"srid", T_USHORT, offsetof(Point2DObject, srid), READONLY, Point2DType_srid_doc}, - {"x_longitude", T_DOUBLE, offsetof(Point2DObject, x_longitude), READONLY, Point2DType_x_longitude_doc}, - {"y_latitude", T_DOUBLE, offsetof(Point2DObject, y_latitude), READONLY, Point2DType_y_latitude_doc}, + {"srid", T_USHORT, offsetof(Point2DObject, srid), READONLY, + Point2DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point2DObject, x_longitude), READONLY, + Point2DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point2DObject, y_latitude), READONLY, + Point2DType_y_latitude_doc}, {NULL}}; -PyDoc_STRVAR(Point2DType_doc, - "A Point2D object."); +PyDoc_STRVAR(Point2DType_doc, "A Point2D object."); // clang-format off PyTypeObject Point2DType = { PyVarObject_HEAD_INIT(NULL, 0) @@ -700,15 +711,22 @@ static void point3d_dealloc(Point3DObject *point3d) { static PyObject *point3d_repr(Point3DObject *point3d) { char buffer[256]; - snprintf(buffer, sizeof(buffer), "<%s(srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f) at %p>", Py_TYPE(point3d)->tp_name, point3d->srid, point3d->x_longitude, point3d->y_latitude, point3d->z_height, point3d); + snprintf(buffer, sizeof(buffer), + "<%s(srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f) at %p>", + Py_TYPE(point3d)->tp_name, point3d->srid, point3d->x_longitude, + point3d->y_latitude, point3d->z_height, point3d); return PyUnicode_FromFormat("%s", buffer); } static PyObject *point3d_str(Point3DObject *point3d) { - // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double values. + // NOTE: Somehow, PyUnicode_FromFormat doesn't suppord formatting double + // values. // https://stackoverflow.com/questions/1701055/what-is-the-maximum-length-in-chars-needed-to-represent-any-double-value char buffer[256]; - snprintf(buffer, sizeof(buffer), "Point3D({ srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f })", point3d->srid, point3d->x_longitude, point3d->y_latitude, point3d->z_height); + snprintf(buffer, sizeof(buffer), + "Point3D({ srid=%u, x_longitude=%f, y_latitude=%f, z_height=%f })", + point3d->srid, point3d->x_longitude, point3d->y_latitude, + point3d->z_height); return PyUnicode_FromFormat("%s", buffer); } @@ -751,7 +769,8 @@ static PyObject *point3d_astuple(Point3DObject *point3d) { return NULL; } -static PyObject *point3d_richcompare(Point3DObject *lhs, PyObject *rhs, int op) { +static PyObject *point3d_richcompare(Point3DObject *lhs, PyObject *rhs, + int op) { PyObject *tlhs = NULL; PyObject *trhs = NULL; PyObject *ret = NULL; @@ -782,7 +801,8 @@ int point3d_init(Point3DObject *point3d, PyObject *args, PyObject *kwargs) { double z_height = 0; static char *kwlist[] = {"", "", "", "", NULL}; // https://docs.python.org/3/c-api/arg.html#numbers - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hddd", kwlist, &srid, &x_longitude, &y_latitude, &z_height)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "Hddd", kwlist, &srid, + &x_longitude, &y_latitude, &z_height)) { return -1; } @@ -794,19 +814,23 @@ int point3d_init(Point3DObject *point3d, PyObject *args, PyObject *kwargs) { } PyDoc_STRVAR(Point3DType_srid_doc, - "Point3D srid (a unique identifier associated with a specific coordinate system, tolerance, and resolution)."); + "Point3D srid (a unique identifier associated with a specific " + "coordinate system, tolerance, and resolution)."); PyDoc_STRVAR(Point3DType_x_longitude_doc, "Point3D x or longitude value."); PyDoc_STRVAR(Point3DType_y_latitude_doc, "Point3D y or latitude value."); PyDoc_STRVAR(Point3DType_z_height_doc, "Point3D z or height value."); static PyMemberDef point3d_members[] = { - {"srid", T_USHORT, offsetof(Point3DObject, srid), READONLY, Point3DType_srid_doc}, - {"x_longitude", T_DOUBLE, offsetof(Point3DObject, x_longitude), READONLY, Point3DType_x_longitude_doc}, - {"y_latitude", T_DOUBLE, offsetof(Point3DObject, y_latitude), READONLY, Point3DType_y_latitude_doc}, - {"z_height", T_DOUBLE, offsetof(Point3DObject, z_height), READONLY, Point3DType_z_height_doc}, + {"srid", T_USHORT, offsetof(Point3DObject, srid), READONLY, + Point3DType_srid_doc}, + {"x_longitude", T_DOUBLE, offsetof(Point3DObject, x_longitude), READONLY, + Point3DType_x_longitude_doc}, + {"y_latitude", T_DOUBLE, offsetof(Point3DObject, y_latitude), READONLY, + Point3DType_y_latitude_doc}, + {"z_height", T_DOUBLE, offsetof(Point3DObject, z_height), READONLY, + Point3DType_z_height_doc}, {NULL}}; -PyDoc_STRVAR(Point3DType_doc, - "A Point3D object."); +PyDoc_STRVAR(Point3DType_doc, "A Point3D object."); // clang-format off PyTypeObject Point3DType = { PyVarObject_HEAD_INIT(NULL, 0)