NEURON
nrnpython.cpp
Go to the documentation of this file.
1 #include "nrnpython.h"
2 #include "nrnpy_utils.h"
3 #include "oc_ansi.h"
4 #include <stdio.h>
5 #include <InterViews/resource.h>
6 #if HAVE_IV
7 #include <InterViews/session.h>
8 #endif
9 #include <nrnoc2iv.h>
10 #include "hoccontext.h"
11 #include <ocfile.h> // bool isDirExist(const std::string& path);
12 
13 #include <hocstr.h>
14 #include "nrnpy.h"
15 
16 #include <filesystem>
17 #include <string>
18 #include <sstream>
19 #include <fstream>
20 
21 #include <nanobind/nanobind.h>
22 
23 extern HocStr* hoc_cbufstr;
24 extern int nrnpy_nositeflag;
25 extern std::string nrnpy_pyexe;
26 extern char* hoc_ctp;
27 extern FILE* hoc_fin;
28 extern const char* hoc_promptstr;
29 extern char* neuronhome_forward();
30 #if DARWIN || defined(__linux__)
31 extern const char* path_prefix_to_libnrniv();
32 #endif
33 static char* nrnpython_getline(FILE*, FILE*, const char*);
34 extern int nrn_global_argc;
35 extern char** nrn_global_argv;
36 int nrnpy_pyrun(const char*);
37 extern int (*p_nrnpy_pyrun)(const char*);
38 
39 static std::string python_sys_path_to_append() {
40  std::string path{neuronhome_forward()};
41  if (path.empty()) {
42  return {};
43  }
44 #if defined(__linux__) || defined(DARWIN)
45  // If /where/installed/lib/python/neuron exists, then append to sys.path
46  path = path_prefix_to_libnrniv();
47 #else // not defined(__linux__) || defined(DARWIN)
48  path += "/lib/";
49 #endif // not defined(__linux__) || defined(DARWIN)
50  path += "python";
51  if (isDirExist(path + "/neuron")) {
52  return path;
53  }
54  return {};
55 }
56 
57 namespace {
58 struct PythonConfigWrapper {
59  PythonConfigWrapper() {
60  PyConfig_InitPythonConfig(&config);
61  }
62  ~PythonConfigWrapper() {
63  PyConfig_Clear(&config);
64  }
65  operator PyConfig*() {
66  return &config;
67  }
68  PyConfig* operator->() {
69  return &config;
70  }
71  PyConfig config;
72 };
73 struct PyMem_RawFree_Deleter {
74  void operator()(wchar_t* ptr) const {
75  PyMem_RawFree(ptr);
76  }
77 };
78 PyObject* basic_sys_path{};
79 
80 /**
81  * @brief Reset sys.path to be basic_sys_path and prepend something.
82  * @param new_first Path to decode and prepend to sys.path.
83  */
84 void reset_sys_path(std::string_view new_first) {
85  nanobind::gil_scoped_acquire _{};
86  auto* const path = PySys_GetObject("path");
87  nrn_assert(path);
88  // Clear sys.path
89  nrn_assert(PyList_SetSlice(path, 0, PyList_Size(path), nullptr) != -1);
90  // Decode new_first and make a Python unicode string out of it
91  auto* const ustr = PyUnicode_DecodeFSDefaultAndSize(new_first.data(), new_first.size());
92  nrn_assert(ustr);
93  // Put the decoded string into sys.path
94  nrn_assert(PyList_Insert(path, 0, ustr) == 0);
95  // Append basic_sys_path to sys.path
96  assert(basic_sys_path && PyTuple_Check(basic_sys_path)); // failing in docs build
97  nrn_assert(PySequence_SetSlice(path, 1, 1 + PyTuple_Size(basic_sys_path), basic_sys_path) == 0);
98 }
99 } // namespace
100 
101 /**
102  * @brief Reset sys.path to its value at initialisation and prepend fname.
103  *
104  * Calling this with fname empty is appropriate ahead of executing code similar to `python -c
105  * "..."`, if fname is non-empty then resolve symlinks in it and get the directory name -- this is
106  * appropriate for `python script.py` compatibility.
107  */
108 static void nrnpython_set_path(std::string_view fname) {
109  if (fname.empty()) {
110  reset_sys_path(fname);
111  } else {
112  // Figure out what sys.path[0] should be; this involves first resolving symlinks in fname
113  // and second getting the directory name from it.
114  auto const realpath = std::filesystem::canonical(fname);
115  // .string() ensures this is not a wchar_t string on Windows
116  auto const dirname = realpath.parent_path().string();
117  reset_sys_path(dirname);
118  }
119 }
120 
121 /**
122  * @brief Execute a Python script.
123  * @return 0 on failure, 1 on success.
124  */
125 int nrnpy_pyrun(const char* fname) {
126  auto* fp = fopen(fname, "r");
127  if (fp) {
128  nrnpython_set_path(fname);
129  } else {
130  Fprintf(stderr, fmt::format("Could not open {}\n", fname).c_str());
131  return 0;
132  }
133  fclose(fp);
134 #if !defined(MINGW)
135  fp = fopen(fname, "r");
136  if (fp) {
137  int const code = PyRun_AnyFile(fp, fname);
138  fclose(fp);
139  return !code;
140  }
141  return 0;
142 #else // MINGW
143  // MINGW and Python have incompatible FILE* so try to accomplish
144  // with pure Python
145  std::string exec{"with open('"};
146  exec += fname;
147  exec +=
148  "', 'rb') as nrnmingw_file:"
149  " exec(nrnmingw_file.read(), globals())\n";
150  int const code = PyRun_SimpleString(exec.c_str());
151  if (code) {
152  PyErr_Print();
153  return 0;
154  }
155  PyRun_SimpleString("del nrnmingw_file\n");
156  return 1;
157 #endif // MINGW
158 }
159 
160 /**
161  * @brief Like a PyRun_InteractiveLoop that does not need a FILE*
162  * Use InteractiveConsole to work around the issue of mingw FILE*
163  * not being compatible with Python via the CAPI on windows11.
164  * @return 0 on success, nonzero on failure.
165  */
167  std::string lines[3]{
168  "import code as nrnmingw_code\n",
169  "nrnmingw_interpreter = nrnmingw_code.InteractiveConsole(locals=globals())\n",
170  "nrnmingw_interpreter.interact(\"\")\n"};
171  for (const auto& line: lines) {
172  if (PyRun_SimpleString(line.c_str())) {
173  PyErr_Print();
174  return -1;
175  }
176  }
177  return 0;
178 }
179 
180 extern "C" PyObject* nrnpy_hoc();
181 extern PyObject* nrnpy_nrn();
182 
183 /** @brief Start the Python interpreter.
184  * @arg b Mode of operation, can be 0 (finalize), 1 (initialize),
185  * or 2 (execute commands/scripts)
186  * @return 0 on success, non-zero on error
187  *
188  * There is an internal state variable that stores whether or not Python has
189  * been initialized. Mode 1 only has an effect if Python is not initialized,
190  * while the other modes only take effect if Python is already initialized.
191  */
192 static int nrnpython_start(int b) {
193 #if USE_PYTHON
194  static int started = 0;
195  if (b == 1 && !started) {
197  // Create a Python configuration, see
198  // https://docs.python.org/3.8/c-api/init_config.html#python-configuration, so that
199  // {nrniv,special} -python behaves as similarly as possible to python. In particular this
200  // affects locale coercion. Under some circumstances Python does not straightforwardly
201  // handle settings like LC_ALL=C, so using a different configuration can lead to surprising
202  // differences.
203  PythonConfigWrapper config;
204  if (nrnpy_nositeflag) {
205  config->site_import = 0;
206  }
207  auto const check = [](const char* desc, PyStatus status) {
208  if (PyStatus_Exception(status)) {
209  std::ostringstream oss;
210  oss << desc;
211  if (status.err_msg) {
212  oss << ": " << status.err_msg;
213  if (status.func) {
214  oss << " in " << status.func;
215  }
216  }
217  throw std::runtime_error(oss.str());
218  }
219  };
220  // Virtual environments are discovered by Python by looking for pyvenv.cfg in the directory
221  // above sys.executable (https://docs.python.org/3/library/site.html), so we want to make
222  // sure that sys.executable is the path to a reasonable choice of Python executable. If we
223  // were to let sys.executable be `/some/path/to/arch/special` then we pick up a surprising
224  // dependency on whether or not `nrnivmodl` happened to be run in the root directory of the
225  // virtual environment
226  auto pyexe = nrnpy_pyexe;
227 #ifndef NRNPYTHON_DYNAMICLOAD
228  // In non-dynamic builds, the -pyexe option has no effect on which Python is linked and
229  // used, but it can be used to change PyConfig.program_name. If -pyexe is not passed then
230  // we use the Python that was discovered at build time. We have to make an std::string
231  // because Python's API requires the null terminator.
232  auto const& default_python = neuron::config::default_python_executable;
233  if (pyexe.empty() && !default_python.empty()) {
234  // -pyexe was not passed
235  pyexe = default_python;
236  }
237 #endif
238  if (pyexe.empty()) {
239  throw std::runtime_error("Do not know what to set PyConfig.program_name to");
240  }
241  // Surprisingly, given the documentation, it seems that passing a non-absolute path to
242  // PyConfig.program_name does not lead to a lookup in $PATH, but rather to the real (nrniv)
243  // path being placed in sys.executable -- at least on macOS.
244  if (auto p = std::filesystem::path{pyexe}; !p.is_absolute()) {
245  std::ostringstream oss;
246  oss << "Setting PyConfig.program_name to a non-absolute path (" << pyexe
247  << ") is not portable; try passing an absolute path to -pyexe or NRN_PYTHONEXE";
248  throw std::runtime_error(oss.str());
249  }
250  // TODO: in non-dynamic builds then -pyexe cannot change the used Python version, and `nrniv
251  // -pyexe /path/to/python3.10 -python` may well not use Python 3.10 at all. Should we do
252  // something about that?
253  check("Could not set PyConfig.program_name",
254  PyConfig_SetBytesString(config, &config->program_name, pyexe.c_str()));
255  // PySys_SetArgv is deprecated in Python 3.11+, write to config.XXX instead.
256  // nrn_global_argv contains the arguments passed to nrniv/special, which are not valid
257  // Python arguments, so tell Python not to try and parse them. In future we might like to
258  // remove the NEURON-specific arguments and pass whatever is left to Python?
259  config->parse_argv = 0;
260  check("Could not set PyConfig.argv",
261  PyConfig_SetBytesArgv(config, nrn_global_argc, nrn_global_argv));
262  // Initialise Python
263  check("Could not initialise Python", Py_InitializeFromConfig(config));
264  // Manipulate sys.path, starting from the default values
265  {
266  nanobind::gil_scoped_acquire _{};
267  auto* const sys_path = PySys_GetObject("path");
268  if (!sys_path) {
269  throw std::runtime_error("Could not get sys.path from C++");
270  }
271  // Append a path to sys.path based on where libnrniv.so is, if it's not already there.
272  // Note that this is magic that is specific to launching via nrniv/special and not
273  // Python, which is unfortunate for consistency...
274  if (auto const path = python_sys_path_to_append(); !path.empty()) {
275  auto* ustr = PyUnicode_DecodeFSDefaultAndSize(path.c_str(), path.size());
276  assert(ustr);
277  auto const already_there = PySequence_Contains(sys_path, ustr);
278  assert(already_there != -1);
279  if (already_there == 0 && PyList_Append(sys_path, ustr)) {
280  // TODO need to cover this without breaking sys.path consistency tests
281  throw std::runtime_error("Could not append " + path + " to sys.path");
282  }
283  }
284  // To match regular Python, we should also prepend an entry to sys.path:
285  // from https://docs.python.org/3/library/sys.html#sys.path:
286  // * python -m module command line: prepend the current working directory.
287  // * python script.py command line: prepend the script’s directory. If
288  // it's a symbolic link, resolve symbolic links.
289  // * python -c code and python (REPL) command lines: prepend an empty
290  // string, which means the current working directory.
291  // We only find out later what we are going to do, so for the moment we just save a copy
292  // of sys.path and then restore + modify a copy of it before each script or command we
293  // execute.
294  assert(PyList_Check(sys_path) && !basic_sys_path);
295  basic_sys_path = PyList_AsTuple(sys_path);
296  }
297  started = 1;
298  nrnpy_hoc();
299  nrnpy_nrn();
300  }
301  if (b == 0 && started) {
302  PyGILState_STATE gilsav = PyGILState_Ensure();
303  assert(basic_sys_path);
304  Py_DECREF(basic_sys_path);
305  basic_sys_path = nullptr;
306  Py_Finalize();
307  // because of finalize, no PyGILState_Release(gilsav);
308  started = 0;
309  }
310  if (b == 2 && started) {
311  // There used to be a call to PySys_SetArgv here, which dates back to
312  // e48d933e03b5c25a454e294deea55e399f8ba1b1 and a comment about sys.argv not being set with
313  // nrniv -python. Today, it seems like this is not needed any more.
314 
315  // Used to crash with MINGW when assocated with a python gui thread e.g
316  // from neuron import h, gui
317  // g = h.Graph()
318  // del g
319  // Also, NEURONMainMenu/File/Quit did not work. The solution to both
320  // seems to be to just avoid gui threads if MINGW and launched nrniv
321 
322  // Beginning with Python 3.13.0 it seems that the readline
323  // module has not been loaded yet. Since PyInit_readline sets
324  // PyOS_ReadlineFunctionPointer = call_readline; without checking,
325  // we need to import here.
326  PyRun_SimpleString("import readline as nrn_readline");
327 
328  PyOS_ReadlineFunctionPointer = nrnpython_getline;
329 
330  // Is there a -c "command" or file.py arg.
331  bool python_error_encountered{false}, have_reset_sys_path{false};
332  for (int i = 1; i < nrn_global_argc; ++i) {
333  char* arg = nrn_global_argv[i];
334  if (strcmp(arg, "-c") == 0 && i + 1 < nrn_global_argc) {
335  // sys.path[0] should be an empty string for -c
336  reset_sys_path("");
337  have_reset_sys_path = true;
338  if (PyRun_SimpleString(nrn_global_argv[i + 1])) {
339  python_error_encountered = true;
340  }
341  break;
342  } else if (strlen(arg) > 3 && strcmp(arg + strlen(arg) - 3, ".py") == 0) {
343  if (!nrnpy_pyrun(arg)) {
344  python_error_encountered = true;
345  }
346  have_reset_sys_path = true; // inside nrnpy_pyrun
347  break;
348  }
349  }
350  // python_error_encountered dictates whether NEURON will exit with a nonzero
351  // code. In noninteractive/batch mode that happens immediately, in
352  // interactive mode then we start a Python interpreter first.
353  if (nrn_istty_) {
354  if (!have_reset_sys_path) {
355  // sys.path[0] should be 0 for interactive use, but if we're dropping into an
356  // interactive shell after executing something else then we don't want to mess with
357  // it.
358  reset_sys_path("");
359  }
360 #if !defined(MINGW)
361  PyRun_InteractiveLoop(hoc_fin, "stdin");
362 #else
363  // mingw FILE incompatible with windows11 Python FILE.
364  int ret = nrnmingw_pyrun_interactiveloop();
365  if (ret) {
366  python_error_encountered = ret;
367  }
368 #endif
369  }
370  return python_error_encountered;
371  }
372 #endif
373  return 0;
374 }
375 
376 /**
377  * @brief Backend to nrnpython(...) in HOC/Python code.
378  *
379  * This can be called both with nrniv and python as the top-level executable, with different code
380  * responsible for initialising Python in the two cases. We trust that Python was initialised
381  * correctly somewhere higher up the call stack.
382  */
383 static void nrnpython_real() {
384  int retval = 0;
385 #if USE_PYTHON
386  {
387  auto interp = HocTopContextManager();
388  nanobind::gil_scoped_acquire lock{};
389  retval = (PyRun_SimpleString(hoc_gargstr(1)) == 0);
390  }
391 #endif
393 }
394 
395 static char* nrnpython_getline(FILE*, FILE*, const char* prompt) {
396  hoc_cbufstr->buf[0] = '\0';
397  hoc_promptstr = prompt;
398  int r = hoc_get_line();
399  // printf("r=%d c=%d\n", r, hoc_cbufstr->buf[0]);
400  if (r == 1) {
401  auto const n = std::strlen(hoc_cbufstr->buf) + 1;
402  hoc_ctp = hoc_cbufstr->buf + n - 1;
403  auto* const p = static_cast<char*>(PyMem_RawMalloc(n));
404  if (!p) {
405  return nullptr;
406  }
407  std::strcpy(p, hoc_cbufstr->buf);
408  return p;
409  } else if (r == EOF) {
410  return static_cast<char*>(PyMem_RawCalloc(1, sizeof(char)));
411  }
412  return 0;
413 }
414 
419 }
static Frame * fp
Definition: code.cpp:96
#define i
Definition: md1redef.h:19
static double interp(double frac, double x1, double x2)
Definition: functabl.cpp:67
char * hoc_gargstr(int)
int hoc_get_line(void)
Definition: hoc.cpp:1613
void hoc_retpushx(double x)
Definition: hocusr.cpp:154
#define assert(ex)
Definition: hocassrt.h:24
int nrn_istty_
Definition: hoc.cpp:778
static void check(VecTNode &)
Definition: cellorder1.cpp:401
#define nrn_assert(x)
assert()-like macro, independent of NDEBUG status
Definition: nrn_assert.h:33
int const size_t const size_t n
Definition: nrngsl.h:10
return status
size_t p
_object PyObject
Definition: nrnpy.h:12
char * neuronhome_forward()
Definition: code2.cpp:190
int nrnpy_nositeflag
Definition: ivocmain.cpp:182
static void nrnpython_real()
Backend to nrnpython(...) in HOC/Python code.
Definition: nrnpython.cpp:383
static std::string python_sys_path_to_append()
Definition: nrnpython.cpp:39
int nrn_global_argc
Definition: hoc.cpp:45
HocStr * hoc_cbufstr
Definition: hoc.cpp:138
static int nrnmingw_pyrun_interactiveloop()
Like a PyRun_InteractiveLoop that does not need a FILE* Use InteractiveConsole to work around the iss...
Definition: nrnpython.cpp:166
PyObject * nrnpy_nrn()
Definition: nrnpy_nrn.cpp:3005
int nrnpy_pyrun(const char *)
Execute a Python script.
Definition: nrnpython.cpp:125
static char * nrnpython_getline(FILE *, FILE *, const char *)
Definition: nrnpython.cpp:395
const char * hoc_promptstr
Definition: hoc.cpp:139
void nrnpython_reg_real_nrnpython_cpp(neuron::python::impl_ptrs *ptrs)
Definition: nrnpython.cpp:415
std::string nrnpy_pyexe
FILE * hoc_fin
Definition: hoc.cpp:152
char ** nrn_global_argv
Definition: hoc.cpp:46
static int nrnpython_start(int b)
Start the Python interpreter.
Definition: nrnpython.cpp:192
static void nrnpython_set_path(std::string_view fname)
Reset sys.path to its value at initialisation and prepend fname.
Definition: nrnpython.cpp:108
char * hoc_ctp
Definition: hoc.cpp:141
int(* p_nrnpy_pyrun)(const char *)
Definition: hoc.cpp:53
PyObject * nrnpy_hoc()
Definition: nrnpy_hoc.cpp:3354
#define lock
static realtype retval
HOC interpreter function declarations (included by hocdec.h)
bool isDirExist(const std::string &path)
Definition: ocfile.cpp:523
Definition: hocstr.h:6
char * buf
Definition: hocstr.h:7
Collection of pointers to functions with python-version-specific implementations.
Definition: nrnpy.h:25
void(* interpreter_set_path)(std::string_view)
Definition: nrnpy.h:42
int(* interpreter_start)(int)
Definition: nrnpy.h:43
void(* hoc_nrnpython)()
Definition: nrnpy.h:39
int Fprintf(FILE *stream, const char *fmt, Args... args)
Definition: logger.hpp:8