diff --git a/.gitignore b/.gitignore index 4d5508aff2..c2fd8dcc99 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,20 @@ nmodlconf.h.in nrnconf.h.in build +.DS_Store +.eggs/ +.idea/ +CMakeFiles/ +Makefile +Makefile.in +.deps +x86_64 +__pycache__ +venv +virtualenv +.python-version +*.o +*.lo # These files are generated at build time # It would be a good idea to create them in the diff --git a/share/lib/python/Makefile.am b/share/lib/python/Makefile.am index bf51cfc3e0..cfc6e5704d 100644 --- a/share/lib/python/Makefile.am +++ b/share/lib/python/Makefile.am @@ -11,6 +11,7 @@ neuron/units.py \ neuron/gui.py \ neuron/hclass2.py \ neuron/hclass3.py \ +neuron/hclass35.py \ neuron/__init__.py \ neuron/psection.py \ neuron/sections.py \ diff --git a/share/lib/python/neuron/__init__.py b/share/lib/python/neuron/__init__.py index 617a0d60a2..762e093386 100644 --- a/share/lib/python/neuron/__init__.py +++ b/share/lib/python/neuron/__init__.py @@ -234,10 +234,24 @@ def test_rxd(exitOnError=True): # using the idiom self.basemethod = self.baseattr('methodname') # ------------------------------------------------------------------------------ +import sys, types + +# Flag for the Python objects interface +_pyobj_enabled = False +# Load the `hclass` factory for the correct Python version 2/3 and prevent the +# incorrect module source code from being opened by creating an empty module. if sys.version_info[0] == 2: + hclass3 = sys.modules["neuron.hclass3"] = types.ModuleType("neuron.hclass3") from neuron.hclass2 import hclass else: - from neuron.hclass3 import hclass + hclass2 = sys.modules["neuron.hclass2"] = types.ModuleType("neuron.hclass2") + if sys.version_info[0] == 3 and sys.version_info[1] < 6: + import neuron.hclass35 + hclass = neuron.hclass35.hclass + hclass3 = neuron.hclass35 + else: + from neuron.hclass3 import HocBaseObject, hclass + _pyobj_enabled = True # global list of paths already loaded by load_mechanisms nrn_dll_loaded = [] @@ -370,13 +384,13 @@ def xopen(*args, **kwargs): Description: - ``h.xopen()`` executes the commands in ``hocfile``. This is a convenient way - to define user functions and procedures. - An optional second argument is the RCS revision number in the form of a - string. The RCS file with that revision number is checked out into a - temporary file and executed. The temporary file is then removed. A file - of the same primary name is unaffected. - + ``h.xopen()`` executes the commands in ``hocfile``. This is a convenient way + to define user functions and procedures. + An optional second argument is the RCS revision number in the form of a + string. The RCS file with that revision number is checked out into a + temporary file and executed. The temporary file is then removed. A file + of the same primary name is unaffected. + This function is deprecated and will be removed in a future release. Use ``h.xopen`` instead. """ @@ -386,7 +400,7 @@ def xopen(*args, **kwargs): def quit(*args, **kwargs): """ - Exits the program. Can be used as the action of a button. If edit buffers + Exits the program. Can be used as the action of a button. If edit buffers are open you will be asked if you wish to save them before the final exit. This function is deprecated and will be removed in a future release. @@ -395,7 +409,7 @@ def quit(*args, **kwargs): """ warnings.warn("neuron.quit() is deprecated; use h.quit() or sys.exit() instead", DeprecationWarning, stacklevel=2) return h.quit(*args, **kwargs) - + def hoc_execute(hoc_commands, comment=None): @@ -447,9 +461,9 @@ def init(): By default, the units used by h.finitialize are in mV, but you can be explicit using NEURON's unit's library, e.g. - + .. code-block:: python - + from neuron.units import mV h.finitialize(-65 * mV) @@ -457,7 +471,7 @@ def init(): """ warnings.warn("neuron.init() is deprecated; use h.init() instead", DeprecationWarning, stacklevel=2) - + h.finitialize() def run(tstop): @@ -465,37 +479,37 @@ def run(tstop): function run(tstop) Run the simulation (advance the solver) until tstop [ms] - + `h.run()` and `h.continuerun(tstop)` are more powerful solutions defined in the `stdrun.hoc` library. - + ** This function exists for historical purposes. Use in new code is not recommended. ** - + This function is deprecated and will be removed in a future - release. - + release. + For running a simulation, consider doing the following instead: - + Begin your code with - + .. code-block:: python - + from neuron import h from neuron.units import ms, mV h.load_file('stdrun.hoc') - + Then when it is time to initialize and run the simulation: - + .. code-block:: python - + h.finitialize(-65 * mV) h.continuerun(100 * ms) - + where the initial membrane potential and the simulation run time are adjusted as appropriate for your model. """ warnings.warn("neuron.run(tstop) is deprecated; use h.stdinit() and h.continuerun(tstop) instead", DeprecationWarning, stacklevel=2) - + h('tstop = %g' % tstop) h('while (t < tstop) { fadvance() }') # what about pc.psolve(tstop)? @@ -667,7 +681,7 @@ def _declare_contour(secobj, obj, name): center_vec = secobj.contourcenter(secobj.raw.getrow(0), secobj.raw.getrow(1), secobj.raw.getrow(2)) x0, y0, z0 = [center_vec.x[i] for i in range(3)] # store a couple of points to check if the section has been moved - pts = [(sec.x3d(i),sec.y3d(i),sec.z3d(i)) for i in [0, sec.n3d()-1]] + pts = [(sec.x3d(i),sec.y3d(i),sec.z3d(i)) for i in [0, sec.n3d()-1]] # (is_stack, x, y, z, xcenter, ycenter, zcenter) _sec_db[sec.hoc_internal_name()] = (True if secobj.contour_list else False, secobj.raw.getrow(0).c(j), secobj.raw.getrow(1).c(j), secobj.raw.getrow(2).c(j), x0, y0, z0, pts) @@ -796,7 +810,7 @@ class _RangeVarPlot(_WrapperPlot): fig.show() pyplot.show() - + # plotnine/ggplot p9.ggplot() + r.plot(p9) @@ -1060,10 +1074,10 @@ def mark(self, segment, marker='or', **kwargs): if secs is None: secs = list(h.allsec()) - + if variable is None: kwargs.setdefault('color', 'black') - + data = [] for sec in secs: xs = [sec.x3d(i) for i in range(sec.n3d())] @@ -1392,7 +1406,7 @@ def _nrnpy_rvp_pyobj_callback(f): f_type = str(type(f)) if f_type not in ("", ""): return f - + # if we're here, f is an rxd variable, and we return a function that looks # up the weighted average concentration given an x and h.cas() # this is not particularly efficient so it is probably better to use this for @@ -1432,4 +1446,3 @@ def clear_gui_callback(): nrnpy_set_gui_callback(None) except: pass - diff --git a/share/lib/python/neuron/hclass2.py b/share/lib/python/neuron/hclass2.py index f5264a5f31..20f41fbe77 100644 --- a/share/lib/python/neuron/hclass2.py +++ b/share/lib/python/neuron/hclass2.py @@ -1,41 +1,53 @@ -#Python2 only +# Python2 only # ------------------------------------------------------------------------------ # class factory for subclassing h.anyclass # h.anyclass methods may be overridden. If so the base method can be called # using the idiom self.basemethod = self.baseattr('methodname') # ------------------------------------------------------------------------------ - + from neuron import h, hoc import nrn -#avoid syntax error if compiled by python 3 -exec(''' + class MetaHocObject(type): - """Provides Exception for Inheritance of multiple HocObject""" - def __new__(cls, name, bases, attrs): - #print cls, name, bases - m = [] - for b in bases: - if issubclass(b, hoc.HocObject): - m.append(b.__name__) - if (len(m) > 1): - raise TypeError('Multiple Inheritance of HocObject in %s' % name - + ' through %s not allowed' % ','.join(m)) - #note that join(b.__name__ for b in m) is not valid for Python 2.3 - - return type.__new__(cls, name, bases, attrs) + """ + Provides error when trying to compose a class of multiple HOC types. + """ + + def __new__(cls, name, bases, attrs): + m = [] + for b in bases: + if issubclass(b, hoc.HocObject): + m.append(b.__name__) + if len(m) > 1: + raise TypeError( + "Multiple Inheritance of HocObject in %s" % name + + " through %s not allowed" % ",".join(m) + ) + + return type.__new__(cls, name, bases, attrs) + def hclass(c): - """Class factory for subclassing h.anyclass. E.g. class MyList(hclass(h.List)):...""" - if c == h.Section : + """ + Class factory for subclassing any HOC type. + + Example: + + .. code-block:: python + + class MyList(hclass(h.List)): + pass + """ + if c == h.Section: return nrn.Section - #class hc(hoc.HocObject, metaclass=MetaHocObject): + class hc(hoc.HocObject): def __new__(cls, *args, **kwds): - kwds2 = {'hocbase': cls.htype} - if 'sec' in kwds: - kwds2['sec'] = kwds['sec'] + kwds2 = {"hocbase": cls.htype} + if "sec" in kwds: + kwds2["sec"] = kwds["sec"] return hoc.HocObject.__new__(cls, *args, **kwds2) - setattr(hc, 'htype', c) + + setattr(hc, "htype", c) return hc -''') diff --git a/share/lib/python/neuron/hclass3.py b/share/lib/python/neuron/hclass3.py index a53f724a48..7b592d16a2 100644 --- a/share/lib/python/neuron/hclass3.py +++ b/share/lib/python/neuron/hclass3.py @@ -1,41 +1,130 @@ -#Python 3 only +# Python 3 only # ------------------------------------------------------------------------------ # class factory for subclassing h.anyclass # h.anyclass methods may be overridden. If so the base method can be called # using the idiom self.basemethod = self.baseattr('methodname') # ------------------------------------------------------------------------------ - -from neuron import h, hoc + +__all__ = ["hclass", "nonlocal_hclass", "HocBaseObject"] + +from . import h, hoc import nrn +import sys + + +def assert_not_hoc_composite(cls): + """ + Asserts that a class is not directly composed of multiple HOC types. + """ + hoc_bases = set( + b._hoc_type + for b in cls.__bases__ + if issubclass(b, HocBaseObject) and b is not HocBaseObject + ) + if len(hoc_bases) > 1: + bases = ", ".join(b.__name__ for b in cls.__bases__) + raise TypeError(f"Composition of {bases} HocObjects with different HOC types.") + + +def _overrides(cls, base, method_name): + return getattr(cls, method_name) is not getattr(base, method_name) + + +def hclass(hoc_type, module_name=None, name=None): + """ + Class factory for subclassing HOC types. + + Example: + + ..code-block:: python + + import neuron -#avoid syntax error if compiled by python 2 -exec(''' -class MetaHocObject(type): - """Provides Exception for Inheritance of multiple HocObject""" - def __new__(cls, name, bases, attrs): - #print cls, name, bases - m = [] - for b in bases: - if issubclass(b, hoc.HocObject): - m.append(b.__name__) - if (len(m) > 1): - raise TypeError('Multiple Inheritance of HocObject in %s' % name - + ' through %s not allowed' % ','.join(m)) - #note that join(b.__name__ for b in m) is not valid for Python 2.3 - - return type.__new__(cls, name, bases, attrs) - -def hclass(c): - """Class factory for subclassing h.anyclass. E.g. class MyList(hclass(h.List)):...""" - if c == h.Section : + myClassTemplate = neuron.hclass( + neuron.h.Vector, + module_name=__name__, + name="MyVector", + ) + + class MyVector(myClassTemplate): + pass + + :param hoc_type: HOC types/classes such as ``h.List``, ``h.NetCon``, ``h.Vector``, ... + :type hoc_type: :class:`hoc.HocObject` + :param module_name: Name of the module where the class will be stored + (usually ``__name__``) + :type module_name: str + :param name: Name of the module level variable that the class will be stored in. When + omitted the name of the HOC type is used. + :deprecated: Inherit from :class:`~neuron.HocBaseObject` instead. + """ + if hoc_type == h.Section: return nrn.Section - class hc(hoc.HocObject, metaclass=MetaHocObject): - #class hc(hoc.HocObject): - def __new__(cls, *args, **kwds): - kwds2 = {'hocbase': cls.htype} - if 'sec' in kwds: - kwds2['sec'] = kwds['sec'] - return hoc.HocObject.__new__(cls, *args, **kwds2) - setattr(hc, 'htype', c) + if module_name is None: + module_name = __name__ + if name is None: + name = hoc_type.hname()[:-2] + try: + hc = type(name, (HocBaseObject,), {}, hoc_type=hoc_type) + except TypeError: + raise TypeError("Argument is not a valid HOC type.") from None + hc.__module__ = module_name + hc.__name__ = name + hc.__qualname__ = name return hc -''') + + +class HocBaseObject(hoc.HocObject): + """ + The base class for inheriting from HOC types. + + .. code-block:: python + + import neuron + + class MyVector(neuron.HocBaseObject, hoc_type=neuron.h.Vector): + pass + + The ``__new__`` method passes the hoc type to the builtin ``hoc.HocObject`` and + returns a new instance with the correct mapping_proxy based on the hoc type template. + """ + + def __init_subclass__(cls, hoc_type=None, **kwargs): + if hoc_type is not None: + if not isinstance(hoc_type, hoc.HocObject): + raise TypeError( + f"Class's `hoc_type` {hoc_type} is not a valid HOC type." + ) + cls._hoc_type = hoc_type + elif not hasattr(cls, "_hoc_type"): + raise TypeError( + "Class keyword argument `hoc_type` is required for HocBaseObjects." + ) + 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 + raise TypeError( + f"`{cls.__qualname__}` implements `__init__` but misses `__new__`. " + + "Class must implement `__new__`" + + " and call `super().__new__` with the arguments required by HOC" + + f" to construct the underlying h.{cls._hoc_type.hname()} HOC object." + ) + super().__init_subclass__(**kwargs) + + 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 + # 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 + # requires section stack access. + kwds2 = {"hocbase": cls._hoc_type} + if "sec" in kwds: + kwds2["sec"] = kwds["sec"] + return hoc.HocObject.__new__(cls, *args, **kwds2) diff --git a/share/lib/python/neuron/hclass35.py b/share/lib/python/neuron/hclass35.py new file mode 100644 index 0000000000..76544740c5 --- /dev/null +++ b/share/lib/python/neuron/hclass35.py @@ -0,0 +1,56 @@ +# Python 3.0 to 3.5 only +# ------------------------------------------------------------------------------ +# class factory for subclassing h.anyclass +# h.anyclass methods may be overridden. If so the base method can be called +# using the idiom self.basemethod = self.baseattr('methodname') +# ------------------------------------------------------------------------------ + +from neuron import h, hoc +import nrn + + +class MetaHocObject(type): + """ + Provides error when trying to compose a class of multiple HOC types. + """ + + def __new__(cls, name, bases, attrs): + # print cls, name, bases + m = [] + for b in bases: + if issubclass(b, hoc.HocObject): + m.append(b.__name__) + if len(m) > 1: + raise TypeError( + "Multiple Inheritance of HocObject in %s" % name + + " through %s not allowed" % ",".join(m) + ) + # note that join(b.__name__ for b in m) is not valid for Python 2.3 + + return type.__new__(cls, name, bases, attrs) + + +def hclass(c): + """ + Class factory for subclassing any HOC type. + + Example: + + .. code-block:: python + + class MyList(hclass(h.List)): + pass + """ + if c == h.Section: + return nrn.Section + + class hc(hoc.HocObject, metaclass=MetaHocObject): + # class hc(hoc.HocObject): + def __new__(cls, *args, **kwds): + kwds2 = {"hocbase": cls.htype} + if "sec" in kwds: + kwds2["sec"] = kwds["sec"] + return hoc.HocObject.__new__(cls, *args, **kwds2) + + setattr(hc, "htype", c) + return hc diff --git a/share/lib/python/neuron/tests/_subclass.py b/share/lib/python/neuron/tests/_subclass.py index b9935ab111..2394aefea5 100644 --- a/share/lib/python/neuron/tests/_subclass.py +++ b/share/lib/python/neuron/tests/_subclass.py @@ -14,7 +14,11 @@ endtemplate A ''') -class A1(hclass(h.A)) : +_cls = hclass(h.A) +class A1(_cls) : + def __new__(cls, arg): + return _cls.__new__(cls, arg) + def __init__(self, arg) : # note, arg used by h.A #self.bp = hoc.HocObject.baseattr(self, 'p') self.bp = self.baseattr('p') diff --git a/test/pynrn/_pyobj_testing.py b/test/pynrn/_pyobj_testing.py new file mode 100644 index 0000000000..cd4faff413 --- /dev/null +++ b/test/pynrn/_pyobj_testing.py @@ -0,0 +1,175 @@ +import neuron +import pytest + + +def test_hocbase(): + class MyList(neuron.HocBaseObject, hoc_type=neuron.h.Vector): + pass + + assert issubclass(MyList, neuron.hoc.HocObject) + assert issubclass(MyList, neuron.HocBaseObject) + assert MyList._hoc_type == neuron.h.Vector + + +def test_hoc_template_hclass(): + neuron.h( + """ + begintemplate A + public x, s, o, xa, oa, f, p + strdef s + objref o, oa[2] + double xa[3] + proc init() { \ + x = $1 \ + } + func f() { return $1*xa[$2] } + proc p() { x += 1 } + endtemplate A + """ + ) + + class A1(neuron.hclass(neuron.h.A)): + def __new__(cls, arg): + return super().__new__(cls, arg) + + def __init__(self, arg): + self.bp = self.baseattr("p") + + def p(self): + self.bp() + return self.x + + a = A1(5) + assert a.x == 5.0 + assert a.p() == 6.0 + b = A1(4) + a.s = "one" + b.s = "two" + assert a.s == "one" + assert b.s == "two" + assert neuron.h.A[0].s == "one" + assert a.p() == 7.0 + assert b.p() == 5.0 + a.a = 2 + b.a = 3 + assert a.a == 2 + assert b.a == 3 + assert neuron.h.List("A").count() == 2 + a = 1 + b = 1 + assert neuron.h.List("A").count() == 0 + + +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): + super().__init__() + self.append(first) + + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): + def __new__(cls, first): + return super().__new__(cls) + + def __init__(self, first): + super().__init__() + self.append(first) + + p = PyObj(neuron.h.List()) + assert p.count() == 1 + + +def test_pyobj_def(): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): + def my_method(self, a): + return a * 2 + + p = PyObj() + assert p.my_method(4) == 8 + + +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) + + p = PyObj() + p2 = PyObj() + assert p.append(p) == 1 + assert p.count() == 1 + assert p[0] == p + + +def test_pyobj_inheritance(): + class PyObj(neuron.HocBaseObject, hoc_type=neuron.h.List): + pass + + class MyObj(PyObj): + pass + + with pytest.raises(TypeError): + + class MyObj2(PyObj): + def __init__(self, arg): + pass + + class List(neuron.HocBaseObject, hoc_type=neuron.h.List): + def __new__(cls, *args, **kwargs): + super().__new__(cls) + + class InitList(List): + def __init__(self, *args): + super().__init__() + for arg in args: + self.append(arg) + + l = InitList(neuron.h.List(), neuron.h.List()) + + +def test_pyobj_composition(): + class A(neuron.HocBaseObject, hoc_type=neuron.h.List): + pass + + class B(neuron.HocBaseObject, hoc_type=neuron.h.List): + pass + + class C(neuron.HocBaseObject, hoc_type=neuron.h.Vector): + pass + + with pytest.raises(TypeError): + # Composition of different HOC types is impossible. + class D(A, C): + pass + + class E(A, B): + pass + + assert E._hoc_type == neuron.h.List + + +class PickleTest(neuron.HocBaseObject, hoc_type=neuron.h.NetStim): + def __new__(cls, *args, **kwargs): + return super().__new__(cls) + + def __init__(self, start, number, interval, noise): + self.start = start + self.number = number + self.interval = interval + self.noise = noise + + def __reduce__(self): + return ( + self.__class__, + (self.start, self.number, self.interval, self.noise), + ) + + +def test_pyobj_pickle(): + import pickle + + p = pickle.loads(pickle.dumps(PickleTest(10, 100, 1, 0))) + assert p.__class__ is PickleTest + assert p.start == 10 diff --git a/test/pynrn/test_pyobj.py b/test/pynrn/test_pyobj.py new file mode 100644 index 0000000000..9defd2b131 --- /dev/null +++ b/test/pynrn/test_pyobj.py @@ -0,0 +1,22 @@ +import neuron +import pytest +import sys + + +def test_hclass_origin(): + import neuron.hclass2, neuron.hclass3 + + if sys.version_info.major == 3: + self = neuron.hclass3 + other = neuron.hclass2 + else: + self = neuron.hclass2 + other = neuron.hclass3 + # Confirm that our hclass was loaded by import machinery + assert self.__spec__ is not None + # And that the other one is a dummy + assert other.__spec__ is None + + +if neuron._pyobj_enabled: + from _pyobj_testing import *