-
Notifications
You must be signed in to change notification settings - Fork 195
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit ports pybind11 functionality ( ``nb::exec()``, ``nb::eval()``) for evaluating Python expressions/statements provided in the form of strings.
- Loading branch information
Showing
9 changed files
with
277 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>``. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]() |