7

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.

luator
  • 4,769
  • 3
  • 30
  • 51

2 Answers2

9

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

Jesse C
  • 779
  • 4
  • 11
1

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.

luator
  • 4,769
  • 3
  • 30
  • 51
  • Try passing PyExc_DeprecationWarning as the second argument to "warn" – Jesse C Jun 19 '20 at 16:23
  • @JesseC It seems that I need to convert `PyExe_DeprecationWarning` to the correct type. Do you know how to do this? When I just do `warnings.attr("warn")("...", PyExe_DeprecationWarning);`, I get the following error at runtime: `RuntimeError: make_tuple(): unable to convert argument of type '_object*' to Python object` – luator Jun 22 '20 at 06:48