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

support exec, eval #299

Merged
merged 37 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
53f514f
first show at exec/eval etc.
nschloe Sep 18, 2023
982a3e4
fix compilation error
nschloe Sep 18, 2023
e0f1e50
fix exec compile error
nschloe Sep 18, 2023
2c1b726
missing header
nschloe Sep 19, 2023
52b918a
global -> dict
nschloe Sep 19, 2023
3cd19ba
replace macro with literal
nschloe Sep 19, 2023
19e22af
fix string conversion
nschloe Sep 19, 2023
a4f42f6
PyRun_String -> Py_CompileString + PyEval_EvalCode
nschloe Sep 19, 2023
02b7600
rm eval_file
nschloe Sep 19, 2023
72f65eb
add eval tests
nschloe Sep 19, 2023
0977a1d
actually test eval
nschloe Sep 19, 2023
c87e30b
rm env
nschloe Sep 19, 2023
5154613
pytest fix
nschloe Sep 19, 2023
aacf2d4
tests: missing #include
nschloe Sep 19, 2023
20cfc43
rm ensure_builtins_in_globals()
nschloe Sep 19, 2023
9fb0cc8
rm unused
nschloe Sep 19, 2023
48b22d7
add name
nschloe Sep 19, 2023
d729cbb
eval: remove code for embedding
nschloe Sep 20, 2023
9ac4041
eval: remove python2 workaround
nschloe Sep 20, 2023
471e3d9
eval: throw python_error() -> detail::raise_python_error()
nschloe Sep 20, 2023
b59a030
eval: fix compile error
nschloe Sep 20, 2023
df81a27
eval: rm switch()
nschloe Sep 20, 2023
03394c9
eval: style
nschloe Sep 20, 2023
d8bb953
simplify cr statements
nschloe Sep 20, 2023
4a297c3
fix mem leak
nschloe Sep 20, 2023
855da55
simplification
nschloe Sep 20, 2023
db1daf2
eval: object() -> handle()
nschloe Sep 20, 2023
749efb4
changelog
nschloe Sep 20, 2023
8b6593a
clarify porting guide on eval()
nschloe Sep 20, 2023
4434ab7
add utilities.rst
nschloe Sep 20, 2023
3eedff6
eval: rm include
nschloe Sep 20, 2023
a0011c4
eval: make optional (-> eval.h)
nschloe Sep 20, 2023
717cbb4
add documentation
nschloe Sep 20, 2023
33e26ef
incorporate requested changes
nschloe Sep 21, 2023
9f60184
small fixes
nschloe Sep 21, 2023
ec9c56a
typo
nschloe Sep 21, 2023
19d0664
add missing :cpp:func:
nschloe Sep 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"]()