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
diff --git a/docs/advanced/cast/index.rst b/docs/advanced/cast/index.rst
index 724585c..3ce9ea0 100644
--- a/docs/advanced/cast/index.rst
+++ b/docs/advanced/cast/index.rst
@@ -1,3 +1,5 @@
+.. _type-conversions:
+
 Type conversions
 ################
 
diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst
index f4efc68..b91e8a1 100644
--- a/docs/advanced/classes.rst
+++ b/docs/advanced/classes.rst
@@ -1232,3 +1232,21 @@
     more complete example, including a demonstration of how to provide
     automatic downcasting for an entire class hierarchy without
     writing one get() function for each class.
+
+Accessing the type object
+=========================
+
+You can get the type object from a C++ class that has already been registered using:
+
+.. code-block:: python
+
+    py::type T_py = py::type::of<T>();
+
+You can directly use ``py::type::of(ob)`` to get the type object from any python
+object, just like ``type(ob)`` in Python.
+
+.. note::
+
+    Other types, like ``py::type::of<int>()``, do not work, see :ref:`type-conversions`.
+
+.. versionadded:: 2.6
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index be62610..5601f2e 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -2204,6 +2204,18 @@
 
 PYBIND11_NAMESPACE_END(detail)
 
+
+template<typename T>
+type type::of() {
+   static_assert(
+      std::is_base_of<detail::type_caster_generic, detail::make_caster<T>>::value,
+      "py::type::of<T> only supports the case where T is a registered C++ types."
+    );
+
+    return type((PyObject*) detail::get_type_handle(typeid(T), true).ptr(), borrowed_t());
+}
+
+
 #define PYBIND11_MAKE_OPAQUE(...) \
     namespace pybind11 { namespace detail { \
         template<> class type_caster<__VA_ARGS__> : public type_caster_base<__VA_ARGS__> { }; \
diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h
index 00c791a..c1219fc 100644
--- a/include/pybind11/pytypes.h
+++ b/include/pybind11/pytypes.h
@@ -19,6 +19,7 @@
 /* A few forward declarations */
 class handle; class object;
 class str; class iterator;
+class type;
 struct arg; struct arg_v;
 
 PYBIND11_NAMESPACE_BEGIN(detail)
@@ -890,6 +891,21 @@
     object value = {};
 };
 
+
+
+class type : public object {
+public:
+    PYBIND11_OBJECT(type, object, PyType_Check)
+
+    static type of(handle h) { return type((PyObject*) Py_TYPE(h.ptr()), borrowed_t{}); }
+
+    /// Convert C++ type to py::type if previously registered. Does not convert
+    // standard types, like int, float. etc. yet.
+    // See https://github.com/pybind/pybind11/issues/2486
+    template<typename T>
+    static type of();
+};
+
 class iterable : public object {
 public:
     PYBIND11_OBJECT_DEFAULT(iterable, object, detail::PyIterable_Check)
diff --git a/tests/test_class.cpp b/tests/test_class.cpp
index 5369cb0..b7d52a1 100644
--- a/tests/test_class.cpp
+++ b/tests/test_class.cpp
@@ -134,6 +134,32 @@
         );
     });
 
+    struct Invalid {};
+
+    // test_type
+    m.def("check_type", [](int category) {
+        // Currently not supported (via a fail at compile time)
+        // See https://github.com/pybind/pybind11/issues/2486
+        // if (category == 2)
+        //     return py::type::of<int>();
+        if (category == 1)
+            return py::type::of<DerivedClass1>();
+        else
+            return py::type::of<Invalid>();
+    });
+
+    m.def("get_type_of", [](py::object ob) {
+        return py::type::of(ob);
+    });
+
+    m.def("as_type", [](py::object ob) {
+        auto tp = py::type(ob);
+        if (py::isinstance<py::type>(ob))
+            return tp;
+        else
+            throw std::runtime_error("Invalid type");
+    });
+
     // test_mismatched_holder
     struct MismatchBase1 { };
     struct MismatchDerived1 : MismatchBase1 { };
diff --git a/tests/test_class.py b/tests/test_class.py
index 4214fe7..be21f37 100644
--- a/tests/test_class.py
+++ b/tests/test_class.py
@@ -26,6 +26,40 @@
     assert cstats.alive() == 0
 
 
+def test_type():
+    assert m.check_type(1) == m.DerivedClass1
+    with pytest.raises(RuntimeError) as execinfo:
+        m.check_type(0)
+
+    assert 'pybind11::detail::get_type_info: unable to find type info' in str(execinfo.value)
+    assert 'Invalid' in str(execinfo.value)
+
+    # Currently not supported
+    # See https://github.com/pybind/pybind11/issues/2486
+    # assert m.check_type(2) == int
+
+
+def test_type_of_py():
+    assert m.get_type_of(1) == int
+    assert m.get_type_of(m.DerivedClass1()) == m.DerivedClass1
+    assert m.get_type_of(int) == type
+
+
+def test_type_of_py_nodelete():
+    # If the above test deleted the class, this will segfault
+    assert m.get_type_of(m.DerivedClass1()) == m.DerivedClass1
+
+
+def test_as_type_py():
+    assert m.as_type(int) == int
+
+    with pytest.raises(RuntimeError):
+        assert m.as_type(1) == int
+
+    with pytest.raises(RuntimeError):
+        assert m.as_type(m.DerivedClass1()) == m.DerivedClass1
+
+
 def test_docstrings(doc):
     assert doc(UserType) == "A `py::class_` type for testing"
     assert UserType.__name__ == "UserType"