diff --git a/docs/python/index.rst b/docs/python/index.rst index 6064efe542..1817e3b993 100755 --- a/docs/python/index.rst +++ b/docs/python/index.rst @@ -55,6 +55,7 @@ Basic Programming programming/dynamiccode.rst programming/projectmanagement.rst programming/internals.rst + programming/neuron_classes.rst programming/hoc-from-python.rst Model Specification diff --git a/docs/python/programming/neuron_classes.rst b/docs/python/programming/neuron_classes.rst new file mode 100644 index 0000000000..1a5db25d3e --- /dev/null +++ b/docs/python/programming/neuron_classes.rst @@ -0,0 +1,87 @@ +NEURON Python Classes and Objects +================================= + +NEURON exposes its internal objects and hoc templates as Python objects via an automatic +conversion layer, effectively making all entities from the HOC stack available to a Python program. + +There are basically two main objects which expose most Neuron entities. The first is `hoc` which +exposes a number of internal established classes and functions. + +.. code-block:: + python + + >>> from neuron import hoc + >>> hoc. + hoc.List( + hoc.SectionList( + hoc.SectionRef( + hoc.Vector( + ... + +However, for *dynamic* entities NEURON provides the `h` gateway object. It gives access to internal +classes (templates) and objects, even if they were just created. E.g.: + +.. code-block:: + python + + >>> from neuron import h + >>> # Create objects in the hoc stack + >>> h("objref vec") + >>> h("vec = new Vector(5, 1)") + >>> # Access to objects + >>> h.vec.as_numpy() + array([1., 1., 1., 1., 1.]) + >>> + >>> # Access to exposed types + >>> vv = h.Vector(5, 2) + >>> vv.as_numpy() + array([1., 1., 1., 1., 1.]) + +This is particularly useful as NEURON can dynamically load libraries with more functions and classes. + +Class Hierarchy +--------------- + +All NEURON's internal interpreter objects are instances of a global top-level type: `HocObject`. +Until very recently they were considered direct instances, without any intermediate hierarchy. + +With #1858 Hoc classes are now associated with actual Python types, created dynamically. Such +change enables type instances to be properly recognized as such, respecting e.g. `isinstance()` +predicates and subclassing. + +.. code-block:: + python + + >>> isinstance(hoc.Vector, type) + True + >>> v = h.Vector() + >>> isinstance(v, hoc.HocObject) + True + >>> isinstance(v, hoc.Vector) + True + >>> type(v) is hoc.Vector # direct subclass + True + >>>isinstance(v, hoc.Deck) # Not instance of other class + False + +Subclasses are also recognized properly. For creating them please inherit from `HocBaseObject` +with `hoc_type` given as argument. E.g.: + +.. code-block:: + python + + >>> class MyStim(neuron.HocBaseObject, hoc_type=h.NetStim): + pass + >>> issubclass(MyStim, hoc.HocObject) + True + >>> issubclass(MyStim, neuron.HocBaseObject) + True + >>> MyStim._hoc_type == h.NetStim + True + >>> stim = MyStim() + >>> isinstance(stim, MyStim) + True + >>> isinstance(stim, h.NetStim) + True + >>> isinstance(stim, h.HocObject) + True diff --git a/share/lib/python/neuron/hclass3.py b/share/lib/python/neuron/hclass3.py index 2ee17f293f..63c3c74389 100644 --- a/share/lib/python/neuron/hclass3.py +++ b/share/lib/python/neuron/hclass3.py @@ -12,6 +12,12 @@ import sys +def _is_hoc_pytype(hoc_type): + return hoc_type is nrn.Section or ( + isinstance(hoc_type, type) and issubclass(hoc_type, hoc.HocObject) + ) + + def assert_not_hoc_composite(cls): """ Asserts that a class is not directly composed of multiple HOC types. @@ -58,8 +64,8 @@ class MyVector(myClassTemplate): omitted the name of the HOC type is used. :deprecated: Inherit from :class:`~neuron.HocBaseObject` instead. """ - if hoc_type == h.Section: - return nrn.Section + if _is_hoc_pytype(hoc_type): + return hoc_type if module_name is None: module_name = __name__ if name is None: @@ -90,23 +96,29 @@ class MyVector(neuron.HocBaseObject, hoc_type=neuron.h.Vector): """ def __init_subclass__(cls, hoc_type=None, **kwargs): - if hoc_type is not None: - if not isinstance(hoc_type, hoc.HocObject): + if _is_hoc_pytype(hoc_type): + cls_name = cls.__name__ + raise TypeError( + f"Using HocBaseObject with {cls_name} is deprecated." + f" Inherit directly from {cls_name} instead." + ) + if hoc_type is None: + if not hasattr(cls, "_hoc_type"): raise TypeError( - f"Class's `hoc_type` {hoc_type} is not a valid HOC type." + "Class keyword argument `hoc_type` is required for HocBaseObjects." ) + elif not isinstance(hoc_type, hoc.HocObject): + raise TypeError(f"Class's `hoc_type` {hoc_type} is not a valid HOC type.") + else: cls._hoc_type = hoc_type - elif not hasattr(cls, "_hoc_type"): - raise TypeError( - "Class keyword argument `hoc_type` is required for HocBaseObjects." - ) + # HOC type classes may not be composed of multiple hoc types assert_not_hoc_composite(cls) - hobj = hoc.HocObject - hbase = HocBaseObject - if _overrides(cls, hobj, "__init__") and not _overrides(cls, hbase, "__new__"): - # Subclasses that override `__init__` must also implement `__new__` to deal - # with the arguments that have to be passed into `HocObject.__new__`. - # See https://github.com/neuronsimulator/nrn/issues/1129 + # Subclasses that override `__init__` must also implement `__new__` to deal + # with the arguments that have to be passed into `HocObject.__new__`. + # See https://github.com/neuronsimulator/nrn/issues/1129 + if _overrides(cls, hoc.HocObject, "__init__") and not _overrides( + cls, HocBaseObject, "__new__" + ): raise TypeError( f"`{cls.__qualname__}` implements `__init__` but misses `__new__`. " + "Class must implement `__new__`" @@ -119,7 +131,7 @@ def __new__(cls, *args, **kwds): # To construct HOC objects within NEURON from the Python interface, we use the # C-extension module `hoc`. `hoc.HocObject.__new__` both creates an internal # representation of the object in NEURON, and hands us back a Python object that - # is linked to that internal representation. The `__new__` functions takes the + # is linked to that internal representation. The `__new__` function takes the # arguments that HOC objects of that type would take, and uses the `hocbase` # keyword argument to determine which type of HOC object to create. The `sec` # keyword argument can be passed along in case the construction of a HOC object diff --git a/share/lib/python/neuron/tests/utils/checkresult.py b/share/lib/python/neuron/tests/utils/checkresult.py index 9c742c2a3e..4a39f434a7 100644 --- a/share/lib/python/neuron/tests/utils/checkresult.py +++ b/share/lib/python/neuron/tests/utils/checkresult.py @@ -1,6 +1,6 @@ import json import math -from neuron import h +from neuron import h, hoc class Chk: @@ -39,7 +39,7 @@ def __call__(self, key, value, tol=0.0): """ if key in self.d: - if type(value) == type(h.Vector): # actually hoc.HocObject + if isinstance(value, hoc.Vector): # Convert to list to keep the `equal` method below simple value = list(value) # Hand-rolled comparison that uses `tol` for arithmetic values @@ -75,7 +75,7 @@ def equal(a, b): assert match else: print("{} added {}".format(self, key)) - if type(value) == type(h.Vector): # actually hoc.HocObject + if isinstance(value, hoc.Vector): self.d[key] = value.to_python() else: self.d[key] = value diff --git a/src/nrnoc/init.cpp b/src/nrnoc/init.cpp index 7a080fefa4..bb5c844e2f 100644 --- a/src/nrnoc/init.cpp +++ b/src/nrnoc/init.cpp @@ -866,13 +866,13 @@ int point_reg_helper(Symbol* s2) { return pointtype++; } -extern void class2oc(const char*, - void* (*cons)(Object*), - void (*destruct)(void*), - Member_func*, - int (*checkpoint)(void**), - Member_ret_obj_func*, - Member_ret_str_func*); +extern void class2oc_base(const char*, + void* (*cons)(Object*), + void (*destruct)(void*), + Member_func*, + int (*checkpoint)(void**), + Member_ret_obj_func*, + Member_ret_str_func*); int point_register_mech(const char** m, @@ -890,7 +890,7 @@ int point_register_mech(const char** m, Symlist* sl; Symbol *s, *s2; nrn_load_name_check(m[1]); - class2oc(m[1], constructor, destructor, fmember, nullptr, nullptr, nullptr); + class2oc_base(m[1], constructor, destructor, fmember, nullptr, nullptr, nullptr); s = hoc_lookup(m[1]); sl = hoc_symlist; hoc_symlist = s->u.ctemplate->symtable; diff --git a/src/nrnpython/nrnpy_hoc.cpp b/src/nrnpython/nrnpy_hoc.cpp index 836ddccceb..6414df11a6 100644 --- a/src/nrnpython/nrnpy_hoc.cpp +++ b/src/nrnpython/nrnpy_hoc.cpp @@ -6,6 +6,8 @@ #include "nrnpy.h" #include "nrnpy_utils.h" #include "nrnpython.h" +#include + #include "nrnwrap_dlfcn.h" #include "ocfile.h" #include "ocjump.h" @@ -20,6 +22,7 @@ #include extern PyTypeObject* psection_type; +extern std::vector py_exposed_classes; #include "parse.hpp" extern void (*nrnpy_sectionlist_helper_)(void*, Object*); @@ -82,6 +85,10 @@ static cTemplate* hoc_vec_template_; static cTemplate* hoc_list_template_; static cTemplate* hoc_sectionlist_template_; +static std::unordered_map sym_to_type_map; +static std::unordered_map type_to_sym_map; +static std::vector exposed_py_type_names; + // typestr returned by Vector.__array_interface__ // byteorder (first element) is modified at import time // to reflect the system byteorder @@ -121,6 +128,7 @@ static PyObject* get_mech_object_ = NULL; static PyObject* nrnpy_rvp_pyobj_callback = NULL; PyTypeObject* hocobject_type; + static PyObject* hocobj_call(PyHocObject* self, PyObject* args, PyObject* kwrds); bool nrn_chk_data_handle(const neuron::container::data_handle& pd) { @@ -192,8 +200,11 @@ static void hocobj_dealloc(PyHocObject* self) { static PyObject* hocobj_new(PyTypeObject* subtype, PyObject* args, PyObject* kwds) { PyObject* subself; + PyObject* base; + PyHocObject* hbase = nullptr; + subself = subtype->tp_alloc(subtype, 0); - // printf("hocobj_new %s %p\n", subtype->tp_name, subself); + // printf("hocobj_new %s %p %p\n", subtype->tp_name, subtype, subself); if (subself == NULL) { return NULL; } @@ -205,37 +216,47 @@ static PyObject* hocobj_new(PyTypeObject* subtype, PyObject* args, PyObject* kwd self->nindex_ = 0; self->type_ = PyHoc::HocTopLevelInterpreter; self->iteritem_ = 0; - if (kwds && PyDict_Check(kwds)) { - PyObject* base = PyDict_GetItemString(kwds, "hocbase"); - if (base) { - int ok = 0; - if (PyObject_TypeCheck(base, hocobject_type)) { - PyHocObject* hbase = (PyHocObject*) base; - if (hbase->type_ == PyHoc::HocFunction && hbase->sym_->type == TEMPLATE) { - // printf("hocobj_new base %s\n", hbase->sym_->name); - // remove the hocbase keyword since hocobj_call only allows - // the "sec" keyword argument - PyDict_DelItemString(kwds, "hocbase"); - PyObject* r = hocobj_call(hbase, args, kwds); - if (!r) { - Py_DECREF(subself); - return NULL; - } - PyHocObject* rh = (PyHocObject*) r; - self->type_ = rh->type_; - self->ho_ = rh->ho_; - hoc_obj_ref(self->ho_); - Py_DECREF(r); - ok = 1; - } - } - if (!ok) { - Py_DECREF(subself); - PyErr_SetString(PyExc_TypeError, "HOC base class not valid"); - return NULL; - } + + // if subtype is a subclass of some NEURON class, then one of its + // tp_mro's is in sym_to_type_map + for (Py_ssize_t i = 0; i < PyTuple_Size(subtype->tp_mro); i++) { + PyObject* item = PyTuple_GetItem(subtype->tp_mro, i); + auto symbol_result = type_to_sym_map.find((PyTypeObject*) item); + if (symbol_result != type_to_sym_map.end()) { + hbase = (PyHocObject*) hocobj_new(hocobject_type, 0, 0); + hbase->type_ = PyHoc::HocFunction; + hbase->sym_ = symbol_result->second; + break; + } + } + + if (kwds && PyDict_Check(kwds) && (base = PyDict_GetItemString(kwds, "hocbase"))) { + if (PyObject_TypeCheck(base, hocobject_type)) { + hbase = (PyHocObject*) base; + } else { + PyErr_SetString(PyExc_TypeError, "HOC base class not valid"); + Py_DECREF(subself); + return NULL; } + PyDict_DelItemString(kwds, "hocbase"); } + + if (hbase and hbase->type_ == PyHoc::HocFunction && hbase->sym_->type == TEMPLATE) { + // printf("hocobj_new base %s\n", hbase->sym_->name); + // remove the hocbase keyword since hocobj_call only allows + // the "sec" keyword argument + PyObject* r = hocobj_call(hbase, args, kwds); + if (!r) { + Py_DECREF(subself); + return NULL; + } + PyHocObject* rh = (PyHocObject*) r; + self->type_ = rh->type_; + self->ho_ = rh->ho_; + hoc_obj_ref(self->ho_); + Py_DECREF(r); + } + return subself; } @@ -514,6 +535,11 @@ PyObject* nrnpy_ho2po(Object* o) { po = hocobj_new(hocobject_type, 0, 0); ((PyHocObject*) po)->ho_ = o; ((PyHocObject*) po)->type_ = PyHoc::HocObject; + auto location = sym_to_type_map.find(o->ctemplate->sym); + if (location != sym_to_type_map.end()) { + Py_INCREF(location->second); + po->ob_type = location->second; + } hoc_obj_ref(o); } return po; @@ -678,6 +704,15 @@ static void* fcall(void* vself, void* vargs) { PyHocObject* result = (PyHocObject*) hocobj_new(hocobject_type, 0, 0); result->ho_ = ho; result->type_ = PyHoc::HocObject; + // Note: I think the only reason we're not using ho2po here is because we don't have to + // hocref ho since it was created by hoc_newobj1... but it would be better if we did + // so we could avoid repetitive code + auto location = sym_to_type_map.find(ho->ctemplate->sym); + if (location != sym_to_type_map.end()) { + Py_INCREF(location->second); + ((PyObject*) result)->ob_type = location->second; + } + hocobj_pushargs_free_strings(strings_to_free); return result; } else { @@ -971,6 +1006,13 @@ static PyObject* hocobj_getattr(PyObject* subself, PyObject* pyname) { } Symbol* sym = getsym(n, self->ho_, 0); + // Return well known types right away + auto location = sym_to_type_map.find(sym); + if (location != sym_to_type_map.end()) { + Py_INCREF(location->second); + return (PyObject*) location->second; + } + if (!sym) { if (self->type_ == PyHoc::HocObject && self->ho_->ctemplate->sym == nrnpy_pyobj_sym_) { PyObject* p = nrnpy_hoc2pyobject(self->ho_); @@ -3152,8 +3194,22 @@ static char* nrncore_arg(double tstop) { return NULL; } + +static PyType_Spec obj_spec_from_name(const char* name) { + return { + name, + sizeof(PyHocObject), + 0, + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + nrnpy_HocObjectType_slots, + }; +} + PyObject* nrnpy_hoc() { PyObject* m; + PyObject* bases; + PyTypeObject* pto; + PyType_Spec spec; nrnpy_vec_from_python_p_ = nrnpy_vec_from_python; nrnpy_vec_to_python_p_ = nrnpy_vec_to_python; nrnpy_vec_as_numpy_helper_ = vec_as_numpy_helper; @@ -3176,13 +3232,27 @@ PyObject* nrnpy_hoc() { m = PyModule_Create(&hocmodule); assert(m); Symbol* s = NULL; - hocobject_type = (PyTypeObject*) PyType_FromSpec(&nrnpy_HocObjectType_spec); + spec = obj_spec_from_name("hoc.HocObject"); + hocobject_type = (PyTypeObject*) PyType_FromSpec(&spec); if (PyType_Ready(hocobject_type) < 0) goto fail; - Py_INCREF(hocobject_type); - // printf("AddObject HocObject\n"); PyModule_AddObject(m, "HocObject", (PyObject*) hocobject_type); + 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) + goto fail; + PyModule_AddObject(m, name, (PyObject*) pto); + } + Py_DECREF(bases); + topmethdict = PyDict_New(); for (PyMethodDef* meth = toplevel_methods; meth->ml_name != NULL; meth++) { PyObject* descr; diff --git a/src/nrnpython/nrnpy_hoc.h b/src/nrnpython/nrnpy_hoc.h index 547bd3cf78..7313cf8cad 100644 --- a/src/nrnpython/nrnpy_hoc.h +++ b/src/nrnpython/nrnpy_hoc.h @@ -27,13 +27,6 @@ static PyType_Slot nrnpy_HocObjectType_slots[] = { {Py_nb_true_divide, (PyObject*) py_hocobj_div}, {0, 0}, }; -static PyType_Spec nrnpy_HocObjectType_spec = { - "hoc.HocObject", - sizeof(PyHocObject), - 0, - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, - nrnpy_HocObjectType_slots, -}; static struct PyModuleDef hocmodule = {PyModuleDef_HEAD_INIT, diff --git a/src/oc/hoc_oop.cpp b/src/oc/hoc_oop.cpp index d5f690a753..af1c88e7d2 100644 --- a/src/oc/hoc_oop.cpp +++ b/src/oc/hoc_oop.cpp @@ -1,6 +1,8 @@ #include <../../nrnconf.h> #include #include +#include + #include "hocstr.h" #include "parse.hpp" #include "hocparse.h" @@ -28,8 +30,9 @@ static int connect_obsec_; static void call_constructor(Object*, Symbol*, int); static void free_objectdata(Objectdata*, cTemplate*); -int hoc_print_first_instance = 1; +std::vector py_exposed_classes{}; +int hoc_print_first_instance = 1; int hoc_max_builtin_class_id = -1; static Symbol* hoc_obj_; @@ -1560,13 +1563,13 @@ void hoc_endtemplate(Symbol* t) { } } -void class2oc(const char* name, - void* (*cons)(Object*), - void (*destruct)(void*), - Member_func* m, - int (*checkpoint)(void**), - Member_ret_obj_func* mobjret, - Member_ret_str_func* strret) { +void class2oc_base(const char* name, + void* (*cons)(Object*), + void (*destruct)(void*), + Member_func* m, + int (*checkpoint)(void**), + Member_ret_obj_func* mobjret, + Member_ret_str_func* strret) { extern int hoc_main1_inited_; Symbol *tsym, *s; cTemplate* t; @@ -1575,6 +1578,7 @@ void class2oc(const char* name, if (hoc_lookup(name)) { hoc_execerror(name, "already being used as a name"); } + tsym = hoc_install(name, UNDEF, 0.0, &hoc_symlist); tsym->subtype = CPLUSOBJECT; hoc_begintemplate(tsym); @@ -1586,6 +1590,7 @@ void class2oc(const char* name, t->destructor = destruct; t->steer = 0; t->checkpoint = checkpoint; + if (m) for (i = 0; m[i].name; ++i) { s = hoc_install(m[i].name, FUNCTION, 0.0, &hoc_symlist); @@ -1608,6 +1613,17 @@ void class2oc(const char* name, } +void class2oc(const char* name, + void* (*cons)(Object*), + void (*destruct)(void*), + Member_func* m, + int (*checkpoint)(void**), + Member_ret_obj_func* mobjret, + Member_ret_str_func* strret) { + class2oc_base(name, cons, destruct, m, checkpoint, mobjret, strret); + py_exposed_classes.push_back(name); +} + Symbol* hoc_decl(Symbol* s) { Symbol* ss; if (templatestackp == templatestack) { diff --git a/test/pytest_coreneuron/test_basic.py b/test/pytest_coreneuron/test_basic.py index a8c951616c..029aa2882f 100644 --- a/test/pytest_coreneuron/test_basic.py +++ b/test/pytest_coreneuron/test_basic.py @@ -449,8 +449,8 @@ def test_help(): assert h.Vector().to_python.__doc__.startswith( "Syntax:\n ``pythonlist = vec.to_python()" ) - assert h.Vector().__doc__.startswith("This class was imple") - assert h.Vector.__doc__.startswith("This class was imple") + assert h.Vector().__doc__.startswith("class neuron.hoc.HocObject") + assert h.Vector.__doc__.startswith("class neuron.hoc.HocObject") assert h.finitialize.__doc__.startswith("Syntax:\n ``h.finiti") assert h.__doc__.startswith("\n\nneuron.h\n====") diff --git a/test/pytest_coreneuron/test_inheritance.py b/test/pytest_coreneuron/test_inheritance.py new file mode 100644 index 0000000000..1fb524e32a --- /dev/null +++ b/test/pytest_coreneuron/test_inheritance.py @@ -0,0 +1,21 @@ +import neuron + + +def test_builtin_templates(): + assert isinstance(neuron.hoc.Vector, type), "Type instance expected for hoc.Vector" + assert isinstance(neuron.hoc.CVode, type), "Type instance expected for hoc.CVode" + assert isinstance(neuron.hoc.List, type), "Type instance expected for hoc.List" + assert isinstance(neuron.hoc.Deck, type), "Type instance expected for hoc.Deck" + + assert neuron.h.Vector is neuron.hoc.Vector, "Redirect to hoc.Vector failed" + assert neuron.h.Deck is neuron.hoc.Deck, "Redirect to hoc.Deck failed" + assert neuron.h.List is neuron.hoc.List, "Redirect to hoc.List failed" + + +def test_inheritance_builtin(): + v = neuron.h.Vector() + assert isinstance(v, neuron.hoc.HocObject), "hoc.HocObject should be parent." + assert isinstance(v, neuron.hoc.Vector), "Should be instance of its class" + assert not isinstance(v, neuron.hoc.Deck), "Should not be instance of another class" + assert type(v) is neuron.hoc.Vector, "Type should be class" + assert type(v) is not neuron.hoc.Deck, "Type should not be another class" diff --git a/test/pytest_coreneuron/test_pyobj.py b/test/pytest_coreneuron/test_pyobj.py index cd4faff413..ad4f3332fa 100644 --- a/test/pytest_coreneuron/test_pyobj.py +++ b/test/pytest_coreneuron/test_pyobj.py @@ -2,13 +2,20 @@ import pytest +def test_builtin(): + with pytest.raises(TypeError): + + class MyList(neuron.HocBaseObject, hoc_type=neuron.h.List): + pass + + def test_hocbase(): - class MyList(neuron.HocBaseObject, hoc_type=neuron.h.Vector): + class MyStim(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): pass - assert issubclass(MyList, neuron.hoc.HocObject) - assert issubclass(MyList, neuron.HocBaseObject) - assert MyList._hoc_type == neuron.h.Vector + assert issubclass(MyStim, neuron.hoc.HocObject) + assert issubclass(MyStim, neuron.HocBaseObject) + assert MyStim._hoc_type == neuron.h.NetStim def test_hoc_template_hclass(): @@ -64,25 +71,25 @@ def test_pyobj_constructor(): # Test that __new__ is required when __init__ is overridden with pytest.raises(TypeError): - class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): - def __init__(self, first): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): + def __init__(self, freq): super().__init__() self.append(first) - class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): - def __new__(cls, first): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): + def __new__(cls, freq): return super().__new__(cls) - def __init__(self, first): + def __init__(self, freq): super().__init__() - self.append(first) + self.interval = 1000 / freq - p = PyObj(neuron.h.List()) - assert p.count() == 1 + p = PyObj(4) + assert p.interval == 250 def test_pyobj_def(): - class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): def my_method(self, a): return a * 2 @@ -91,20 +98,29 @@ def my_method(self, a): def test_pyobj_overloading(): - class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): - def append(self, i): - self.last_appended = i - return self.baseattr("append")(i) + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.PatternStim): + def play(self, i): + self.played = True + v = neuron.h.Vector([i]) + return self.baseattr("play")(v, v) + + p = PyObj() + p.play(2) + assert hasattr(p, "played") + + +@pytest.mark.xfail(reason="inf. recursion because baseattr finds Python attrs") +def test_bad_overload(): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.PatternStim): + def not_on_base(self): + return p.baseattr("not_on_base")() p = PyObj() - p2 = PyObj() - assert p.append(p) == 1 - assert p.count() == 1 - assert p[0] == p + p.not_on_base() def test_pyobj_inheritance(): - class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): pass class MyObj(PyObj): @@ -116,7 +132,7 @@ class MyObj2(PyObj): def __init__(self, arg): pass - class List(neuron.HocBaseObject, hoc_type=neuron.h.List): + class List(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): def __new__(cls, *args, **kwargs): super().__new__(cls) @@ -126,17 +142,17 @@ def __init__(self, *args): for arg in args: self.append(arg) - l = InitList(neuron.h.List(), neuron.h.List()) + l = InitList(neuron.h.NetStim(), neuron.h.NetStim()) def test_pyobj_composition(): - class A(neuron.HocBaseObject, hoc_type=neuron.h.List): + class A(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): pass - class B(neuron.HocBaseObject, hoc_type=neuron.h.List): + class B(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): pass - class C(neuron.HocBaseObject, hoc_type=neuron.h.Vector): + class C(neuron.HocBaseObject, hoc_type=neuron.h.ExpSyn): pass with pytest.raises(TypeError): @@ -147,7 +163,7 @@ class D(A, C): class E(A, B): pass - assert E._hoc_type == neuron.h.List + assert E._hoc_type == neuron.h.NetStim class PickleTest(neuron.HocBaseObject, hoc_type=neuron.h.NetStim):