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

release/8.2 can use Python 3.13 #3317

Open
wants to merge 8 commits into
base: release/8.2
Choose a base branch
from
4 changes: 2 additions & 2 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ on:
# - 'docs/**'

env:
PY_MIN_VERSION: '3.7'
PY_MAX_VERSION: '3.11'
PY_MIN_VERSION: '3.9'
PY_MAX_VERSION: '3.13'

jobs:
coverage:
Expand Down
27 changes: 24 additions & 3 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
version: 2

sphinx:
configuration: docs/conf.py

build:
os: "ubuntu-22.04"
tools:
python: "mambaforge-22.9"
python: "3.12"
apt_packages:
- build-essential
- libx11-dev
- libxcomposite-dev
- libmpich-dev
- ffmpeg
- doxygen
- pandoc
- cmake
- bison
- flex
- libfl-dev
- libreadline-dev

submodules:
recursive: true

conda:
environment: docs/conda_environment.yml
python:
install:
- requirements: nrn_requirements.txt
- requirements: docs/docs_requirements.txt
40 changes: 29 additions & 11 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import glob
import os
import sys
import subprocess
from pathlib import Path

# Make translators & domains available for include
sys.path.insert(0, os.path.abspath("./translators"))
Expand Down Expand Up @@ -40,6 +42,7 @@
"sphinx.ext.mathjax",
"nbsphinx",
"sphinx_design",
"sphinx_inline_tabs",
]

source_suffix = {
Expand Down Expand Up @@ -112,17 +115,32 @@ def setup(app):

rtd_ver = PKGVER.parse(os.environ.get("READTHEDOCS_VERSION"))

# Install neuron accordingly (nightly for master, otherwise incoming version)
# Note that neuron wheel must be published a priori.
subprocess.run(
"pip install neuron{}".format(
"=={}".format(rtd_ver.base_version)
if isinstance(rtd_ver, PKGVER.Version)
else "-nightly"
),
shell=True,
check=True,
)
# see:
# https://docs.readthedocs.com/platform/stable/reference/environment-variables.html#envvar-READTHEDOCS_VERSION_TYPE
if os.environ.get("READTHEDOCS_VERSION_TYPE") == "external":
# Build and install NEURON from source
subprocess.run(
"cd .. && python setup.py build_ext bdist_wheel",
shell=True,
check=True,
)
subprocess.run(
f"pip install {glob.glob('../dist/*.whl')[0]}",
shell=True,
check=True,
)
else:
# Install neuron accordingly (nightly for master, otherwise incoming version)
# Note that neuron wheel must be published a priori.
subprocess.run(
"pip install neuron{}".format(
f"=={rtd_ver.base_version}"
if isinstance(rtd_ver, PKGVER.Version)
else "-nightly"
),
shell=True,
check=True,
)

# Execute & convert notebooks + doxygen
subprocess.run("cd .. && python setup.py docs", check=True, shell=True)
8 changes: 5 additions & 3 deletions docs/docs_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
sphinx
# do not check import of next line
sphinx_rtd_theme
jupyter
nbconvert
recommonmark
matplotlib
# bokeh 3 seems to break docs notebooks
bokeh<3
bokeh>=3
# do not check import of next line
ipython
plotnine
numpy<2
numpy>=2
plotly
nbsphinx
jinja2
sphinx-design
sphinx-inline-tabs
packaging==21.3
tenacity<8.4
anywidget
7 changes: 3 additions & 4 deletions nrn_requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ wheel
setuptools<=70.3.0
scikit-build
matplotlib
# bokeh 3 seems to break docs notebooks
bokeh<3
bokeh>=3
ipython
cython<3
cython>=3
packaging
pytest
pytest-cov
mpi4py
numpy<2
numpy>=2
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ def setup_package():
NRN_COLLECT_DIRS = ["bin", "lib", "include", "share"]

docs_require = [] # sphinx, themes, etc
maybe_rxd_reqs = ["numpy<2", "Cython<3"] if Components.RX3D else []
maybe_rxd_reqs = ["numpy", "Cython"] if Components.RX3D else []
maybe_docs = docs_require if "docs" in sys.argv else []
maybe_test_runner = ["pytest-runner"] if "test" in sys.argv else []

Expand Down
11 changes: 11 additions & 0 deletions share/lib/python/neuron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1751,3 +1751,14 @@ def _mview_html_tree(hlist, inside_mechanisms_in_use=0):
if _get_ipython() is not None:
html_formatter = _get_ipython().display_formatter.formatters["text/html"]
html_formatter.for_type(hoc.HocObject, _hocobj_html)

# in case Bokeh is installed, register a serialization function for hoc.Vector
try:
from bokeh.core.serialization import Serializer

Serializer.register(
type(h.Vector),
lambda obj, serializer: [serializer.encode(item) for item in obj],
)
except ImportError:
pass
38 changes: 24 additions & 14 deletions share/lib/python/neuron/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
Note that python threads are not used if nrniv is launched instead of Python
"""


from neuron import h

from contextlib import contextmanager
import threading
import time
import atexit

# recursive, especially in case stop/start pairs called from doNotify code.
_lock = threading.RLock()
Expand All @@ -29,9 +29,10 @@ def process_events():
_lock.acquire()
try:
h.doNotify()
except:
print("Exception in gui thread")
_lock.release()
except Exception as e:
print(f"Exception in gui thread: {e}")
finally:
_lock.release()


class Timer:
Expand Down Expand Up @@ -67,31 +68,40 @@ def end(self):

class LoopTimer(threading.Thread):
"""
a Timer that calls f every interval
A Timer that calls a function at regular intervals.
"""

def __init__(self, interval, fun):
"""
@param interval: time in seconds between call to fun()
@param fun: the function to call on timer update
"""
self.started = False
self.interval = interval
self.fun = fun
threading.Thread.__init__(self)
self.setDaemon(True)
self._running = threading.Event()
threading.Thread.__init__(self, daemon=True)

def run(self):
h.nrniv_bind_thread(threading.current_thread().ident)
self.started = True
while True:
self._running.set()
while self._running.is_set():
self.fun()
time.sleep(self.interval)

def stop(self):
"""Stop the timer thread and wait for it to terminate."""
self._running.clear()
self.join()


if h.nrnversion(9) == "2": # launched with python (instead of nrniv)
if h.nrnversion(9) == "2": # Launched with Python (instead of nrniv)
timer = LoopTimer(0.1, process_events)
timer.start()

def cleanup():
if timer.started:
timer.stop()

atexit.register(cleanup)

while not timer.started:
time.sleep(0.001)

Expand Down
44 changes: 41 additions & 3 deletions src/nrnpython/inithoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,52 @@ static int have_opt(const char* arg) {
return 0;
}

#if defined(__linux__) || defined(DARWIN)

/* we do this because thread sanitizer does not allow system calls.
In particular
system("stty sane")
returns an error code of 139
*/

#include <iostream>
#include <termios.h>
#include <unistd.h>

static struct termios original_termios;

static void save_original_terminal_settings() {
if (tcgetattr(STDIN_FILENO, &original_termios) == -1 && isatty(STDIN_FILENO)) {
std::cerr << "Error getting original terminal attributes\n";
}
}

static void restore_original_terminal_settings() {
if (tcsetattr(STDIN_FILENO, TCSANOW, &original_termios) == -1 && isatty(STDIN_FILENO)) {
std::cerr << "Error restoring terminal attributes\n";
}
}
#endif // __linux__

void nrnpython_finalize() {
#if USE_PTHREAD
pthread_t now = pthread_self();
if (pthread_equal(main_thread_, now)) {
#else
{
#endif
// Call python_gui_cleanup() if defined in Python
PyRun_SimpleString(
"try:\n"
" gui.cleanup()\n"
"except NameError:\n"
" pass\n");

// Finalize Python
Py_Finalize();
}
#if linux
if (system("stty sane > /dev/null 2>&1")) {
} // 'if' to avoid ignoring return value warning
#if defined(__linux__) || defined(DARWIN)
restore_original_terminal_settings();
#endif
}

Expand All @@ -239,6 +273,10 @@ extern "C" PyObject* PyInit_hoc() {
main_thread_ = pthread_self();
#endif

#if defined(__linux__) || defined(DARWIN)
save_original_terminal_settings();
#endif // __linux__

if (nrn_global_argv) { // ivocmain was already called so already loaded
return nrnpy_hoc();
}
Expand Down
5 changes: 4 additions & 1 deletion src/nrnpython/nrnpy_hoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1596,13 +1596,16 @@ PyObject* nrnpy_forall(PyObject* self, PyObject* args) {
static PyObject* hocobj_iter(PyObject* self) {
// printf("hocobj_iter %p\n", self);
PyHocObject* po = (PyHocObject*) self;
if (po->type_ == PyHoc::HocObject) {
if (po->type_ == PyHoc::HocObject || po->type_ == PyHoc::HocSectionListIterator) {
if (po->ho_->ctemplate == hoc_vec_template_) {
return PySeqIter_New(self);
} else if (po->ho_->ctemplate == hoc_list_template_) {
return PySeqIter_New(self);
} else if (po->ho_->ctemplate == hoc_sectionlist_template_) {
// need a clone of self so nested loops do not share iteritem_
// The HocSectionListIter arm of the outer 'if' became necessary
// at Python-3.13.1 upon which the following body is executed
// twice. See https://github.com/python/cpython/issues/127682
PyObject* po2 = nrnpy_ho2po(po->ho_);
PyHocObject* pho2 = (PyHocObject*) po2;
pho2->type_ = PyHoc::HocSectionListIterator;
Expand Down
7 changes: 7 additions & 0 deletions src/nrnpython/nrnpython.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ extern "C" int nrnpython_start(int b) {
// del g
// Also, NEURONMainMenu/File/Quit did not work. The solution to both
// seems to be to just avoid gui threads if MINGW and launched nrniv

// Beginning with Python 3.13.0 it seems that the readline
// module has not been loaded yet. Since PyInit_readline sets
// PyOS_ReadlineFunctionPointer = call_readline; without checking,
// we need to import here.
PyRun_SimpleString("import readline as nrn_readline");

PyOS_ReadlineFunctionPointer = nrnpython_getline;

// Is there a -c "command" or file.py arg.
Expand Down
Loading