Skip to content

Commit c8c6f58

Browse files
committed
Added py::with as C++ implementation of a Python with statement
1 parent d53b01d commit c8c6f58

File tree

2 files changed

+138
-23
lines changed

2 files changed

+138
-23
lines changed

include/pybind11/detail/internals.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,32 @@ T &get_or_create_shared_data(const std::string &name) {
288288
return *ptr;
289289
}
290290

291+
/// Translates a std::exception_ptr into a Python exception, using the registered translators.
292+
/// Returns nullptr if the exception was successfully translated, or the leftover untranslated
293+
/// C++ exception resulting from the potential partial translation.
294+
inline std::exception_ptr translate_exception(std::exception_ptr last_exception) {
295+
/* Give each registered exception translator a chance to translate it
296+
to a Python exception in reverse order of registration.
297+
298+
A translator may choose to do one of the following:
299+
300+
- catch the exception and call PyErr_SetString or PyErr_SetObject
301+
to set a standard (or custom) Python exception, or
302+
- do nothing and let the exception fall through to the next translator, or
303+
- delegate translation to the next translator by throwing a new type of exception. */
304+
305+
auto &registered_exception_translators = detail::get_internals().registered_exception_translators;
306+
for (auto& translator : registered_exception_translators) {
307+
try {
308+
translator(last_exception);
309+
} catch (...) {
310+
last_exception = std::current_exception();
311+
continue;
312+
}
313+
return nullptr;
314+
}
315+
// Return the leftover exception that is not handled by any registered translator
316+
return last_exception;
317+
}
318+
291319
NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/pybind11.h

Lines changed: 110 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -664,29 +664,10 @@ class cpp_function : public function {
664664
e.restore();
665665
return nullptr;
666666
} catch (...) {
667-
/* When an exception is caught, give each registered exception
668-
translator a chance to translate it to a Python exception
669-
in reverse order of registration.
670-
671-
A translator may choose to do one of the following:
672-
673-
- catch the exception and call PyErr_SetString or PyErr_SetObject
674-
to set a standard (or custom) Python exception, or
675-
- do nothing and let the exception fall through to the next translator, or
676-
- delegate translation to the next translator by throwing a new type of exception. */
677-
678-
auto last_exception = std::current_exception();
679-
auto &registered_exception_translators = get_internals().registered_exception_translators;
680-
for (auto& translator : registered_exception_translators) {
681-
try {
682-
translator(last_exception);
683-
} catch (...) {
684-
last_exception = std::current_exception();
685-
continue;
686-
}
687-
return nullptr;
688-
}
689-
PyErr_SetString(PyExc_SystemError, "Exception escaped from default exception translator!");
667+
// When an exception is caught, try to translate it into Python exception.
668+
auto untranslated = translate_exception(std::current_exception());
669+
if (untranslated)
670+
PyErr_SetString(PyExc_SystemError, "Exception escaped from default exception translator!");
690671
return nullptr;
691672
}
692673

@@ -1841,6 +1822,112 @@ void print(Args &&...args) {
18411822
detail::print(c.args(), c.kwargs());
18421823
}
18431824

1825+
NAMESPACE_BEGIN(detail)
1826+
1827+
template <typename T>
1828+
struct dependent_false { static constexpr bool value = false; };
1829+
1830+
template <typename Block, typename Signature = remove_cv_t<function_signature_t<Block>>, typename SFINAE = void>
1831+
struct with_block_call_traits {
1832+
static void call(Block &&block, object &&) {
1833+
static_assert(dependent_false<Block>::value,
1834+
"The inner block function passed to pybind11::with should either take no arguments, "
1835+
"or a single argument convertible from a pybind11::object& or pybind11::object&&, "
1836+
"and should return void.");
1837+
}
1838+
};
1839+
1840+
template <typename Block>
1841+
struct with_block_call_traits<Block, void()> {
1842+
public:
1843+
static void call(Block &&block, object &&) {
1844+
std::forward<Block>(block)();
1845+
}
1846+
};
1847+
1848+
template <typename Block, typename Arg>
1849+
struct with_block_call_traits<Block, void(Arg),
1850+
enable_if_t<std::is_convertible<object&, Arg>::value ||
1851+
std::is_convertible<object&&, Arg>::value>> {
1852+
private:
1853+
static void call_impl(Block &&block, object &&obj, std::true_type) {
1854+
std::forward<Block>(block)(std::move(obj));
1855+
}
1856+
1857+
static void call_impl(Block &&block, object &&obj, std::false_type) {
1858+
std::forward<Block>(block)(obj);
1859+
}
1860+
1861+
public:
1862+
static void call(Block &&block, object &&obj) {
1863+
call_impl(std::forward<Block>(block), std::move(obj), std::is_convertible<object&&, Arg>());
1864+
}
1865+
};
1866+
1867+
template<typename Block>
1868+
void call_with_block(Block &&block, object &&obj) {
1869+
with_block_call_traits<Block>::call(std::forward<Block>(block), std::move(obj));
1870+
}
1871+
1872+
NAMESPACE_END(detail)
1873+
1874+
enum class with_exception_policy {
1875+
cascade,
1876+
translate
1877+
};
1878+
1879+
// PEP 343 specification: https://www.python.org/dev/peps/pep-0343/#specification-the-with-statement
1880+
template <typename Block>
1881+
void with(const object &mgr, Block &&block, with_exception_policy policy = with_exception_policy::translate) {
1882+
object exit = mgr.attr("__exit__");
1883+
object value = mgr.attr("__enter__")();
1884+
bool exc = true;
1885+
1886+
std::exception_ptr original_exception = nullptr;
1887+
try {
1888+
try {
1889+
detail::call_with_block(std::forward<Block>(block), std::move(value));
1890+
}
1891+
catch (const error_already_set &) {
1892+
exc = false;
1893+
// If already a Python error, catch in the outer try-catch
1894+
original_exception = std::current_exception();
1895+
throw;
1896+
}
1897+
catch (...) {
1898+
exc = false;
1899+
// Else, try our best to translate the error into a Python error before calling mrg.__exit__
1900+
original_exception = std::current_exception();
1901+
if (policy == with_exception_policy::translate) {
1902+
auto untranslated = translate_exception(std::current_exception());
1903+
if (untranslated)
1904+
std::rethrow_exception(untranslated);
1905+
else
1906+
throw error_already_set();
1907+
}
1908+
else {
1909+
throw;
1910+
}
1911+
}
1912+
}
1913+
catch (const error_already_set &e) {
1914+
// A Python error
1915+
auto exit_result = exit(e.get_type() ? e.get_type() : none(),
1916+
e.get_value() ? e.get_value() : none(),
1917+
e.get_trace() ? e.get_trace() : none());
1918+
if (!bool_(std::move(exit_result)))
1919+
std::rethrow_exception(original_exception);
1920+
}
1921+
catch (...) {
1922+
// Not a Python error
1923+
exit(none(), none(), none());
1924+
std::rethrow_exception(original_exception);
1925+
}
1926+
1927+
if (exc)
1928+
exit(none(), none(), none());
1929+
}
1930+
18441931
#if defined(WITH_THREAD) && !defined(PYPY_VERSION)
18451932

18461933
/* The functions below essentially reproduce the PyGILState_* API using a RAII

0 commit comments

Comments
 (0)