Skip to content

Commit f12ec00

Browse files
authored
feat: py::type::of<T>() and py::type::of(h) (#2364)
* feat: type<T>() * refactor: using py::type as class * refactor: py::object as base * wip: tigher api * refactor: fix conversion and limit API further * docs: some added notes from @EricCousineau-TRI * refactor: use py::type::of
1 parent 32bb907 commit f12ec00

File tree

6 files changed

+108
-0
lines changed

6 files changed

+108
-0
lines changed

docs/advanced/cast/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
.. _type-conversions:
2+
13
Type conversions
24
################
35

docs/advanced/classes.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,3 +1232,21 @@ appropriate derived-class pointer (e.g. using
12321232
more complete example, including a demonstration of how to provide
12331233
automatic downcasting for an entire class hierarchy without
12341234
writing one get() function for each class.
1235+
1236+
Accessing the type object
1237+
=========================
1238+
1239+
You can get the type object from a C++ class that has already been registered using:
1240+
1241+
.. code-block:: python
1242+
1243+
py::type T_py = py::type::of<T>();
1244+
1245+
You can directly use ``py::type::of(ob)`` to get the type object from any python
1246+
object, just like ``type(ob)`` in Python.
1247+
1248+
.. note::
1249+
1250+
Other types, like ``py::type::of<int>()``, do not work, see :ref:`type-conversions`.
1251+
1252+
.. versionadded:: 2.6

include/pybind11/cast.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,6 +2204,18 @@ object object_api<Derived>::call(Args &&...args) const {
22042204

22052205
PYBIND11_NAMESPACE_END(detail)
22062206

2207+
2208+
template<typename T>
2209+
type type::of() {
2210+
static_assert(
2211+
std::is_base_of<detail::type_caster_generic, detail::make_caster<T>>::value,
2212+
"py::type::of<T> only supports the case where T is a registered C++ types."
2213+
);
2214+
2215+
return type((PyObject*) detail::get_type_handle(typeid(T), true).ptr(), borrowed_t());
2216+
}
2217+
2218+
22072219
#define PYBIND11_MAKE_OPAQUE(...) \
22082220
namespace pybind11 { namespace detail { \
22092221
template<> class type_caster<__VA_ARGS__> : public type_caster_base<__VA_ARGS__> { }; \

include/pybind11/pytypes.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
1919
/* A few forward declarations */
2020
class handle; class object;
2121
class str; class iterator;
22+
class type;
2223
struct arg; struct arg_v;
2324

2425
PYBIND11_NAMESPACE_BEGIN(detail)
@@ -890,6 +891,21 @@ class iterator : public object {
890891
object value = {};
891892
};
892893

894+
895+
896+
class type : public object {
897+
public:
898+
PYBIND11_OBJECT(type, object, PyType_Check)
899+
900+
static type of(handle h) { return type((PyObject*) Py_TYPE(h.ptr()), borrowed_t{}); }
901+
902+
/// Convert C++ type to py::type if previously registered. Does not convert
903+
// standard types, like int, float. etc. yet.
904+
// See https://github.com/pybind/pybind11/issues/2486
905+
template<typename T>
906+
static type of();
907+
};
908+
893909
class iterable : public object {
894910
public:
895911
PYBIND11_OBJECT_DEFAULT(iterable, object, detail::PyIterable_Check)

tests/test_class.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,32 @@ TEST_SUBMODULE(class_, m) {
134134
);
135135
});
136136

137+
struct Invalid {};
138+
139+
// test_type
140+
m.def("check_type", [](int category) {
141+
// Currently not supported (via a fail at compile time)
142+
// See https://github.com/pybind/pybind11/issues/2486
143+
// if (category == 2)
144+
// return py::type::of<int>();
145+
if (category == 1)
146+
return py::type::of<DerivedClass1>();
147+
else
148+
return py::type::of<Invalid>();
149+
});
150+
151+
m.def("get_type_of", [](py::object ob) {
152+
return py::type::of(ob);
153+
});
154+
155+
m.def("as_type", [](py::object ob) {
156+
auto tp = py::type(ob);
157+
if (py::isinstance<py::type>(ob))
158+
return tp;
159+
else
160+
throw std::runtime_error("Invalid type");
161+
});
162+
137163
// test_mismatched_holder
138164
struct MismatchBase1 { };
139165
struct MismatchDerived1 : MismatchBase1 { };

tests/test_class.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,40 @@ def test_instance(msg):
2626
assert cstats.alive() == 0
2727

2828

29+
def test_type():
30+
assert m.check_type(1) == m.DerivedClass1
31+
with pytest.raises(RuntimeError) as execinfo:
32+
m.check_type(0)
33+
34+
assert 'pybind11::detail::get_type_info: unable to find type info' in str(execinfo.value)
35+
assert 'Invalid' in str(execinfo.value)
36+
37+
# Currently not supported
38+
# See https://github.com/pybind/pybind11/issues/2486
39+
# assert m.check_type(2) == int
40+
41+
42+
def test_type_of_py():
43+
assert m.get_type_of(1) == int
44+
assert m.get_type_of(m.DerivedClass1()) == m.DerivedClass1
45+
assert m.get_type_of(int) == type
46+
47+
48+
def test_type_of_py_nodelete():
49+
# If the above test deleted the class, this will segfault
50+
assert m.get_type_of(m.DerivedClass1()) == m.DerivedClass1
51+
52+
53+
def test_as_type_py():
54+
assert m.as_type(int) == int
55+
56+
with pytest.raises(RuntimeError):
57+
assert m.as_type(1) == int
58+
59+
with pytest.raises(RuntimeError):
60+
assert m.as_type(m.DerivedClass1()) == m.DerivedClass1
61+
62+
2963
def test_docstrings(doc):
3064
assert doc(UserType) == "A `py::class_` type for testing"
3165
assert UserType.__name__ == "UserType"

0 commit comments

Comments
 (0)