From 0dfffcf257f01ddc72488a0408d227591e5f7e67 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 5 Apr 2020 02:34:00 -0400 Subject: [PATCH] Add is_final to disallow inheritance from Python - Not currently supported on PyPy --- docs/advanced/classes.rst | 26 ++++++++++++++++++++++++++ include/pybind11/attr.h | 13 ++++++++++++- include/pybind11/detail/class.h | 4 +++- tests/test_class.cpp | 8 ++++++++ tests/test_class.py | 18 ++++++++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index ae5907dee..20760b704 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -1042,6 +1042,32 @@ described trampoline: ``.def("foo", static_cast(&Publicist::foo));`` where ``int (A::*)() const`` is the type of ``A::foo``. +Binding final classes +===================== + +Some classes may not be appropriate to inherit from. In C++11, classes can +use the ``final`` specifier to ensure that a class cannot be inherited from. +The ``py::is_final`` attribute can be used to ensure that Python classes +cannot inherit from a specified type. The underlying C++ type does not need +to be declared final. + +.. code-block:: cpp + + class IsFinal final {}; + + py::class_(m, "IsFinal", py::is_final()); + +When you try to inherit from such a class in Python, you will now get this +error: + +.. code-block:: pycon + + >>> class PyFinalChild(IsFinal): + ... pass + TypeError: type 'IsFinal' is not an acceptable base type + +.. note:: This attribute is currently ignored on PyPy + Custom automatic downcasters ============================ diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index 6962d6fc5..744284a83 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -23,6 +23,9 @@ struct is_method { handle class_; is_method(const handle &c) : class_(c) { } }; /// Annotation for operators struct is_operator { }; +/// Annotation for classes that cannot be subclassed +struct is_final { }; + /// Annotation for parent scope struct scope { handle value; scope(const handle &s) : value(s) { } }; @@ -201,7 +204,7 @@ struct function_record { struct type_record { PYBIND11_NOINLINE type_record() : multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false), - default_holder(true), module_local(false) { } + default_holder(true), module_local(false), is_final(false) { } /// Handle to the parent scope handle scope; @@ -254,6 +257,9 @@ struct type_record { /// Is the class definition local to the module shared object? bool module_local : 1; + /// Is the class inheritable from python classes? + bool is_final : 1; + PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *)) { auto base_info = detail::get_type_info(base, false); if (!base_info) { @@ -416,6 +422,11 @@ struct process_attribute : process_attribute_default static void init(const dynamic_attr &, type_record *r) { r->dynamic_attr = true; } }; +template <> +struct process_attribute : process_attribute_default { + static void init(const is_final &, type_record *r) { r->is_final = true; } +}; + template <> struct process_attribute : process_attribute_default { static void init(const buffer_protocol &, type_record *r) { r->buffer_protocol = true; } diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index edfa7de68..a05edeb4c 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -604,10 +604,12 @@ inline PyObject* make_new_python_type(const type_record &rec) { #endif /* Flags */ - type->tp_flags |= Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE; + type->tp_flags |= Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE; #if PY_MAJOR_VERSION < 3 type->tp_flags |= Py_TPFLAGS_CHECKTYPES; #endif + if (!rec.is_final) + type->tp_flags |= Py_TPFLAGS_BASETYPE; if (rec.dynamic_attr) enable_dynamic_attributes(heap_type); diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 499d0cc51..128bc39e9 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -367,6 +367,14 @@ TEST_SUBMODULE(class_, m) { .def(py::init<>()) .def("ptr", &Aligned::ptr); #endif + + // test_final + struct IsFinal final {}; + py::class_(m, "IsFinal", py::is_final()); + + // test_non_final_final + struct IsNonFinalFinal {}; + py::class_(m, "IsNonFinalFinal", py::is_final()); } template class BreaksBase { public: virtual ~BreaksBase() = default; }; diff --git a/tests/test_class.py b/tests/test_class.py index ed63ca853..e58fef698 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -279,3 +279,21 @@ def test_aligned(): if hasattr(m, "Aligned"): p = m.Aligned().ptr() assert p % 1024 == 0 + + +# https://bitbucket.org/pypy/pypy/issues/2742 +@pytest.unsupported_on_pypy +def test_final(): + with pytest.raises(TypeError) as exc_info: + class PyFinalChild(m.IsFinal): + pass + assert str(exc_info.value).endswith("is not an acceptable base type") + + +# https://bitbucket.org/pypy/pypy/issues/2742 +@pytest.unsupported_on_pypy +def test_non_final_final(): + with pytest.raises(TypeError) as exc_info: + class PyNonFinalFinalChild(m.IsNonFinalFinal): + pass + assert str(exc_info.value).endswith("is not an acceptable base type")