Skip to content
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

Fix h.Vector[0] : TypeError: type 'hoc.Vector' is not subscriptable #2862

Merged
merged 19 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions src/nrnpython/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ set(nrnpython_lib_list)
set(NRNPYTHON_FILES_LIST
nrnpython.cpp
nrnpy_hoc.cpp
nrn_metaclass.cpp
nrnpy_nrn.cpp
nrnpy_p2h.cpp
grids.cpp
Expand Down
75 changes: 75 additions & 0 deletions src/nrnpython/nrn_metaclass.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Derived from
// https://github.com/python/cpython/pull/93012#issuecomment-1138876646

#include <Python.h>

PyObject* nrn_type_from_metaclass(PyTypeObject* metaclass,
PyObject* mod,
PyType_Spec* spec,
PyObject* base) {
#if PY_VERSION_HEX >= 0x030C0000
// Life is good, PyType_FromMetaclass() is available
PyObject* result = PyType_FromMetaclass(metaclass, mod, spec, base);
if (!result) {
// fail("nanobind::detail::nb_type_new(\"%s\"): type construction failed!", t->name);
return result;
}
#else
/* The fallback code below is cursed. It provides an alternative when
PyType_FromMetaclass() is not available we are furthermore *not*
targeting the stable ABI interface. It calls PyType_FromSpec() to create
a tentative type, copies its contents into a larger type with a
different metaclass, then lets the original type expire. */

PyObject* temp = PyType_FromSpecWithBases(spec, base); // since 3.3
Py_XINCREF(temp);
PyHeapTypeObject* temp_ht = (PyHeapTypeObject*) temp;
PyTypeObject* temp_tp = &temp_ht->ht_type;

Py_INCREF(temp_ht->ht_name);
Py_INCREF(temp_ht->ht_qualname);
Py_INCREF(temp_tp->tp_base);
Py_XINCREF(temp_ht->ht_slots);

PyObject* result = PyType_GenericAlloc(metaclass, 0);
if (!temp || !result) {
// fail("nanobind::detail::nb_type_new(\"%s\"): type construction failed!", t->name);
return nullptr;
}
PyHeapTypeObject* ht = (PyHeapTypeObject*) result;
PyTypeObject* tp = &ht->ht_type;

memcpy(ht, temp_ht, sizeof(PyHeapTypeObject));

tp->ob_base.ob_base.ob_type = metaclass;
tp->ob_base.ob_base.ob_refcnt = 1;
tp->ob_base.ob_size = 0;
tp->tp_as_async = &ht->as_async;
tp->tp_as_number = &ht->as_number;
tp->tp_as_sequence = &ht->as_sequence;
tp->tp_as_mapping = &ht->as_mapping;
tp->tp_as_buffer = &ht->as_buffer;
tp->tp_name = strdup(spec->name); // name_copy;
tp->tp_flags = spec->flags | Py_TPFLAGS_HEAPTYPE;

tp->tp_dict = tp->tp_bases = tp->tp_mro = tp->tp_cache = tp->tp_subclasses = tp->tp_weaklist =
nullptr;
ht->ht_cached_keys = nullptr;
tp->tp_version_tag = 0;

PyType_Ready(tp);
Py_DECREF(temp);

// PyType_FromMetaclass consistently sets the __module__ to 'hoc' (never 'neuron.hoc')
// So rather than use PyModule_GetName or PyObject_GetAttrString(mod, "__name__") just
// explicitly set to 'hoc'
PyObject* module_name = PyUnicode_FromString("hoc");
if (PyObject_SetAttrString(result, "__module__", module_name) < 0) {
Py_DECREF(module_name);
return nullptr;
}
Py_DECREF(module_name);
#endif

return result;
}
130 changes: 117 additions & 13 deletions src/nrnpython/nrnpy_hoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,61 @@ PyTypeObject* hocobject_type;

static PyObject* hocobj_call(PyHocObject* self, PyObject* args, PyObject* kwrds);

typedef struct {
PyTypeObject head;
Symbol* sym;
} hocclass;

static int hocclass_init(hocclass* cls, PyObject* args, PyObject* kwds) {
if (PyType_Type.tp_init((PyObject*) cls, args, kwds) < 0) {
return -1;
}
return 0;
}

static PyObject* pytype_getname(PyTypeObject* pto) {
#if PY_VERSION_HEX >= 0x030B0000 // since python-3.11
return PyType_GetName(pto);
#else
return PyObject_GetAttrString((PyObject*) pto, "__name__");
#endif
}

static PyObject* hocclass_getitem(PyObject* self, Py_ssize_t ix) {
hocclass* hclass = (hocclass*) self;
Symbol* sym = hclass->sym;
assert(sym);

assert(sym->type == TEMPLATE);
hoc_Item *q, *ql = sym->u.ctemplate->olist;
Object* ob;
ITERATE(q, ql) {
ob = OBJ(q);
if (ob->index == ix) {
return nrnpy_ho2po(ob);
}
}
char e[200];
Sprintf(e, "%s[%ld] instance does not exist", sym->name, ix);
PyErr_SetString(PyExc_IndexError, e);
return NULL;
}

// Note use of slots was informed by nanobind (search for nb_meta)

static PyType_Slot hocclass_slots[] = {{Py_tp_base, nullptr}, // &PyType_Type : not obvious why it
// must be set at runtime
{Py_tp_init, (void*) hocclass_init},
{Py_sq_item, (void*) hocclass_getitem},
{0, NULL}};

static PyType_Spec hocclass_spec = {"hoc.HocClass",
0, // .basicsize fill later
0, // .itemsize
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE,
hocclass_slots};


bool nrn_chk_data_handle(const neuron::container::data_handle<double>& pd) {
if (pd) {
return true;
Expand Down Expand Up @@ -919,7 +974,6 @@ static PyObject* hocobj_getsec(Symbol* sym) {

// leave pointer on stack ready for get/set final
static void eval_component(PyHocObject* po, int ix) {
int j;
hoc_push_object(po->ho_);
hocobj_pushtop(po, 0, ix);
component(po);
Expand Down Expand Up @@ -2263,7 +2317,6 @@ static PyObject* setpointer(PyObject* self, PyObject* args) {
PyObject *ref, *name, *pp;
if (PyArg_ParseTuple(args, "O!OO", hocobject_type, &ref, &name, &pp) == 1) {
PyHocObject* href = (PyHocObject*) ref;
double** ppd = 0;
if (href->type_ != PyHoc::HocScalarPtr) {
goto done;
}
Expand Down Expand Up @@ -2960,7 +3013,7 @@ static PyObject* hocpickle_reduce_safe(PyObject* self, PyObject* args) {
static PyObject* hocpickle_setstate(PyObject* self, PyObject* args) {
BYTEHEADER
int version = -1;
int size = -1;
int size = 0;
PyObject* rawdata = NULL;
PyObject* endian_data;
PyHocObject* pho = (PyHocObject*) self;
Expand Down Expand Up @@ -3002,7 +3055,7 @@ static PyObject* hocpickle_setstate(PyObject* self, PyObject* args) {
Py_DECREF(rawdata);
return NULL;
}
if (len != size * sizeof(double)) {
if (len != Py_ssize_t(size * sizeof(double))) {
PyErr_SetString(PyExc_ValueError, "buffer size does not match array size");
Py_DECREF(rawdata);
return NULL;
Expand Down Expand Up @@ -3331,6 +3384,11 @@ static PyType_Spec obj_spec_from_name(const char* name) {
};
}

extern PyObject* nrn_type_from_metaclass(PyTypeObject* meta,
PyObject* mod,
PyType_Spec* spec,
PyObject* bases);

extern "C" NRN_EXPORT PyObject* nrnpy_hoc() {
PyObject* m;
PyObject* bases;
Expand All @@ -3357,25 +3415,71 @@ extern "C" NRN_EXPORT PyObject* nrnpy_hoc() {
}
m = PyModule_Create(&hocmodule);
assert(m);

Symbol* s = NULL;
spec = obj_spec_from_name("hoc.HocObject");
hocobject_type = (PyTypeObject*) PyType_FromSpec(&spec);
if (PyType_Ready(hocobject_type) < 0)
goto fail;
PyModule_AddObject(m, "HocObject", (PyObject*) hocobject_type);
hocobject_type = (PyTypeObject*) nrn_type_from_metaclass(&PyType_Type, m, &spec, nullptr);
if (hocobject_type == NULL) {
return NULL;
}
if (PyModule_AddObject(m, "HocObject", (PyObject*) hocobject_type) < 0) {
return NULL;
}

hocclass_slots[0].pfunc = (PyObject*) &PyType_Type;
// I have no idea what is going on here. If use
// hocclass_spec.basicsize = sizeof(hocclass);
// then get error
// TypeError: tp_basicsize for type 'hoc.HocClass' (424) is
// too small for base 'type' (920)

#if 1
hocclass_spec.basicsize = PyType_Type.tp_basicsize + sizeof(Symbol*);
// and what about alignment?
// recommended by chatgpt
size_t alignment = alignof(Symbol*);
size_t remainder = hocclass_spec.basicsize % alignment;
if (remainder != 0) {
hocclass_spec.basicsize += alignment - remainder;
// printf("aligned hocclass_spec.basicsize = %d\n", hocclass_spec.basicsize);
}
#else
// chatgpt agrees that the following suggestion
// https://github.com/neuronsimulator/nrn/pull/2862/files#r1749797713
// is equivalent to the '#if 1' fragment above. However the above
// may "ensures portability and correctness across different architectures."
hocclass_spec.basicsize = PyType_Type.tp_basicsize + sizeof(hocclass) - sizeof(PyTypeObject);
#endif

PyObject* custom_hocclass = PyType_FromSpec(&hocclass_spec);
if (custom_hocclass == NULL) {
return NULL;
}
if (PyModule_AddObject(m, "HocClass", custom_hocclass) < 0) {
return NULL;
}


bases = PyTuple_Pack(1, hocobject_type);
Py_INCREF(bases);
for (auto name: py_exposed_classes) {
// TODO: obj_spec_from_name needs a hoc. prepended
exposed_py_type_names.push_back(std::string("hoc.") + name);
spec = obj_spec_from_name(exposed_py_type_names.back().c_str());
pto = (PyTypeObject*) PyType_FromSpecWithBases(&spec, bases);
sym_to_type_map[hoc_lookup(name)] = pto;
type_to_sym_map[pto] = hoc_lookup(name);
if (PyType_Ready(pto) < 0)
pto = (PyTypeObject*)
nrn_type_from_metaclass((PyTypeObject*) custom_hocclass, m, &spec, bases);
hocclass* hclass = (hocclass*) pto;
hclass->sym = hoc_lookup(name);
// printf("%s hocclass pto->tp_basicsize = %zd sizeof(*pto)=%zd\n",
// hclass->sym->name, pto->tp_basicsize, sizeof(*pto));
sym_to_type_map[hclass->sym] = pto;
type_to_sym_map[pto] = hclass->sym;
if (PyType_Ready(pto) < 0) {
goto fail;
PyModule_AddObject(m, name, (PyObject*) pto);
}
if (PyModule_AddObject(m, name, (PyObject*) pto) < 0) {
return NULL;
}
}
Py_DECREF(bases);

Expand Down
50 changes: 50 additions & 0 deletions test/hoctests/tests/test_hoc_class_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from neuron import h
import neuron
import re

h(
"""
begintemplate A
proc init() {
x = 2
}
endtemplate A
"""
)


def extract_index(s):
match = re.search(r"\[(\d+)\]", s)
if match:
return int(match.group(1))
else:
raise ValueError("String does not match the expected format")


def test1():
v = h.Vector()
assert type(v) is h.Vector
assert type(type(v)) is neuron.hoc.HocClass
assert type(type(type(v))) is type
assert type(neuron.hoc.HocObject) is type
assert type(h.sin) is neuron.hoc.HocObject
assert type(h) is neuron.hoc.HocObject
assert bool(v) is False
vstr = "h." + str(v)
print(vstr)
exec(vstr + ".resize(10)")
assert len(v) == 10
assert bool(v) is True
ix = extract_index(str(v))
assert h.Vector[ix] == v
assert type(v).__module__ == "hoc"


def test2():
a = h.A()
assert h.A[0] == a


if __name__ == "__main__":
test1()
test2()
Loading