pthom / litgen

litgen: a pybind11 automatic generator for humans who like nice code and API documentation. Also a C++ transformer tool
https://pthom.github.io/litgen
GNU General Public License v3.0
43 stars 6 forks source link

Alternate binding specification #13

Closed finger563 closed 1 month ago

finger563 commented 1 month ago

I'm having difficulty adapting my project to use litgen, as the way it generates the python bindings leads to errors of the form:

chosen constructor is explicit in copy-initialization

Here is the generated binding on the right (generated by litgen), and on the left is the original binding I hand-wrote: CleanShot 2024-07-27 at 22 30 56@2x

For reference, here is the associated c++ code: CleanShot 2024-07-27 at 22 37 57@2x

I've looked through the docs and the examples but I've yet to find any controls for how this part of the generated code can be configured - are there any controls that might help?

pthom commented 1 month ago

Hello,

Please try to use code blocks instead of screenshots, since it is easier to copy-paste. Also, please try to provide a minimal example that reproduces the error.

In your case, I suspect that on_receive_callback might be the member that creates the error. I cannot be certain, since I do not have access to the code, nor to the definition of receive_callback.

To write this class binding by hand use options.class_exclude_by_nameregex To exclude the creation of a named ctor, use options.struct_create_default_named_ctorregex

finger563 commented 1 month ago

Thanks @pthom for the reply - I'm trying to not have to manually write bindings for these things, so I'm modifying my code so that litgen can handle it as best as possible.

Thanks for the tip on how to remove the creation of the named ctor, i'll try that!

I don't have a simply reproducible example yet, but i'll try to whip one up. In the meantime, this is the PR for the litgen work I'm doing at espp:

https://github.com/esp-cpp/espp/pull/291

pthom commented 1 month ago

In the meantime, this is the PR for the litgen work I'm doing at espp: https://github.com/esp-cpp/espp/pull/291

I had a look at the options, and the generated (and documented) stubs. Nice!

finger563 commented 1 month ago

@pthom thanks! yea i've been really liking your project and I'm hopeful that I can get it configured to automatically keep the python bindings complete and up to date, though right now I'm struggling because the way my code is written litgen doesn't seem to like parsing it :/

Related to the options, is there a different way I should be specifying template class structs, such as template <typename T> RangeMapper { public: struct Config {...} } so that it gets generated properly when i specialize the template container class? Right now my options which specify the same specializations for RangeMapper::Config as for RangeMapper do not seem to work.

Related to the original issue I posted - I found that it was specifically for the std::chrono::duration<float> default initialization I was specifying (which I was specifying as 0.5f). If I change my default initialization value to be an explicit std::chrono::duration<float>(0.5f) then the error I was getting about copy-initialization went away.

Edit: relevant config:

    options.class_template_options.add_specialization(r"Bezier", ["espp::Vector2f"])
    options.class_template_options.add_specialization(r"Bezier::Config", ["espp::Vector2f"])
    options.class_template_options.add_specialization(r"Bezier::WeightedConfig", ["espp::Vector2f"])
    options.class_template_options.add_specialization(r"RangeMapper", ["int", "float"])
    options.class_template_options.add_specialization(r"RangeMapper::Config", ["int", "float"])

Even though I have that specialization specified, I still get the following incorrect generated binding:

It's incorrect for two reasons:

  1. The RangeMapper::Config is not specialized with the containing class as it should be, and
  2. The python binding object it is attached to does not have the appropriate name (it's simply pyClassRangeMapper instead of pyClassRangeMapper_int)

Note: same errors in the shorter bezier example shown for clarity

  ////////////////////    <generated_from:bezier.hpp>    ////////////////////
  auto pyClassBezier_espp_Vector2f =
      py::class_<espp::Bezier<espp::Vector2f>>
          (m, "Bezier_espp_Vector2f", "*\n * @brief Implements rational / weighted and unweighted cubic bezier curves\n *        between control points.\n * @note See https://pomax.github.io/bezierinfo/ for information on bezier\n *       curves.\n * @note Template class which can be used individually on floating point\n *       values directly or on containers such as Vector2<float>.\n * @tparam T The type of the control points, e.g. float or Vector2<float>.\n * @note The bezier curve is defined by 4 control points, P0, P1, P2, P3.\n *      The curve is defined by the equation:\n *      \\f$B(t) = (1-t)^3 * P0 + 3 * (1-t)^2 * t * P1 + 3 * (1-t) * t^2 * P2 + t^3 * P3\\f$\n *      where t is the evaluation parameter, [0, 1].\n *\n * @note The weighted bezier curve is defined by 4 control points, P0, P1, P2, P3\n *      and 4 weights, W0, W1, W2, W3.\n *      The curve is defined by the equation:\n *      \\f$B(t) = (W0 * (1-t)^3 * P0 + W1 * 3 * (1-t)^2 * t * P1 + W2 * 3 * (1-t) * t^2 * P2 + W3 *\n * t^3 * P3) / (W0 + W1 + W2 + W3)\\f$ where t is the evaluation parameter, [0, 1].\n *\n * \\section bezier_ex1 Example\n * \\snippet math_example.cpp bezier example\n");

  { // inner classes & enums of Bezier_espp_Vector2f
      auto pyClassBezier_ClassConfig =
          py::class_<espp::Bezier::Config>
              (pyClassBezier, "Config", "*\n   * @brief Unweighted cubic bezier configuration for 4 control points.\n")
          .def(py::init<>()) // implicit default constructor
          .def_readwrite("control_points", &espp::Bezier::Config::control_points, "/< Array of 4 control points")
          ;
      auto pyClassBezier_ClassWeightedConfig =
          py::class_<espp::Bezier::WeightedConfig>
              (pyClassBezier, "WeightedConfig", "*\n   * @brief Weighted cubic bezier configuration for 4 control points with\n   *        individual weights.\n")
          .def(py::init<>()) // implicit default constructor
          .def_readwrite("control_points", &espp::Bezier::WeightedConfig::control_points, "/< Array of 4 control points")
          .def_readwrite("weights", &espp::Bezier::WeightedConfig::weights, "/< Array of 4 weights, default is array of 1.0")
          ;
  } // end of inner classes & enums of Bezier_espp_Vector2f

  pyClassBezier_espp_Vector2f
      .def(py::init<>()) // implicit default constructor
      .def("__call__",
          &espp::Bezier<espp::Vector2f>::operator(),
          py::arg("t"),
          "*\n   * @brief Evaluate the bezier at \\p t.\n   * @note Convienience wrapper around the at() method.\n   * @param t The evaluation parameter, [0, 1].\n   * @return The bezier evaluated at \\p t.\n")
      ;
  ////////////////////    </generated_from:bezier.hpp>    ////////////////////
  auto pyClassRangeMapper_int =
      py::class_<espp::RangeMapper<int>>
          (m, "RangeMapper_int", "*\n * @brief Template class for converting a value from an uncentered [minimum,\n *        maximum] range into a centered output range (default [-1,1]). If\n *        provided a non-zero deadband, it will convert all values within\n *        [center-deadband, center+deadband] to be the configured\n *        output_center (default 0).\n *\n *        The RangeMapper can be optionally configured to invert the input,\n *        so that it will compute the input w.r.t. the configured min/max of\n *        the input range when mapping to the output range - this will mean\n *        that a values within the ranges [minimum, minimum+deadband] and\n *        [maximum-deadband, maximum] will all map to the output_center and\n *        the input center will map to both output_max and output_min\n *        depending on the sign of the input.\n *\n * @note When inverting the input range, you are introducing a discontinuity\n *       between the input distribution and the output distribution at the\n *       input center. Noise around the input's center value will create\n *       oscillations in the output which will jump between output maximum\n *       and output minimum. Therefore it is advised to use \\p invert_input\n *       sparignly, and to set the values robustly.\n *\n *        The RangeMapper can be optionally configured to invert the output,\n *        so that after converting from the input range to the output range,\n *        it will flip the sign on the output.\n *\n * \\section range_mapper_ex1 Example\n * \\snippet math_example.cpp range_mapper example\n");

  { // inner classes & enums of RangeMapper_int
      auto pyClassRangeMapper_ClassConfig =
          py::class_<espp::RangeMapper::Config>
              (pyClassRangeMapper, "Config", "*\n   *  @brief Configuration for the input uncentered range with optional\n   *  values for the centered output range, default values of 0 output center\n   *  and 1 output range provide a default output range between [-1, 1].\n")
          .def(py::init<>([](
          int center = int(), int center_deadband = 0, int minimum = int(), int maximum = int(), int range_deadband = 0, int output_center = 0, int output_range = 1, bool invert_output = false)
          {
              auto r = std::make_unique<espp::RangeMapper::Config>();
              r->center = center;
              r->center_deadband = center_deadband;
              r->minimum = minimum;
              r->maximum = maximum;
              r->range_deadband = range_deadband;
              r->output_center = output_center;
              r->output_range = output_range;
              r->invert_output = invert_output;
              return r;
          })
          , py::arg("center") = int(), py::arg("center_deadband") = 0, py::arg("minimum") = int(), py::arg("maximum") = int(), py::arg("range_deadband") = 0, py::arg("output_center") = 0, py::arg("output_range") = 1, py::arg("invert_output") = false
          )
          .def_readwrite("center", &espp::RangeMapper::Config::center, "*< Center value for the input range.")
          .def_readwrite("center_deadband", &espp::RangeMapper::Config::center_deadband, "*< Deadband amount around (+-) the center for which output will be 0.")
          .def_readwrite("minimum", &espp::RangeMapper::Config::minimum, "*< Minimum value for the input range.")
          .def_readwrite("maximum", &espp::RangeMapper::Config::maximum, "*< Maximum value for the input range.")
          .def_readwrite("range_deadband", &espp::RangeMapper::Config::range_deadband, "*< Deadband amount around the minimum and maximum for which output will\n                             be min/max output.")
          .def_readwrite("output_center", &espp::RangeMapper::Config::output_center, "*< The center for the output. Default 0.")
          .def_readwrite("output_range", &espp::RangeMapper::Config::output_range, "*< The range (+/-) from the center for the output. Default 1. @note Will\n                             be passed through std::abs() to ensure it is positive.")
          .def_readwrite("invert_output", &espp::RangeMapper::Config::invert_output, "*< Whether to invert the output (default False). @note If True will flip the sign\n                  of the output after converting from the input distribution.")
          ;
  } // end of inner classes & enums of RangeMapper_int

  pyClassRangeMapper_int
      .def(py::init<>())
      .def("get_center_deadband",
          &espp::RangeMapper<int>::get_center_deadband, "*\n   * @brief Return the configured deadband around the center of the input\n   *        distribution\n   * @return Deadband around the center of the input distribution for this\n   *         range mapper.\n")
      .def("get_minimum",
          &espp::RangeMapper<int>::get_minimum, "*\n   * @brief Return the configured minimum of the input distribution\n   * @return Minimum of the input distribution for this range mapper.\n")
      .def("get_maximum",
          &espp::RangeMapper<int>::get_maximum, "*\n   * @brief Return the configured maximum of the input distribution\n   * @return Maximum of the input distribution for this range mapper.\n")
      .def("get_range",
          &espp::RangeMapper<int>::get_range, "*\n   * @brief Return the configured range of the input distribution\n   * @note Always positive.\n   * @return Range of the input distribution for this range mapper.\n")
      .def("get_range_deadband",
          &espp::RangeMapper<int>::get_range_deadband, "*\n   * @brief Return the configured deadband around the min/max of the input\n   *        distribution\n   * @return Deadband around the min/max of the input distribution for this\n   *         range mapper.\n")
      .def("get_output_center",
          &espp::RangeMapper<int>::get_output_center, "*\n   * @brief Return the configured center of the output distribution\n   * @return Center of the output distribution for this range mapper.\n")
      .def("get_output_range",
          &espp::RangeMapper<int>::get_output_range, "*\n   * @brief Return the configured range of the output distribution\n   * @note Always positive.\n   * @return Range of the output distribution for this range mapper.\n")
      .def("get_output_min",
          &espp::RangeMapper<int>::get_output_min, "*\n   * @brief Return the configured minimum of the output distribution\n   * @return Minimum of the output distribution for this range mapper.\n")
      .def("get_output_max",
          &espp::RangeMapper<int>::get_output_max, "*\n   * @brief Return the configured maximum of the output distribution\n   * @return Maximum of the output distribution for this range mapper.\n")
      .def("set_center_deadband",
          &espp::RangeMapper<int>::set_center_deadband,
          py::arg("deadband"),
          "*\n   * @brief Set the deadband around the center of the input distribution.\n   * @param deadband The deadband to use around the center of the input\n   *        distribution.\n   * @note The deadband must be non-negative.\n   * @note The deadband is applied around the center value of the input\n   *       distribution.\n")
      .def("set_range_deadband",
          &espp::RangeMapper<int>::set_range_deadband,
          py::arg("deadband"),
          "*\n   * @brief Set the deadband around the min/max of the input distribution.\n   * @param deadband The deadband to use around the min/max of the input\n   *        distribution.\n   * @note The deadband must be non-negative.\n   * @note The deadband is applied around the min/max values of the input\n   *       distribution.\n")
      .def("map",
          &espp::RangeMapper<int>::map,
          py::arg("v"),
          "*\n   * @brief Map a value \\p v from the input distribution into the configured\n   *        output range (centered, default [-1,1]).\n   * @param v Value from the (possibly uncentered and possibly inverted -\n   *        defined by the previously configured Config) input distribution\n   * @return Value within the centered output distribution.\n")
      .def("unmap",
          &espp::RangeMapper<int>::unmap,
          py::arg("v"),
          "*\n   * @brief Unmap a value \\p v from the configured output range (centered,\n   *        default [-1,1]) back into the input distribution.\n   * @param T&v Value from the centered output distribution.\n   * @return Value within the input distribution.\n")
      ;
finger563 commented 1 month ago

I realized I didn't close this issue, sorry for that! The workaround (for anybody else encountering this) is to be explicit about setting your default values for your std::Chrono::duration and don't rely on type conversions.