wjakob / nanobind

nanobind: tiny and efficient C++/Python bindings
BSD 3-Clause "New" or "Revised" License
2.14k stars 161 forks source link

[BUG]: No implicit conversion to nb::object #577

Closed juanjosegarciaripoll closed 1 month ago

juanjosegarciaripoll commented 1 month ago

Problem description

I have a function with signature

  Strategy replace(nb::object a_method, nb::object a_tolerance,
                   nb::object a_simplification_method,
                   nb::object a_simplification_tolerance,
                   nb::object a_bond_dimension, nb::object a_num_sweeps,
                   nb::object a_normalize_flag) const;

that takes care of parsing various possible input types. In particular, I need every of these values is allowed to be nb::none, in case a given object parameter is not to be changed. The problem is that this function rejects arguments that are, for instance, of an enum type created by me in nanobind. This leads to very ugly error messages of the kind

  File "C:\Users\juanj\src\seemps5\src\seemps\optimization\descent.py", line 11, in <module>
    DESCENT_STRATEGY = DEFAULT_STRATEGY.replace(simplify=Simplification.VARIATIONAL)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: replace(): incompatible function arguments. The following argument types are supported:
    1. replace(self, method: object | None, tolerance: object | None, simplify: object | None, simplification_tolerance: object | None, max_bond_dimension: object | None, max_sweeps: object | None, normalize: object | None) -> seemps.state.core.Strategy

Invoked with types: seemps.state.core.Strategy, kwargs = { simplify: seemps.state.core.Simplification }

Reproducible example code

No response

wjakob commented 1 month ago

Please always provide a minimal complete reproducer that I can compile on my end. This snippet isn't enough to investigate. I am closing this for now. What I suspect is that you forgot an nb::arg().none() annotation.

juanjosegarciaripoll commented 1 month ago

Here's a minimal example. First the C++ file (I have stripped some code)

#include <nanobind/nanobind.h>
// core.cc
NB_MODULE(core, m) {
  class A {
    double x{0.0};

  public:
    A(nb::object data) : x{nb::cast<double>(data)} {};
  };

  class B : public A {
    double y{0.1};

  public:
    B(nb::object data, double ydata, nb::object rescale) : A(data), y{ydata} {
      if (!rescale.is_none()) {
        y /= nb::cast<double>(rescale);
      }
    }
  };

  nb::class_<A>(m, "A").def(nb::init<nb::object>());

  nb::class_<B, A>(m, "B").def(nb::init<nb::object, double, nb::object>(),
                               "data"_a, "error"_a = 0.0, "rescale"_a.none());
}

Here is the Python file

from seemps.state.core import A, B
data = B(1.0, 0.0)
print(data)

And here's the error message. Obviously there's some problem in the argument parsing code, because instead of setting None to the missing optional argument, the value of the last argument is copied.

(nanobind) PS C:\Users\juanj\src\seemps5> python foo.py
Traceback (most recent call last):
  File "C:\Users\juanj\src\seemps5\foo.py", line 3, in <module>
    data = B(1.0, 0.0)
           ^^^^^^^^^^^
TypeError: __init__(): incompatible function arguments. The following argument types are supported:
    1. __init__(self, data: object, error: float = 0.0, rescale: object | None) -> None

Invoked with types: seemps.state.core.B, float, float
juanjosegarciaripoll commented 1 month ago

In this particular example, if I replace "rescale"_a.none() with "rescale"_a = nb::none(), the example works. However, when I do this in the code where I had the original problem, there is an obscure cast error.