Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way to mark a pybind11-binding as deprecated

I have a C++ class with Python bindings using pybind11.

Now I want to mark the binding of one method as deprecated. Let's assume it looks something like this:

PYBIND11_MODULE(my_module, m)
{
    pybind11::class_<Foobar>(m, "PyFoobar")
        .def("old_foo", &Foobar::foo)  // <-- this is deprecated in favour of "new_foo"
        .def("new_foo", &Foobar::foo);
}

What is the best way to mark PyFoobar.old_foo() as deprecated such that the user notices it when calling the method? Ideally I would like a DeprecationWarning to be triggered.

like image 492
luator Avatar asked Jun 19 '20 07:06

luator


2 Answers

Okay, here is my working example. I actually couldn't figure out to call the imported python function with a Python C Type, so I just went straight to the C API, which should perform better anyway

struct Foobar {
  Foobar() {}

  void foo(int32_t art) {}

};

PYBIND11_MODULE(example, m) {

  pybind11::class_<Foobar>(m, "PyFoobar")
    .def(py::init<>())
    .def("old_foo",
          [](pybind11::object &self, int arg_to_foo)
          {
            PyErr_WarnEx(PyExc_DeprecationWarning, 
                         "old_foo() is deprecated, use new_foo() instead.", 
                         1);
              return self.attr("new_foo")(arg_to_foo);
          })
    .def("new_foo", &Foobar::foo);
}

You can pass any of the warning types from here as the first argument: https://docs.python.org/3/c-api/exceptions.html#standard-warning-categories

The final int is the level of the stack that you want to be flagged as being deprecated.

So looking at this python code

import example

import warnings

warnings.simplefilter("default")

def mary():
    f = example.PyFoobar()
    f.old_foo(1)

mary()

If you set the stack level to 1 you'll get

test.py:9: DeprecationWarning: old_foo() is deprecated, use new_foo() instead.
  f.old_foo(1)

You might want 2 which will give you the actual calling context, but depends on what your use case is

test.py:11: DeprecationWarning: old_foo() is deprecated, use new_foo() instead.
  mary()

It is also important to note that by default, many versions of python have Deprecation warnings turned off. You can check this by checking the value of warnings.filters. That is why in my example I have the call to warnings.simplefilter("default") which enables all types of warnings, but only the first time they are hit. There are other ways you can control this as well, including using the -W flag when running python or an environment variable. https://docs.python.org/3/library/warnings.html#describing-warning-filters

like image 186
Jesse C Avatar answered Nov 15 '22 05:11

Jesse C


I found some way to get most of what I want: Use a lambda for the deprecated binding. In this lambda, issue the warning and then call the actual function. As in my example, the only change is the name, I simply call new_foo inside old_foo. If the actual function that is bound differs, this would get more complicated.

PYBIND11_MODULE(my_module, m)
{
    pybind11::class_<Foobar>(m, "PyFoobar")
        .def("old_foo",
             [](pybind11::object &self, int arg_to_foo)
             {
                 auto warnings = pybind11::module::import("warnings");
                 warnings.attr("warn")(
                     "old_foo() is deprecated, use new_foo() instead.");

                 return self.attr("new_foo")(arg_to_foo);
             })
        .def("new_foo", &Foobar::foo);
}

This results in

UserWarning: old_foo() is deprecated, use new_foo() instead.

When old_foo() is called for the first time.

Unfortunately, I did not yet figure out how to make it a DeprecationWarning instead of UserWarning.

like image 1
luator Avatar answered Nov 15 '22 06:11

luator