Skip to content

Commit

Permalink
Add support for nb::exec(), nb::eval() (#299)
Browse files Browse the repository at this point in the history
This commit ports pybind11 functionality ( ``nb::exec()``, ``nb::eval()``) for evaluating Python expressions/statements provided in the form of strings.
  • Loading branch information
nschloe authored Sep 22, 2023
1 parent 41b241b commit 47a250f
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 1 deletion.
40 changes: 40 additions & 0 deletions docs/api_extra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -904,3 +904,43 @@ converting to Python.
number of seconds, and fractional seconds are supported to the
extent representable. The resulting timepoint will be that many
seconds after the target clock's epoch time.


Evaluating Python expressions from strings
------------------------------------------

The following functions can be used to evaluate Python functions and
expressions. They require an additional include directive:

.. code-block:: cpp
#include <nanobind/eval.h>
Detailed documentation including example code is provided in a :ref:`separate
section <utilities_eval>`.

.. cpp:enum-class:: eval_mode

This enumeration specifies how the content of a string should be
interpreted. Used in Py_CompileString().

.. cpp:enumerator:: eval_expr = Py_eval_input

Evaluate a string containing an isolated expression

.. cpp:enumerator:: eval_single_statement = Py_single_input

Evaluate a string containing a single statement. Returns \c None

.. cpp:enumerator:: eval_statements = Py_file_input

Evaluate a string containing a sequence of statement. Returns \c None

.. cpp:function:: template <eval_mode start = eval_expr, size_t N> object eval(const char (&s)[N], handle global = handle(), handle local = handle())

Evaluate the given Python code in the given global/local scopes, and return
the value.

.. cpp:function:: inline void exec(const str &expr, handle global = handle(), handle local = handle())

Execute the given Python code in the given global/local scopes.
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Version 1.6.0 (TBA)
wrappers. (commit `64d87a
<https://github.com/wjakob/nanobind/commit/64d87ae01355c247123613f140cef8e71bc98fc7>`__).

* Added :cpp:func:`nb::exec() <exec>` and :cpp:func:`nb:eval() <eval>`. (PR `#299`
<https://github.com/wjakob/nanobind/pull/299>`__).

Version 1.5.2 (Aug 24, 2023)
----------------------------

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ The nanobind logo was designed by `AndoTwin Studio
exceptions
ndarray_index
packaging
utilities

.. toctree::
:caption: Advanced
Expand Down
2 changes: 1 addition & 1 deletion docs/porting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ Removed features include:
pybind11, however.
- ● Buffer protocol binding (``.def_buffer()``) was removed in favor of
:cpp:class:`nb::ndarray\<..\> <nanobind::ndarray>`.
- ● Support for evaluating Python code strings was removed.
- ● Support for evaluating Python files was removed.

Bullet points marked with ● may be reintroduced eventually, but this will
need to be done in a careful opt-in manner that does not affect code
Expand Down
59 changes: 59 additions & 0 deletions docs/utilities.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
.. cpp:namespace:: nanobind

.. _utilities:

Utilities
==========

.. _utilities_eval:

Evaluating Python expressions from strings
==========================================

nanobind provides the :cpp:func:`eval` and :cpp:func:`exec` functions to
evaluate Python expressions and statements. The following example illustrates
how they can be used.

.. code-block:: cpp
// At beginning of file
#include <nanobind/eval.h>
...
// Evaluate in scope of main module
nb::object scope = nb::module_::import_("__main__").attr("__dict__");
// Evaluate an isolated expression
int result = nb::eval("my_variable + 10", scope).cast<int>();
// Evaluate a sequence of statements
nb::exec(
"print('Hello')\n"
"print('world!');",
scope);
C++11 raw string literals are also supported and quite handy for this purpose.
The only requirement is that the first statement must be on a new line
following the raw string delimiter ``R"(``, ensuring all lines have common
leading indent:

.. code-block:: cpp
nb::exec(R"(
x = get_answer()
if x == 42:
print('Hello World!')
else:
print('Bye!')
)", scope
);
.. note::

:cpp:func:`eval` accepts a template parameter that describes how the
string/file should be interpreted. Possible choices include ``eval_expr``
(isolated expression), ``eval_single_statement`` (a single statement,
return value is always ``none``), and ``eval_statements`` (sequence of
statements, return value is always ``none``). `eval` defaults to
``eval_expr`` and `exec` is just a shortcut for ``eval<eval_statements>``.
61 changes: 61 additions & 0 deletions include/nanobind/eval.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
nanobind/eval.h: Support for evaluating Python expressions and
statements from strings
Adapted by Nico Schlömer from pybind11's eval.h.
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/

#pragma once

#include <nanobind/nanobind.h>

NAMESPACE_BEGIN(NB_NAMESPACE)

enum eval_mode {
// Evaluate a string containing an isolated expression
eval_expr = Py_eval_input,

// Evaluate a string containing a single statement. Returns \c none
eval_single_statement = Py_single_input,

// Evaluate a string containing a sequence of statement. Returns \c none
eval_statements = Py_file_input
};

template <eval_mode start = eval_expr>
object eval(const str &expr, handle global = handle(), handle local = handle()) {
if (!local.is_valid())
local = global;

// This used to be PyRun_String, but that function isn't in the stable ABI.
object codeobj = steal(Py_CompileString(expr.c_str(), "<string>", start));
if (!codeobj.is_valid())
detail::raise_python_error();

PyObject *result = PyEval_EvalCode(codeobj.ptr(), global.ptr(), local.ptr());
if (!result)
detail::raise_python_error();

return steal(result);
}

template <eval_mode start = eval_expr, size_t N>
object eval(const char (&s)[N], handle global = handle(), handle local = handle()) {
// Support raw string literals by removing common leading whitespace
str expr = (s[0] == '\n') ? str(module_::import_("textwrap").attr("dedent")(s)) : str(s);
return eval<start>(expr, global, local);
}

inline void exec(const str &expr, handle global = handle(), handle local = handle()) {
eval<eval_statements>(expr, global, local);
}

template <size_t N>
void exec(const char (&s)[N], handle global = handle(), handle local = handle()) {
eval<eval_statements>(s, global, local);
}

NAMESPACE_END(NB_NAMESPACE)
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ nanobind_add_module(test_bind_map_ext test_stl_bind_map.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_bind_vector_ext test_stl_bind_vector.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_chrono_ext test_chrono.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_enum_ext test_enum.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_eval_ext test_eval.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_ndarray_ext test_ndarray.cpp ${NB_EXTRA_ARGS})
nanobind_add_module(test_intrusive_ext test_intrusive.cpp object.cpp object.h ${NB_EXTRA_ARGS})
nanobind_add_module(test_exception_ext test_exception.cpp ${NB_EXTRA_ARGS})
Expand Down Expand Up @@ -62,6 +63,7 @@ set(TEST_FILES
test_classes.py
test_eigen.py
test_enum.py
test_eval.py
test_exception.py
test_functions.py
test_holders.py
Expand Down
77 changes: 77 additions & 0 deletions tests/test_eval.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#include <nanobind/nanobind.h>
#include <nanobind/eval.h>
#include <nanobind/stl/pair.h>

namespace nb = nanobind;

NB_MODULE(test_eval_ext, m) {
auto global = nb::dict(nb::module_::import_("__main__").attr("__dict__"));

m.def("test_eval_statements", [global]() {
auto local = nb::dict();
local["call_test"] = nb::cpp_function([&]() -> int { return 42; });

// Regular string literal
nb::exec("message = 'Hello World!'\n"
"x = call_test()",
global,
local);

// Multi-line raw string literal
nb::exec(R"(
if x == 42:
print(message)
else:
raise RuntimeError
)",
global,
local);
auto x = nb::cast<int>(local["x"]);
return x == 42;
});

m.def("test_eval", [global]() {
auto local = nb::dict();
local["x"] = nb::int_(42);
auto x = nb::eval("x", global, local);
return nb::cast<int>(x) == 42;
});

m.def("test_eval_single_statement", []() {
auto local = nb::dict();
local["call_test"] = nb::cpp_function([&]() -> int { return 42; });

auto result = nb::eval<nb::eval_single_statement>("x = call_test()", nb::dict(), local);
auto x = nb::cast<int>(local["x"]);
return result.is_none() && x == 42;
});

m.def("test_eval_failure", []() {
try {
nb::eval("nonsense code ...");
} catch (nb::python_error &) {
return true;
}
return false;
});

// test_eval_closure
m.def("test_eval_closure", []() {
nb::dict global;
global["closure_value"] = 42;
nb::dict local;
local["closure_value"] = 0;
nb::exec(R"(
local_value = closure_value
def func_global():
return closure_value
def func_local():
return local_value
)",
global,
local);
return std::make_pair(global, local);
});
}
33 changes: 33 additions & 0 deletions tests/test_eval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os

import pytest

import test_eval_ext as m


def test_evals(capsys):
assert m.test_eval_statements()
captured = capsys.readouterr()
assert captured.out == "Hello World!\n"

assert m.test_eval()
assert m.test_eval_single_statement()

assert m.test_eval_failure()


def test_eval_closure():
global_, local = m.test_eval_closure()

assert global_["closure_value"] == 42
assert local["closure_value"] == 0

assert "local_value" not in global_
assert local["local_value"] == 0

assert "func_global" not in global_
assert local["func_global"]() == 42

assert "func_local" not in global_
with pytest.raises(NameError):
local["func_local"]()

0 comments on commit 47a250f

Please sign in to comment.