Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions bindings/pyroot/cppyy/CPyCppyy/src/Pythonize.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,111 @@ static ItemGetter* GetGetter(PyObject* args)
return getter;
}

namespace {

void compileSpanHelpers()
{
static bool compiled = false;

if (compiled)
return;

compiled = true;

auto code = R"(
namespace __cppyy_internal {

template <class T>
struct ptr_iterator {
T *cur;
T *end;

ptr_iterator(T *c, T *e) : cur(c), end(e) {}

T &operator*() const { return *cur; }
ptr_iterator &operator++()
{
++cur;
return *this;
}
bool operator==(const ptr_iterator &other) const { return cur == other.cur; }
bool operator!=(const ptr_iterator &other) const { return !(*this == other); }
};

template <class T>
ptr_iterator<T> make_iter(T *begin, T *end)
{
return {begin, end};
}

} // namespace __cppyy_internal

// Note: for const span<T>, T is const-qualified here
template <class T>
auto __cppyy_internal_begin(T &s) noexcept
{
return __cppyy_internal::make_iter(s.data(), s.data() + s.size());
}

// Note: for const span<T>, T is const-qualified here
template <class T>
auto __cppyy_internal_end(T &s) noexcept
{
// end iterator = begin iterator with cur == end
return __cppyy_internal::make_iter(s.data() + s.size(), s.data() + s.size());
}
)";
Cppyy::Compile(code, /*silent*/ true);
}

PyObject *spanBegin()
{
static PyObject *pyFunc = nullptr;
if (!pyFunc) {
compileSpanHelpers();
PyObject *py_ns = CPyCppyy::GetScopeProxy(Cppyy::gGlobalScope);
pyFunc = PyObject_GetAttrString(py_ns, "__cppyy_internal_begin");
if (!pyFunc) {
PyErr_Format(PyExc_RuntimeError, "cppyy internal error: failed to locate helper "
"'__cppyy_internal_begin' for std::span pythonization");
}
}
return pyFunc;
}

PyObject *spanEnd()
{
static PyObject *pyFunc = nullptr;
if (!pyFunc) {
compileSpanHelpers();
PyObject *py_ns = CPyCppyy::GetScopeProxy(Cppyy::gGlobalScope);
pyFunc = PyObject_GetAttrString(py_ns, "__cppyy_internal_end");
if (!pyFunc) {
PyErr_Format(PyExc_RuntimeError, "cppyy internal error: failed to locate helper "
"'__cppyy_internal_end' for std::span pythonization");
}
}
return pyFunc;
}

} // namespace

static PyObject *SpanBegin(PyObject *self, PyObject *)
{
auto begin = spanBegin();
if (!begin)
return nullptr;
return PyObject_CallOneArg(begin, self);
}

static PyObject *SpanEnd(PyObject *self, PyObject *)
{
auto end = spanEnd();
if (!end)
return nullptr;
return PyObject_CallOneArg(end, self);
}

static bool FillVector(PyObject* vecin, PyObject* args, ItemGetter* getter)
{
Py_ssize_t sz = getter->size();
Expand Down Expand Up @@ -1827,6 +1932,21 @@ bool CPyCppyy::Pythonize(PyObject* pyclass, const std::string& name)

//- class name based pythonization -------------------------------------------

if (IsTemplatedSTLClass(name, "span")) {
// libstdc++ (GCC >= 15) implements std::span::iterator using a private
// nested tag type, which makes the iterator non-instantiable by
// CallFunc-generated wrappers (the return type cannot be named without
// violating access rules).
//
// To preserve correct Python iteration semantics, we replace begin()/end()
// for std::span to return a custom pointer-based iterator instead. This
// avoids relying on std::span::iterator while still providing a real C++
// iterator object that CPyCppyy can also wrap and expose via
// __iter__/__next__.
Utility::AddToClass(pyclass, "begin", (PyCFunction)SpanBegin, METH_NOARGS);
Utility::AddToClass(pyclass, "end", (PyCFunction)SpanEnd, METH_NOARGS);
}

if (IsTemplatedSTLClass(name, "vector")) {

// std::vector<bool> is a special case in C++
Expand Down
28 changes: 28 additions & 0 deletions bindings/pyroot/cppyy/cppyy/test/test_stltypes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: UTF-8 -*-
import py, sys, pytest, os

Check failure on line 2 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:2:8: F401 `py` imported but unused

Check failure on line 2 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E401)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:2:1: E401 Multiple imports on one line
from pytest import mark, raises, skip
from support import setup_make, pylong, pyunicode, maxvalue, ispypy, no_root_errors

Check failure on line 4 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F401)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:4:21: F401 `support.setup_make` imported but unused

Check failure on line 4 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:2:1: I001 Import block is un-sorted or un-formatted

Expand Down Expand Up @@ -29,7 +29,7 @@
def __iter__(self):
return self
def __next__(self):
if self.i >= len(self.seqn): raise StopIteration

Check failure on line 32 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E701)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:32:36: E701 Multiple statements on one line (colon)
v = self.seqn[self.i]
self.i += 1
return v
Expand All @@ -50,7 +50,7 @@
self.seqn = seqn
self.i = 0
def __next__(self):
if self.i >= len(self.seqn): raise StopIteration

Check failure on line 53 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E701)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:53:36: E701 Multiple statements on one line (colon)
v = self.seqn[self.i]
self.i += 1
return v
Expand Down Expand Up @@ -85,7 +85,7 @@
raise StopIteration
next = __next__ # p2.7

from itertools import chain

Check failure on line 88 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:88:1: I001 Import block is un-sorted or un-formatted

Check failure on line 88 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E402)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:88:1: E402 Module level import not at top of file
def itermulti(seqn):
"""Test multiple tiers of iterators"""
return chain(map(lambda x:x, iterfunc(IterGen(Sequence(seqn)))))
Expand All @@ -108,8 +108,8 @@
u1 = type2test(l1)
u2 = type2test(l2)

uu = type2test(u)

Check failure on line 111 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F841)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:111:5: F841 Local variable `uu` is assigned to but never used
uu0 = type2test(u0)

Check failure on line 112 in bindings/pyroot/cppyy/cppyy/test/test_stltypes.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (F841)

bindings/pyroot/cppyy/cppyy/test/test_stltypes.py:112:5: F841 Local variable `uu0` is assigned to but never used
uu1 = type2test(u1)
uu2 = type2test(u2)

Expand Down Expand Up @@ -2147,5 +2147,33 @@
assert cppyy.gbl.GetMyErrorCount() == 0


def has_cpp_20():
import cppyy

return cppyy.gbl.gInterpreter.ProcessLine("__cplusplus;") >= 202002


@mark.skipif(not has_cpp_20(), reason="std::span requires C++20")
class TestSTLSPAN:
import cppyy

def test01_span_iterators(self):
"""
Test that std::span::begin() and std::span::end() can be used.

Covers https://github.com/root-project/root/issues/18837
"""
import cppyy

l1 = [1, 2, 3]
v = cppyy.gbl.vector(int)(l1)
s = cppyy.gbl.span(int)(v)
s.begin()
s.end()
# Check that the iteration also works, which uses begin() and end()
# internally.
assert [b for b in s] == l1


if __name__ == "__main__":
exit(pytest.main(args=['-sv', '-ra', __file__]))
Loading