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

[smart_holder] git merge master #5474

Merged
merged 11 commits into from
Dec 20, 2024
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
4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,6 @@ jobs:
strategy:
fail-fast: false
matrix:
clang:
- dev
std:
- 11
container_suffix:
- ""
include:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/emscripten.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
submodules: true
fetch-depth: 0

- uses: pypa/cibuildwheel@v2.21
- uses: pypa/cibuildwheel@v2.22
env:
PYODIDE_BUILD_EXPORTS: whole_archive
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
- uses: actions/download-artifact@v4

- name: Generate artifact attestation for sdist and wheel
uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
with:
subject-path: "*/pybind11*"

Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ repos:

# Clang format the codebase automatically
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: "v19.1.3"
rev: "v19.1.4"
hooks:
- id: clang-format
types_or: [c++, c, cuda]

# Ruff, the Python auto-correcting linter/formatter written in Rust
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.2
rev: v0.8.1
hooks:
- id: ruff
args: ["--fix", "--show-fixes"]
Expand Down Expand Up @@ -95,7 +95,7 @@ repos:

# Avoid directional quotes
- repo: https://github.com/sirosen/texthooks
rev: "0.6.7"
rev: "0.6.8"
hooks:
- id: fix-ligatures
- id: fix-smartquotes
Expand Down Expand Up @@ -144,14 +144,14 @@ repos:

# PyLint has native support - not always usable, but works for us
- repo: https://github.com/PyCQA/pylint
rev: "v3.3.1"
rev: "v3.3.2"
hooks:
- id: pylint
files: ^pybind11

# Check schemas on some of our YAML files
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.29.4
rev: 0.30.0
hooks:
- id: check-readthedocs
- id: check-github-workflows
Expand Down
160 changes: 102 additions & 58 deletions docs/advanced/cast/custom.rst
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
Custom type casters
===================

In very rare cases, applications may require custom type casters that cannot be
expressed using the abstractions provided by pybind11, thus requiring raw
Python C API calls. This is fairly advanced usage and should only be pursued by
experts who are familiar with the intricacies of Python reference counting.

The following snippets demonstrate how this works for a very simple ``inty``
type that that should be convertible from Python types that provide a
``__int__(self)`` method.
Some applications may prefer custom type casters that convert between existing
Python types and C++ types, similar to the ``list`` ↔ ``std::vector``
and ``dict`` ↔ ``std::map`` conversions which are built into pybind11.
Implementing custom type casters is fairly advanced usage.
While it is recommended to use the pybind11 API as much as possible, more complex examples may
require familiarity with the intricacies of the Python C API.
You can refer to the `Python/C API Reference Manual <https://docs.python.org/3/c-api/index.html>`_
for more information.

The following snippets demonstrate how this works for a very simple ``Point2D`` type.
We want this type to be convertible to C++ from Python types implementing the
``Sequence`` protocol and having two elements of type ``float``.
When returned from C++ to Python, it should be converted to a Python ``tuple[float, float]``.
For this type we could provide Python bindings for different arithmetic functions implemented
in C++ (here demonstrated by a simple ``negate`` function).

..
PLEASE KEEP THE CODE BLOCKS IN SYNC WITH
tests/test_docs_advanced_cast_custom.cpp
tests/test_docs_advanced_cast_custom.py
Ideally, change the test, run pre-commit (incl. clang-format),
then copy the changed code back here.
Also use TEST_SUBMODULE in tests, but PYBIND11_MODULE in docs.

.. code-block:: cpp

struct inty { long long_value; };
namespace user_space {

void print(inty s) {
std::cout << s.long_value << std::endl;
}
struct Point2D {
double x;
double y;
};

The following Python snippet demonstrates the intended usage from the Python side:
Point2D negate(const Point2D &point) { return Point2D{-point.x, -point.y}; }

.. code-block:: python
} // namespace user_space

class A:
def __int__(self):
return 123

The following Python snippet demonstrates the intended usage of ``negate`` from the Python side:

.. code-block:: python

from example import print
from my_math_module import docs_advanced_cast_custom as m

print(A())
point1 = [1.0, -1.0]
point2 = m.negate(point1)
assert point2 == (-1.0, 1.0)

To register the necessary conversion routines, it is necessary to add an
instantiation of the ``pybind11::detail::type_caster<T>`` template.
Expand All @@ -38,56 +56,82 @@ type is explicitly allowed.

.. code-block:: cpp

namespace PYBIND11_NAMESPACE { namespace detail {
template <> struct type_caster<inty> {
public:
/**
* This macro establishes the name 'inty' in
* function signatures and declares a local variable
* 'value' of type inty
*/
PYBIND11_TYPE_CASTER(inty, const_name("inty"));

/**
* Conversion part 1 (Python->C++): convert a PyObject into a inty
* instance or return false upon failure. The second argument
* indicates whether implicit conversions should be applied.
*/
bool load(handle src, bool) {
/* Extract PyObject from handle */
PyObject *source = src.ptr();
/* Try converting into a Python integer value */
PyObject *tmp = PyNumber_Long(source);
if (!tmp)
namespace pybind11 {
namespace detail {

template <>
struct type_caster<user_space::Point2D> {
// This macro inserts a lot of boilerplate code and sets the default type hint to `tuple`
PYBIND11_TYPE_CASTER(user_space::Point2D, const_name("tuple"));
// `arg_name` and `return_name` may optionally be used to specify type hints separately for
// arguments and return values.
// The signature of our negate function would then look like:
// `negate(Sequence[float]) -> tuple[float, float]`
static constexpr auto arg_name = const_name("Sequence[float]");
static constexpr auto return_name = const_name("tuple[float, float]");

// C++ -> Python: convert `Point2D` to `tuple[float, float]`. The second and third arguments
// are used to indicate the return value policy and parent object (for
// return_value_policy::reference_internal) and are often ignored by custom casters.
// The return value should reflect the type hint specified by `return_name`.
static handle
cast(const user_space::Point2D &number, return_value_policy /*policy*/, handle /*parent*/) {
return py::make_tuple(number.x, number.y).release();
}

// Python -> C++: convert a `PyObject` into a `Point2D` and return false upon failure. The
// second argument indicates whether implicit conversions should be allowed.
// The accepted types should reflect the type hint specified by `arg_name`.
bool load(handle src, bool /*convert*/) {
// Check if handle is a Sequence
if (!py::isinstance<py::sequence>(src)) {
return false;
}
auto seq = py::reinterpret_borrow<py::sequence>(src);
// Check if exactly two values are in the Sequence
if (seq.size() != 2) {
return false;
}
// Check if each element is either a float or an int
for (auto item : seq) {
if (!py::isinstance<py::float_>(item) && !py::isinstance<py::int_>(item)) {
return false;
/* Now try to convert into a C++ int */
value.long_value = PyLong_AsLong(tmp);
Py_DECREF(tmp);
/* Ensure return code was OK (to avoid out-of-range errors etc) */
return !(value.long_value == -1 && !PyErr_Occurred());
}
}
value.x = seq[0].cast<double>();
value.y = seq[1].cast<double>();
return true;
}
};

/**
* Conversion part 2 (C++ -> Python): convert an inty instance into
* a Python object. The second and third arguments are used to
* indicate the return value policy and parent object (for
* ``return_value_policy::reference_internal``) and are generally
* ignored by implicit casters.
*/
static handle cast(inty src, return_value_policy /* policy */, handle /* parent */) {
return PyLong_FromLong(src.long_value);
}
};
}} // namespace PYBIND11_NAMESPACE::detail
} // namespace detail
} // namespace pybind11

// Bind the negate function
PYBIND11_MODULE(docs_advanced_cast_custom, m) { m.def("negate", user_space::negate); }

.. note::

A ``type_caster<T>`` defined with ``PYBIND11_TYPE_CASTER(T, ...)`` requires
that ``T`` is default-constructible (``value`` is first default constructed
and then ``load()`` assigns to it).

.. note::
For further information on the ``return_value_policy`` argument of ``cast`` refer to :ref:`return_value_policies`.
To learn about the ``convert`` argument of ``load`` see :ref:`nonconverting_arguments`.

.. warning::

When using custom type casters, it's important to declare them consistently
in every compilation unit of the Python extension module. Otherwise,
in every compilation unit of the Python extension module to satisfy the C++ One Definition Rule
(`ODR <https://en.cppreference.com/w/cpp/language/definition>`_). Otherwise,
undefined behavior can ensue.

.. note::

Using the type hint ``Sequence[float]`` signals to static type checkers, that not only tuples may be
passed, but any type implementing the Sequence protocol, e.g., ``list[float]``.
Unfortunately, that loses the length information ``tuple[float, float]`` provides.
One way of still providing some length information in type hints is using ``typing.Annotated``, e.g.,
``Annotated[Sequence[float], 2]``, or further add libraries like
`annotated-types <https://github.com/annotated-types/annotated-types>`_.
4 changes: 2 additions & 2 deletions docs/advanced/cast/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
+------------------------------------+---------------------------+-----------------------------------+
| ``std::variant<...>`` | Type-safe union (C++17) | :file:`pybind11/stl.h` |
+------------------------------------+---------------------------+-----------------------------------+
| ``std::filesystem::path<T>`` | STL path (C++17) [#]_ | :file:`pybind11/stl/filesystem.h` |
| ``std::filesystem::path`` | STL path (C++17) [#]_ | :file:`pybind11/stl/filesystem.h` |
+------------------------------------+---------------------------+-----------------------------------+
| ``std::function<...>`` | STL polymorphic function | :file:`pybind11/functional.h` |
+------------------------------------+---------------------------+-----------------------------------+
Expand All @@ -167,4 +167,4 @@ as arguments and return values, refer to the section on binding :ref:`classes`.
+------------------------------------+---------------------------+-----------------------------------+

.. [#] ``std::filesystem::path`` is converted to ``pathlib.Path`` and
``os.PathLike`` is converted to ``std::filesystem::path``.
can be loaded from ``os.PathLike``, ``str``, and ``bytes``.
4 changes: 2 additions & 2 deletions docs/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def generate_dummy_code_boost(nclasses=10):
decl += "\n"

for cl in range(nclasses):
decl += "class cl%03i {\n" % cl
decl += f"class cl{cl:03} {{\n"
decl += "public:\n"
bindings += f' py::class_<cl{cl:03}>("cl{cl:03}")\n'
for fn in range(nfns):
Expand Down Expand Up @@ -85,5 +85,5 @@ def generate_dummy_code_boost(nclasses=10):
n2 = dt.datetime.now()
elapsed = (n2 - n1).total_seconds()
size = os.stat("test.so").st_size
print(" {%i, %f, %i}," % (nclasses * nfns, elapsed, size))
print(f" {{{nclasses * nfns}, {elapsed:.6f}, {size}}},")
print("}")
47 changes: 42 additions & 5 deletions include/pybind11/cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,39 @@ PYBIND11_WARNING_DISABLE_MSVC(4127)

PYBIND11_NAMESPACE_BEGIN(detail)

// Type trait checker for `descr`
template <typename>
struct is_descr : std::false_type {};

template <size_t N, typename... Ts>
struct is_descr<descr<N, Ts...>> : std::true_type {};

template <size_t N, typename... Ts>
struct is_descr<const descr<N, Ts...>> : std::true_type {};

// Use arg_name instead of name when available
template <typename T, typename SFINAE = void>
struct as_arg_type {
static constexpr auto name = T::name;
};

template <typename T>
struct as_arg_type<T, typename std::enable_if<is_descr<decltype(T::arg_name)>::value>::type> {
static constexpr auto name = T::arg_name;
};

// Use return_name instead of name when available
template <typename T, typename SFINAE = void>
struct as_return_type {
static constexpr auto name = T::name;
};

template <typename T>
struct as_return_type<T,
typename std::enable_if<is_descr<decltype(T::return_name)>::value>::type> {
static constexpr auto name = T::return_name;
};

template <typename type, typename SFINAE = void>
class type_caster : public type_caster_base<type> {};
template <typename type>
Expand Down Expand Up @@ -1140,18 +1173,20 @@ using type_caster_holder = conditional_t<is_copy_constructible<holder_type>::val
copyable_holder_caster<type, holder_type>,
move_only_holder_caster<type, holder_type>>;

template <typename T, bool Value = false>
struct always_construct_holder {
template <bool Value = false>
struct always_construct_holder_value {
static constexpr bool value = Value;
};

template <typename T, bool Value = false>
struct always_construct_holder : always_construct_holder_value<Value> {};

/// Create a specialization for custom holder types (silently ignores std::shared_ptr)
#define PYBIND11_DECLARE_HOLDER_TYPE(type, holder_type, ...) \
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) \
namespace detail { \
template <typename type> \
struct always_construct_holder<holder_type> : always_construct_holder<void, ##__VA_ARGS__> { \
}; \
struct always_construct_holder<holder_type> : always_construct_holder_value<__VA_ARGS__> {}; \
template <typename type> \
class type_caster<holder_type, enable_if_t<!is_shared_ptr<holder_type>::value>> \
: public type_caster_holder<type, holder_type> {}; \
Expand Down Expand Up @@ -1361,6 +1396,8 @@ struct pyobject_caster {
return src.inc_ref();
}
PYBIND11_TYPE_CASTER(type, handle_type_name<type>::name);
static constexpr auto arg_name = as_arg_type<handle_type_name<type>>::name;
static constexpr auto return_name = as_return_type<handle_type_name<type>>::name;
};

template <typename T>
Expand Down Expand Up @@ -1889,7 +1926,7 @@ class argument_loader {
"py::args cannot be specified more than once");

static constexpr auto arg_names
= ::pybind11::detail::concat(type_descr(make_caster<Args>::name)...);
= ::pybind11::detail::concat(type_descr(as_arg_type<make_caster<Args>>::name)...);

bool load_args(function_call &call) { return load_impl_sequence(call, indices{}); }

Expand Down
Loading
Loading