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 27 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
1 change: 1 addition & 0 deletions include/nanobind/nanobind.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "nb_func.h"
#include "nb_class.h"
#include "nb_misc.h"
#include "nb_eval.h"
nschloe marked this conversation as resolved.
Show resolved Hide resolved

#if defined(_MSC_VER)
# pragma warning(pop)
Expand Down
63 changes: 63 additions & 0 deletions include/nanobind/nb_eval.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
nanobind/nb_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>

#include <utility>
nschloe marked this conversation as resolved.
Show resolved Hide resolved

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)
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)
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
auto 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
80 changes: 80 additions & 0 deletions tests/test_eval.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#include <nanobind/nanobind.h>
#include <nanobind/stl/pair.h>

// #include "pybind11_tests.h"
nschloe marked this conversation as resolved.
Show resolved Hide resolved

// #include <utility>

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"]()