Skip to content

Add C++ Backend for BFS and Use std::variant for Graph Node Data #684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ jobs:
python -m pip install -r docs/requirements.txt

- name: Build package
env:
MACOSX_DEPLOYMENT_TARGET: 11.0
run: |
CXXFLAGS="-std=c++17" python scripts/build/install.py

Expand Down
2 changes: 1 addition & 1 deletion pydatastructs/graphs/_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@

include_dir = os.path.abspath(os.path.join(project, 'utils', '_backend', 'cpp'))

extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir])]
extensions = [Extension(graph, sources=graph_sources,include_dirs=[include_dir], language="c++", extra_compile_args=["-std=c++17"])]
65 changes: 56 additions & 9 deletions pydatastructs/utils/_backend/cpp/AdjacencyListGraphNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
#include <string>
#include <unordered_map>
#include "GraphNode.hpp"
#include <variant>

extern PyTypeObject AdjacencyListGraphNodeType;

typedef struct {
PyObject_HEAD
std::string name;
PyObject* data;
std::variant<std::monostate, int64_t, double, std::string> data;
DataType data_type;
std::unordered_map<std::string, PyObject*> adjacent;
} AdjacencyListGraphNode;

static void AdjacencyListGraphNode_dealloc(AdjacencyListGraphNode* self) {
Py_XDECREF(self->data);
for (auto& pair : self->adjacent) {
Py_XDECREF(pair.second);
}
Expand All @@ -30,6 +31,9 @@ static PyObject* AdjacencyListGraphNode_new(PyTypeObject* type, PyObject* args,
if (!self) return NULL;
new (&self->adjacent) std::unordered_map<std::string, PyObject*>();
new (&self->name) std::string();
new (&self->data) std::variant<std::monostate, int64_t, double, std::string>();
self->data_type = DataType::None;
self->data = std::monostate{};

static char* kwlist[] = { "name", "data", "adjacency_list", NULL };
const char* name;
Expand All @@ -42,8 +46,24 @@ static PyObject* AdjacencyListGraphNode_new(PyTypeObject* type, PyObject* args,
}

self->name = std::string(name);
Py_INCREF(data);
self->data = data;

if (data == Py_None) {
self->data_type = DataType::None;
self->data = std::monostate{};
} else if (PyLong_Check(data)) {
self->data_type = DataType::Int;
self->data = static_cast<int64_t>(PyLong_AsLongLong(data));
} else if (PyFloat_Check(data)) {
self->data_type = DataType::Double;
self->data = PyFloat_AsDouble(data);
} else if (PyUnicode_Check(data)) {
const char* str = PyUnicode_AsUTF8(data);
self->data_type = DataType::String;
self->data = std::string(str);
} else {
PyErr_SetString(PyExc_TypeError, "Unsupported data type. Must be int, float, str, or None.");
return NULL;
}

if (PyList_Check(adjacency_list)) {
Py_ssize_t size = PyList_Size(adjacency_list);
Expand Down Expand Up @@ -127,14 +147,41 @@ static int AdjacencyListGraphNode_set_name(AdjacencyListGraphNode* self, PyObjec
}

static PyObject* AdjacencyListGraphNode_get_data(AdjacencyListGraphNode* self, void* closure) {
Py_INCREF(self->data);
return self->data;
switch (self->data_type) {
case DataType::Int:
return PyLong_FromLongLong(std::get<int64_t>(self->data));
case DataType::Double:
return PyFloat_FromDouble(std::get<double>(self->data));
case DataType::String:
return PyUnicode_FromString(std::get<std::string>(self->data).c_str());
case DataType::None:
default:
Py_RETURN_NONE;
}
}

static int AdjacencyListGraphNode_set_data(AdjacencyListGraphNode* self, PyObject* value, void* closure) {
Py_XDECREF(self->data);
Py_INCREF(value);
self->data = value;
if (value == Py_None) {
self->data_type = DataType::None;
self->data = std::monostate{};
} else if (PyLong_Check(value)) {
self->data_type = DataType::Int;
self->data = static_cast<int64_t>(PyLong_AsLongLong(value));
} else if (PyFloat_Check(value)) {
self->data_type = DataType::Double;
self->data = PyFloat_AsDouble(value);
} else if (PyUnicode_Check(value)) {
const char* str = PyUnicode_AsUTF8(value);
if (!str) {
PyErr_SetString(PyExc_ValueError, "Invalid UTF-8 string.");
return -1;
}
self->data_type = DataType::String;
self->data = std::string(str);
} else {
PyErr_SetString(PyExc_TypeError, "Unsupported data type. Must be int, float, str, or None.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other cases set to normal PyObject data type.

return -1;
}
return 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ typedef struct {
} AdjacencyMatrixGraphNode;

static void AdjacencyMatrixGraphNode_dealloc(AdjacencyMatrixGraphNode* self){
Py_XDECREF(self->super.data);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is PyObject then keep this inside if.

Py_TYPE(self)->tp_free(reinterpret_cast<PyTypeObject*>(self));
}

Expand Down
117 changes: 98 additions & 19 deletions pydatastructs/utils/_backend/cpp/GraphNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string>
#include <variant>

enum class DataType {
None,
Int,
Double,
String
};

typedef struct {
PyObject_HEAD
std::string name;
PyObject* data;
std::variant<std::monostate, int64_t, double, std::string> data;
DataType data_type;
} GraphNode;

static void GraphNode_dealloc(GraphNode* self){
Py_XDECREF(self->data);
Py_TYPE(self)->tp_free(reinterpret_cast<PyTypeObject*>(self));
}

static PyObject* GraphNode_new(PyTypeObject* type, PyObject* args, PyObject* kwds){
GraphNode* self;
self = reinterpret_cast<GraphNode*>(type->tp_alloc(type,0));
new (&self->name) std::string();
new (&self->data) std::variant<std::monostate, int64_t, double, std::string>();
self->data_type = DataType::None;
if (!self) return NULL;

static char* kwlist[] = { "name", "data", NULL };
Expand All @@ -32,54 +42,123 @@ static PyObject* GraphNode_new(PyTypeObject* type, PyObject* args, PyObject* kwd
}

self->name = std::string(name);
Py_INCREF(data);
self->data = data;

if (data == Py_None) {
self->data = std::monostate{};
self->data_type = DataType::None;
} else if (PyLong_Check(data)) {
self->data = static_cast<int64_t>(PyLong_AsLongLong(data));
self->data_type = DataType::Int;
} else if (PyFloat_Check(data)) {
self->data = PyFloat_AsDouble(data);
self->data_type = DataType::Double;
} else if (PyUnicode_Check(data)) {
const char* s = PyUnicode_AsUTF8(data);
self->data = std::string(s);
self->data_type = DataType::String;
} else {
PyErr_SetString(PyExc_TypeError, "data must be int, float, str, or None");
return NULL;
}

return reinterpret_cast<PyObject*>(self);
}

static PyObject* GraphNode_str(GraphNode* self) {
return PyUnicode_FromString(("('" + self->name + "', " + PyUnicode_AsUTF8(PyObject_Str(self->data)) + ")").c_str());
std::string repr = "('" + self->name + "', ";

switch (self->data_type) {
case DataType::None:
repr += "None";
break;
case DataType::Int:
repr += std::to_string(std::get<int64_t>(self->data));
break;
case DataType::Double:
repr += std::to_string(std::get<double>(self->data));
break;
case DataType::String:
repr += "'" + std::get<std::string>(self->data) + "'";
break;
}
repr += ")";
return PyUnicode_FromString(repr.c_str());
}

static PyObject* GraphNode_get(GraphNode* self, void *closure) {
if (closure == (void*)"name") {
return PyUnicode_FromString(self->name.c_str());
}
if (closure == (void*)"data") {
Py_INCREF(self->data);
return self->data;
} else if (closure == (void*)"data") {
switch (self->data_type) {
case DataType::None:
Py_RETURN_NONE;
case DataType::Int:
return PyLong_FromLongLong(std::get<int64_t>(self->data));
case DataType::Double:
return PyFloat_FromDouble(std::get<double>(self->data));
case DataType::String:
return PyUnicode_FromString(std::get<std::string>(self->data).c_str());
}
}
Py_RETURN_NONE;
}

static int GraphNode_set(GraphNode* self, PyObject *value, void *closure) {
if (value == NULL) {
PyErr_SetString(PyExc_ValueError, "value is NULL");
if (!value) {
PyErr_SetString(PyExc_ValueError, "Cannot delete attributes");
return -1;
}

if (closure == (void*)"name") {
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError, "value to be set must be a string");
PyErr_SetString(PyExc_TypeError, "name must be a string");
return -1;
}
self->name = PyUnicode_AsUTF8(value);
}
else if (closure == (void*)"data") {
PyObject *tmp = self->data;
Py_INCREF(value);
self->data = value;
Py_DECREF(tmp);
}
else {
if (value == Py_None) {
self->data = std::monostate{};
self->data_type = DataType::None;
} else if (PyLong_Check(value)) {
self->data = static_cast<int64_t>(PyLong_AsLongLong(value));
self->data_type = DataType::Int;
} else if (PyFloat_Check(value)) {
self->data = PyFloat_AsDouble(value);
self->data_type = DataType::Double;
} else if (PyUnicode_Check(value)) {
self->data = std::string(PyUnicode_AsUTF8(value));
self->data_type = DataType::String;
} else {
PyErr_SetString(PyExc_TypeError, "data must be int, float, str, or None");
return -1;
}
} else {
PyErr_SetString(PyExc_AttributeError, "Unknown attribute");
return -1;
}

return 0;
}

static PyGetSetDef GraphNode_getsetters[] = {
{
const_cast<char*>("name"),
reinterpret_cast<getter>(GraphNode_get),
reinterpret_cast<setter>(GraphNode_set),
const_cast<char*>("name"),
reinterpret_cast<void*>(const_cast<char*>("name"))
},
{
const_cast<char*>("data"),
reinterpret_cast<getter>(GraphNode_get),
reinterpret_cast<setter>(GraphNode_set),
const_cast<char*>("data"),
reinterpret_cast<void*>(const_cast<char*>("data"))
},
{nullptr}
};

static PyTypeObject GraphNodeType = {
/* tp_name */ PyVarObject_HEAD_INIT(NULL, 0) "GraphNode",
/* tp_basicsize */ sizeof(GraphNode),
Expand Down Expand Up @@ -109,7 +188,7 @@ static PyTypeObject GraphNodeType = {
/* tp_iternext */ 0,
/* tp_methods */ 0,
/* tp_members */ 0,
/* tp_getset */ 0,
/* tp_getset */ GraphNode_getsetters,
/* tp_base */ &PyBaseObject_Type,
/* tp_dict */ 0,
/* tp_descr_get */ 0,
Expand Down
23 changes: 14 additions & 9 deletions pydatastructs/utils/_extensions.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
from setuptools import Extension
import os
import sys

project = 'pydatastructs'

module = 'utils'

backend = '_backend'

cpp = 'cpp'

nodes = '.'.join([project, module, backend, cpp, '_nodes'])
nodes_sources = ['/'.join([project, module, backend, cpp,
'nodes.cpp'])]
nodes_sources = [os.path.join(project, module, backend, cpp, 'nodes.cpp')]

graph_utils = '.'.join([project, module, backend, cpp, '_graph_utils'])
graph_utils_sources = ['/'.join([project, module, backend, cpp,
'graph_utils.cpp'])]
graph_utils_sources = [os.path.join(project, module, backend, cpp, 'graph_utils.cpp')]

extra_compile_args = ["-std=c++17"]

if sys.platform == "darwin":
extra_compile_args.append("-mmacosx-version-min=10.13")
elif sys.platform == "win32":
extra_compile_args = ["/std:c++17"]

extensions = [
Extension(nodes, sources=nodes_sources),
Extension(graph_utils, sources = graph_utils_sources)
Extension(nodes, sources=nodes_sources, language="c++", extra_compile_args=extra_compile_args),
Extension(graph_utils, sources=graph_utils_sources, language="c++", extra_compile_args=extra_compile_args),
]
Loading