From 14199e2a69872ad3b526d7091e700838090161c7 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Thu, 2 Jul 2026 16:54:50 +0200 Subject: [PATCH 01/18] updating the readme --- README.md | 275 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 250 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index caae061f..08382919 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,265 @@ -# hpp-python package +# hpp-python -Provide Python bindings of the HPP software. These bindings differ from the one provided by -`hpp-corbaserver`. They are native bindings that do not use a middleware. -The bindings are generated by `boost::python`. +[![Pipeline status](https://gitlab.laas.fr/humanoid-path-planner/hpp-python/badges/master/pipeline.svg)](https://gitlab.laas.fr/humanoid-path-planner/hpp-python/commits/master) +[![Coverage report](https://gitlab.laas.fr/humanoid-path-planner/hpp-python/badges/master/coverage.svg?job=doc-coverage)](https://gepettoweb.laas.fr/doc/humanoid-path-planner/hpp-python/master/coverage/) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -### Compilation +`hpp-python` provides native Python bindings for the [HPP](https://github.com/humanoid-path-planner/hpp-doc) (Humanoid Path Planner) C++ libraries (`hpp-core`, `hpp-constraints`, `hpp-pinocchio`, `hpp-manipulation`, ...), generated with [`boost::python`](https://www.boost.org/doc/libs/release/libs/python/). These bindings differ from the ones provided by `hpp-corbaserver`: they are **native, in-process bindings that do not use a CORBA middleware**, so there is no client/server split and no serialization overhead. -Installation follows the standard CMake procedure: -```sh -git clone --recursive ... +The resulting `pyhpp` package mirrors the C++ namespace layout: + +- **`pyhpp.pinocchio`** — robot model (`Device`), grippers, Lie-group utilities. Wraps `hpp-pinocchio`. +- **`pyhpp.constraints`** — differentiable functions, transformations, implicit/explicit constraints, the hierarchical iterative solver. Wraps `hpp-constraints`. +- **`pyhpp.core`** — planning problem, path planners, path optimizers, path validation, roadmap, steering methods. Wraps `hpp-core`. +- **`pyhpp.manipulation`** — manipulation-specific `Device`, constraint graph, path planners/optimizers for manipulation problems, URDF/SRDF loading. Wraps `hpp-manipulation` and `hpp-manipulation-urdf`. +- **`pyhpp.tools`** — pure-Python helpers (xacro processing, constraint-error reporting) that are not generated bindings. + +## Table of contents + +- [Package layout](#package-layout) +- [Module dependency graph](#module-dependency-graph) +- [Dependencies](#dependencies) +- [Installation](#installation) + - [From source with CMake](#from-source-with-cmake) + - [With Nix](#with-nix) +- [Usage](#usage) + - [Building a robot and a problem (`pyhpp.pinocchio` / `pyhpp.core`)](#building-a-robot-and-a-problem-pyhpppinocchio--pyhppcore) + - [Constraints (`pyhpp.constraints`)](#constraints-pyhppconstraints) + - [Manipulation problems (`pyhpp.manipulation`)](#manipulation-problems-pyhppmanipulation) + - [Diagnosing constraint errors (`pyhpp.tools`)](#diagnosing-constraint-errors-pyhpptools) +- [Tests](#tests) +- [Documentation generation](#documentation-generation) +- [License](#license) + +## Package layout + +``` +hpp-python/ +├── CMakeLists.txt # C++/CMake build (bindings + headers + Python stub generation) +├── package.xml # ROS package manifest +├── flake.nix # Nix flake (build via github:gepetto/nix) +├── include/pyhpp/ # C++ headers shared by the binding translation units +├── doc/ +│ ├── configure.py # Injects Doxygen-extracted docstrings into the .cc binding sources +│ └── doxygen_xml_parser.py # Parses Doxygen XML to feed configure.py +├── src/pyhpp/ +│ ├── __init__.py # Top-level package init (imports eigenpy, silences converter warnings) +│ ├── pinocchio/ # hpp-pinocchio bindings: Device, Gripper, Lie-group utilities +│ │ ├── bindings.cc, device.cc/.hh, liegroup.cc, urdf/ +│ │ └── utils.py # shrinkJointRange() and other pure-Python helpers +│ ├── constraints/ # hpp-constraints bindings +│ │ ├── bindings.cc, differentiable-function.cc, generic-transformation.cc, +│ │ │ implicit.cc, explicit.cc, explicit-constraint-set.cc, locked-joint.cc, +│ │ │ iterative-solver.cc, by-substitution.cc, relative-com.cc +│ ├── core/ # hpp-core bindings +│ │ ├── bindings.cc, problem.cc, path.cc, path-planner.cc, path-optimizer.cc, +│ │ │ path-projector.cc, path-validation.cc, roadmap.cc, steering-method.cc, +│ │ │ config-validation.cc, configuration-shooter.cc, constraint.cc, +│ │ │ connected-component.cc, distance.cc, node.cc, parameter.cc, reports.cc, +│ │ │ problem-target.cc +│ │ ├── path/ # concrete Path subclasses submodule +│ │ ├── path_optimization/ # concrete PathOptimizer subclasses submodule +│ │ ├── problem_target/ # concrete ProblemTarget subclasses submodule +│ │ └── static_stability_constraint_factory.py +│ ├── manipulation/ # hpp-manipulation(-urdf) bindings +│ │ ├── bindings.cc, device.cc/.hh, graph.cc/.hh, problem.cc/.hh, +│ │ │ path-planner.cc/.hh, path-optimizer.cc, path-projector.cc, +│ │ │ steering-method.cc/.hh +│ │ ├── steering_method/ # e.g. cartesian.cc submodule +│ │ ├── urdf/ # URDF/SRDF loading submodule +│ │ ├── constraint_graph_factory.py # ConstraintGraphFactory: build constraint graphs declaratively +│ │ └── security_margins.py # SecurityMargins: per-pair collision security margins +│ └── tools/ # Pure-Python helpers, no C++ bindings +│ ├── xacro.py # process_xacro(): resolve xacro files (with ROS 2 AMENT_PREFIX_PATH support) +│ └── constraint_error.py # describe_error(): human-readable constraint violation report +└── tests/ + ├── unit/ # unittest-based tests, one file per bound module (see below) + └── integration/ # End-to-end scenarios (pr2-in-iai-kitchen, romeo-placard, ur3-spheres, ...) +``` + +## Module dependency graph + +The `BOOST_PYTHON_MODULE` initializers `import` each other in a fixed order, which also reflects the underlying C++ library dependencies: + +```mermaid +flowchart LR + pin["pyhpp.pinocchio\n(hpp-pinocchio)"] + cons["pyhpp.constraints\n(hpp-constraints)"] + core["pyhpp.core\n(hpp-core)"] + manip["pyhpp.manipulation\n(hpp-manipulation)"] + ext["pinocchio\n(external package)"] + + ext --> pin + pin --> cons + cons --> core + pin --> manip + cons --> manip + core --> manip + + style pin fill:#dbe9f4,stroke:#4a90d9 + style cons fill:#dbe9f4,stroke:#4a90d9 + style core fill:#dbe9f4,stroke:#4a90d9 + style manip fill:#dbe9f4,stroke:#4a90d9 + style ext fill:#e8f4e8,stroke:#4a9d4a +``` + +Importing `pyhpp.manipulation` transitively imports `pyhpp.core`, `pyhpp.constraints` and `pyhpp.pinocchio`; you rarely need to import the lower-level modules explicitly unless you only need robot/constraint primitives without a full manipulation problem. + +## Dependencies + +- Python ≥ 3.9, with `numpy` +- [Boost.Python](https://www.boost.org/doc/libs/release/libs/python/) (found via `search_for_boost_python()`) +- [`eigenpy`](https://github.com/stack-of-tasks/eigenpy) +- [Pinocchio](https://github.com/stack-of-tasks/pinocchio) (C++ and Python) +- HPP C++ libraries: `hpp-util`, `hpp-pinocchio`, `hpp-constraints`, `hpp-core`, `hpp-manipulation`, `hpp-manipulation-urdf` +- Optional: [`pybind11-stubgen`](https://github.com/sizmailov/pybind11-stubgen) to generate `.pyi` type stubs (`GENERATE_PYTHON_STUBS`, default `ON`) +- For running the test suite (`BUILD_TESTING`): [`example-robot-data`](https://github.com/Gepetto/example-robot-data), `hpp-environments` + +## Installation + +### From source with CMake + +```bash +git clone --recursive https://github.com/humanoid-path-planner/hpp-python.git mkdir hpp-python/build cd hpp-python/build -cmake -DCMAKE_INSTALL_PREFIX=... -DCMAKE_BUILD_TYPE=Release .. +cmake -DCMAKE_INSTALL_PREFIX= -DCMAKE_BUILD_TYPE=Release .. make make test make install ``` -### Generate documentation +Relevant CMake options: + +- `GENERATE_PYTHON_STUBS` (default `ON`): generate `.pyi` stub files with `pybind11-stubgen` for IDE autocompletion; automatically disabled if the tool is not found. +- `HPP_DEBUG` (default `OFF`): enable `hpp-util` debug logging (`-DHPP_DEBUG`). +- `HPP_BENCHMARK` (default `OFF`): enable `hpp-util` benchmark output (`-DHPP_ENABLE_BENCHMARK`). +- `BUILD_TESTING`: build and run the unit/integration test suite (requires `example-robot-data` and `hpp-environments`). + +### With Nix + +A flake is provided, pulling `hpp-constraints`, `hpp-core` and `hpp-manipulation` from their own flakes and building against [`github:gepetto/nix`](https://github.com/gepetto/nix): + +```bash +nix build github:humanoid-path-planner/hpp-python +``` + +## Usage + +### Building a robot and a problem (`pyhpp.pinocchio` / `pyhpp.core`) + +```python +from pinocchio import SE3 +from pyhpp.pinocchio import Device, urdf +from pyhpp.core import Problem + +UR5_URDF = "package://example-robot-data/robots/ur_description/urdf/ur5_joint_limited_robot.urdf" +UR5_SRDF = "package://example-robot-data/robots/ur_description/srdf/ur5_joint_limited_robot.srdf" + +robot = Device("ur5") +urdf.loadModel(robot, 0, "ur5", "anchor", UR5_URDF, UR5_SRDF, SE3.Identity()) + +problem = Problem(robot) +sm = problem.steeringMethod() +distance = problem.distance() +shooter = problem.configurationShooter() +``` + +Path planners, path optimizers, path validation and the roadmap are all bound in `pyhpp.core`, with concrete implementations exposed through the `pyhpp.core.path`, `pyhpp.core.path_optimization` and `pyhpp.core.problem_target` submodules. + +### Constraints (`pyhpp.constraints`) + +```python +from pyhpp.constraints import ( + Transformation, + ComparisonTypes, + ComparisonType, + Implicit, + LockedJoint, +) + +# A 6D relative-transformation function between two frames, turned into an +# implicit equality constraint on the robot's configuration space. +function = Transformation.create("placement", robot, joint, frame_in_joint, target) +constraint = Implicit.create( + function, ComparisonTypes([ComparisonType.Equality] * 6) +) +``` + +The hierarchical iterative solver (`ExplicitConstraintSet`, `HierarchicalIterativeSolver`, `BySubstitution`) lets you compose several `Implicit`/`Explicit` constraints and project configurations onto the resulting manifold. + +`pyhpp.core.static_stability_constraint_factory.StaticStabilityConstraintsFactory` builds static-stability constraints (e.g. for legged or manipulation problems) on top of these primitives. + +### Manipulation problems (`pyhpp.manipulation`) + +```python +from pyhpp.manipulation import Device, Graph, Problem, urdf, ManipulationPlanner +from pyhpp.manipulation.constraint_graph_factory import ConstraintGraphFactory +from pyhpp.manipulation.security_margins import SecurityMargins + +robot = Device("ur3-and-objects") +urdf.loadModel(robot, 0, "ur3", "anchor", UR3_URDF, UR3_SRDF, SE3.Identity()) +# ... load additional robots / objects into the same Device ... + +problem = Problem(robot) +graph = Graph("graph", problem) + +factory = ConstraintGraphFactory(graph) +factory.setGrippers(["ur3/gripper"]) +factory.setObjects(["sphere"], [["sphere/handle"]], [[]]) +factory.generate() + +margins = SecurityMargins(problem, factory, robotsAndObjects, robot) +margins.setSecurityMarginBetween("ur3", "sphere", 0.01) +margins.apply() -Script `doc/configure.py` is used to generate documentation of Python objects from C++ objects. -It reads the XML documentation generated by doxygen so the dependencies must provide such -files. To make it work, do: -- Add `GENERATE_XML=YES` into `doc/Doxyfile.extra.in` of all the dependency of this project. -- Install the generated documentation by adding the following lines to `CMakeLists.txt`: -```cmake -IF(_INSTALL_DOC) - INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/doxygen-xml - DESTINATION ${CMAKE_INSTALL_DOCDIR}) -ENDIF() +planner = ManipulationPlanner(problem, problem.roadmap()) ``` -File `doc/configure.py` contains a short documentation of how to document the bindings. +`ConstraintGraphFactory` builds a constraint graph (nodes, edges, grippers, handles) declaratively from lists of grippers and objects, instead of assembling `Graph`/`Implicit`/`LockedJoint` objects by hand. `SecurityMargins` then lets you set per-pair collision security margins across the whole graph in one call. + +### Diagnosing constraint errors (`pyhpp.tools`) + +```python +from pyhpp.tools.constraint_error import describe_error + +entries, satisfied = describe_error(config_projector, q) +for entry in entries: + status = "OK" if entry["satisfied"] else "FAILED" + print(f"{entry['name']} ({entry['kind']}): |error| = {entry['norm']:.3g} [{status}]") +``` + +`pyhpp.tools.xacro.process_xacro(*args)` resolves a xacro file into a URDF string, transparently picking up ROS 2 resource paths from `AMENT_PREFIX_PATH` when available. + +## Tests + +Unit tests (`tests/unit/`) use Python's built-in `unittest` framework, with shared robot/problem fixtures in `tests/unit/conftest.py` (`create_ur5_problem`, `create_ur3_robot`, ...). One test file typically covers one bound module: `test_problem.py`, `test_path_planner.py`, `test_path_optimizer.py`, `test_path_projector.py`, `test_path_validation.py`, `test_steering_method.py`, `test_configuration_shooter.py`, `test_constraint_factory.py`, `test_constraint_graph_factory.py`, `test_security_margins.py`, `test_static_stability.py`, `test_device.py`, `test_handles_grippers.py`, `test_liegroup.py`, `test_differentiable_function.py`, `test_position_constraint.py`, `test_roadmap.py`. + +Integration tests (`tests/integration/`) run complete planning scenarios end to end, e.g. `pr2-in-iai-kitchen.py`, `romeo-placard.py`, `ur3-spheres.py` / `ur3-spheres-spf.py`, and `construction-set-m-rrt.py`, plus `test_benchmarks.py` / `benchmark_utils.py` for timing-oriented benchmarks. + +Run them via CMake (`make test`, requires `BUILD_TESTING=ON`) or directly with `pytest`/`unittest` once the package is on your `PYTHONPATH`. + +## Documentation generation + +Doxygen comments in the upstream C++ libraries can be injected into the Python docstrings exposed by these bindings: + +1. In every dependency's `doc/Doxyfile.extra.in`, set `GENERATE_XML=YES`. +2. Install the generated XML documentation from each dependency's `CMakeLists.txt`: + ```cmake + IF(_INSTALL_DOC) + INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/doxygen-xml + DESTINATION ${CMAKE_INSTALL_DOCDIR}) + ENDIF() + ``` +3. `doc/configure.py` then reads that XML (via `doc/doxygen_xml_parser.py`) and rewrites special in-place comments in the `.cc` binding sources: + - `DocNamespace(namespace)` sets the current C++ namespace (mapped to a Python package via `nsToPackage`). + - `DocClass(classname)` sets the current class being documented. + - `DocClassMethod(methodname, classname=None)` is replaced by the corresponding Doxygen-extracted docstring. + +See the header comment of `doc/configure.py` for the full syntax and a worked example. + +> **TODO** (tracked in the source): use Doxygen to generate XML documentation of the headers included by a given binding file, then use that generated XML to auto-update the documentation comments in `src/pyhpp` (Doxygen's `INCLUDE_PATH` / `SEARCH_INCLUDES` options may help here). -### TODO +## License -- Use doxygen to generate XML documentation of the headers included by a file. - Then use the generated XML doc to update the documentation of the files in `src/pyhpp`. - Doxygen configuration variables `INCLUDE_PATH` and `SEARCH_INCLUDES` might be helpful. +`hpp-python` is released under the [BSD 2-Clause License](LICENSE), Copyright (c) 2018-2025, CNRS. From 29c975a643aba8fba6238d6d6c164cd08fe1bcdc Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Mon, 6 Jul 2026 11:27:05 +0200 Subject: [PATCH 02/18] Add Python docstrings to pyhpp/core, constraints and manipulation bindings - core: document PathPlanner.maxIterations/timeOut, TrapezoidalTimeParameterization properties, ConfigProjector.lineSearchType/maxIterations/errorThreshold, Node.connectedComponent setter, SteeringMethod.constraints getter - constraints: document DifferentiableFunction call/properties, Implicit.comparisonType getter/setter (const char* to avoid kwargs mismatch), ExplicitConstraintSet.errorSize, HierarchicalIterative and BySubstitution errorThreshold/maxIterations/rightHandSide* - manipulation: document Problem.constraintGraph, TransitionPlanner.innerPlanner/ checkFeasibilityOnly/nRandomConfig/nDiscreteSteps, Handle properties, EndEffectorTrajectorySteeringMethod, Cartesian steering method - All getter/setter pairs with /// on both sides use const char* constants instead of DocClassMethod to avoid Boost.Python more_keywords_than_function_arguments static_assert - Add doc/core_doc_todo.md, doc/constraints_doc_todo.md, doc/manipulation_doc_todo.md with full status tables for all binding files --- doc/constraints_doc_todo.md | 158 +++++++ doc/core_doc_todo.md | 394 ++++++++++++++++++ doc/manipulation_doc_todo.md | 237 +++++++++++ src/pyhpp/constraints/by-substitution.cc | 47 ++- .../constraints/differentiable-function.cc | 27 +- .../constraints/explicit-constraint-set.cc | 3 +- src/pyhpp/constraints/implicit.cc | 10 +- src/pyhpp/constraints/iterative-solver.cc | 52 ++- src/pyhpp/core/constraint.cc | 30 +- src/pyhpp/core/node.cc | 3 +- src/pyhpp/core/path-optimizer.cc | 18 +- src/pyhpp/core/path-planner.cc | 22 +- src/pyhpp/core/steering-method.cc | 2 +- src/pyhpp/manipulation/device.cc | 37 +- src/pyhpp/manipulation/path-planner.cc | 41 +- src/pyhpp/manipulation/problem.cc | 17 +- src/pyhpp/manipulation/steering-method.cc | 19 +- 17 files changed, 1055 insertions(+), 62 deletions(-) create mode 100644 doc/constraints_doc_todo.md create mode 100644 doc/core_doc_todo.md create mode 100644 doc/manipulation_doc_todo.md diff --git a/doc/constraints_doc_todo.md b/doc/constraints_doc_todo.md new file mode 100644 index 00000000..afd41cb5 --- /dev/null +++ b/doc/constraints_doc_todo.md @@ -0,0 +1,158 @@ +# pyhpp.constraints — documentation status + +## Legend + +- ✅ **Documented** — docstring present in the binding +- ❌ **No C++ doc** — C++ method has no `///` → nothing to add +- ⚠️ **Python-only** — no direct C++ equivalent → doc written in binding or to be written +- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert + +--- + +## differentiable-function.cc + +### DifferentiableFunction + +| Python method / property | Status | Note | +|---|---|---| +| `__call__` | ✅ | `DOC_DF_CALL` from `operator()` in `differentiable-function.hh` | +| `J` | ✅ | Inline string (Python-only: returns Jacobian as numpy array) | +| `name` | ✅ | `DocClassMethod(name)` | +| `ni` | ✅ | `DOC_DF_NI` from `inputSize()` in `differentiable-function.hh` | +| `no` | ✅ | `DOC_DF_NO` from `outputSize()` | +| `ndi` | ✅ | `DOC_DF_NDI` from `inputDerivativeSize()` | +| `ndo` | ✅ | `DOC_DF_NDO` from `outputDerivativeSize()` | +| `value` | ✅ | `DocClassMethod(value)` | +| `jacobian` | ✅ | `DocClassMethod(jacobian)` | +| `outputSpace` | ✅ | `DocClassMethod(outputSpace)` | +| `inputSize` | ✅ | `DocClassMethod(inputSize)` | +| `outputSize` | ✅ | `DocClassMethod(outputSize)` | +| `inputDerivativeSize` | ✅ | `DocClassMethod(inputDerivativeSize)` | +| `outputDerivativeSize` | ✅ | `DocClassMethod(outputDerivativeSize)` | +| `impl_compute` | ❌ No C++ doc | Protected pure virtual, no `///` in the public interface | +| `impl_jacobian` | ❌ No C++ doc | Same | + +### Manipulability / MinManipulability + +| Python method | Status | Note | +|---|---|---| +| `Manipulability.__init__` | ⚠️ Python-only | Factory `Manipulability::create` | +| `Manipulability.lockJoint` | ❌ No C++ doc | No `///` in `manipulability.hh` | +| `MinManipulability.__init__` | ⚠️ Python-only | Factory `MinManipulability::create` | +| `MinManipulability.lockJoint` | ❌ No C++ doc | Same | + +--- + +## implicit.cc + +### Implicit + +| Python method | Status | Note | +|---|---|---| +| `__init__` | ⚠️ Python-only | Wrapper for `Implicit::create` | +| `comparisonType` (getter) | ✅ | `DOC_COMPARISONTYPE_GET` (`DocClassMethod` unsafe: setter has `comp` param → kwargs mismatch) | +| `comparisonType` (setter) | ✅ | `DOC_COMPARISONTYPE_SET` | +| `function` | ✅ | `DocClassMethod(function)` | +| `parameterSize` | ✅ | `DocClassMethod(parameterSize)` | +| `rightHandSideSize` | ✅ | `DocClassMethod(rightHandSideSize)` | +| `getFunctionOutputSize` | ⚠️ Python-only | Static method, no direct C++ equivalent | + +--- + +## explicit.cc + +| Python method | Status | Note | +|---|---|---| +| `createExplicit` | ⚠️ Python-only | Wrapper around `Explicit::create` | + +--- + +## explicit-constraint-set.cc + +### ExplicitConstraintSet + +| Python method | Status | Note | +|---|---|---| +| `add` | ✅ | `DocClassMethod(add)` | +| `errorSize` | ✅ | `DocClassMethod(errorSize)` | + +--- + +## iterative-solver.cc + +### HierarchicalIterative + +| Method / property | Status | Note | +|---|---|---| +| `__str__` | ⚠️ Python-only | Uses `to_str` | +| `add` | ✅ | `DocClassMethod(add)` | +| `errorThreshold` (1st add_property) | ✅ | `DOC_HI_ERRORTHRESHOLD` from `hierarchical-iterative.hh` | +| `errorThreshold` (2nd add_property, duplicate) | ❌ | Pre-existing duplicate in the code, no docstring | +| `rightHandSideFromConfig` (config only) | ✅ | `DOC_HI_RHSFC1` from `hierarchical-iterative.hh` | +| `rightHandSideFromConfig` (constraint + config) | ✅ | `DOC_HI_RHSFC2` | +| `rightHandSide` (setter constraint + rhs) | ✅ | `DOC_HI_RHS_SET1` | +| `rightHandSide` (setter rhs only) | ✅ | `DOC_HI_RHS_SET2` | +| `rightHandSide` (getter) | ✅ | `DOC_HI_RHS_GET` | +| `maxIterations` (add_property) | ✅ | `DOC_HI_MAXITERATIONS` from `hierarchical-iterative.hh` | +| `lastIsOptional` (add_property) | ❌ No C++ doc | No `///` in `hierarchical-iterative.hh` (uses `//`) | +| `solveLevelByLevel` (add_property) | ❌ No C++ doc | Same (`//` non-doxygen) | +| `numberStacks` | ❌ No C++ doc | No `///` | +| `constraintsForPriority` | ⚠️ Python-only | Wrapper returning a Python list | +| `dimension` | ❌ No C++ doc | No `///` on the exposed method | + +--- + +## by-substitution.cc + +### BySubstitution + +| Method / property | Status | Note | +|---|---|---| +| `SolverStatus` (enum) | ❌ No C++ doc | Enum without class-level `///` | +| `explicitConstraintSetHasChanged` | ✅ | `DocClassMethod(explicitConstraintSetHasChanged)` | +| `solve` | ⚠️ Signature diff. | Python wrapper returns `(qout, status)` tuple, hides output arg | +| `explicitConstraintSet` | ✅ | `DocClassMethod(explicitConstraintSet)` | +| `rightHandSideFromConfig` (config only) | ✅ | `DOC_BS_RHSFC1` from `by-substitution.hh` | +| `rightHandSideFromConfig` (constraint + config) | ✅ | `DOC_BS_RHSFC2` | +| `rightHandSide` (setter constraint + rhs) | ✅ | `DOC_BS_RHS_SET1` | +| `rightHandSide` (setter rhs only) | ✅ | `DOC_BS_RHS_SET2` | +| `rightHandSide` (getter) | ✅ | `DOC_BS_RHS_GET` | +| `errorThreshold` (add_property) | ✅ | `DOC_BS_ERRORTHRESHOLD` from `by-substitution.hh` | +| `describeError` | ⚠️ Python-only | Wrapper decomposing error by constraint | + +--- + +## generic-transformation.cc + +| Python class | Status | Note | +|---|---|---| +| `Position.__init__` | ✅ | Inline string (absolute generic transformation) | +| `Orientation.__init__` | ✅ | Inline string | +| `Transformation.__init__` | ✅ | Inline string | +| `RelativePosition.__init__` | ✅ | Inline string (relative generic transformation) | +| `RelativeOrientation.__init__` | ✅ | Inline string | +| `RelativeTransformation.__init__` | ✅ | Inline string | +| `RelativeTransformationR3xSO3.__init__` | ✅ | Inline string | + +--- + +## locked-joint.cc + +### LockedJoint + +| Python method | Status | Note | +|---|---|---| +| `__init__` (joint + config) | ⚠️ Python-only | `createLockedJoint`: wrapper without direct C++ equivalent | +| `__init__` (joint + config + comp) | ⚠️ Python-only | `createLockedJointWithComp`: same | + +--- + +## relative-com.cc + +### RelativeCom + +| Python method | Status | Note | +|---|---|---| +| `__init__` (create1: name+robot+joint+ref+mask) | ✅ | `DocClassMethod(create)` | +| `__init__` (create2: robot+comc+joint+ref+mask) | ⚠️ Python-only | No dedicated `///` in `relative-com.hh` | +| `__init__` (create3: name+robot+comc+joint+ref+mask) | ⚠️ Python-only | Same | diff --git a/doc/core_doc_todo.md b/doc/core_doc_todo.md new file mode 100644 index 00000000..994b56a1 --- /dev/null +++ b/doc/core_doc_todo.md @@ -0,0 +1,394 @@ +# pyhpp.core — documentation status + +## Legend + +- ✅ **Documented** — docstring present in the binding +- ❌ **No C++ doc** — C++ method has no `///` → nothing to add +- ⚠️ **Python-only** — no direct C++ equivalent → wrapper or modified signature +- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert + +--- + +## path.cc + +### Path + +| Python method | Status | Note | +|---|---|---| +| `__call__` (×2) | ⚠️ Python-only | Wrappers returning `(q, success)` and `success` | +| `eval` (×2) | ⚠️ Python-only | Same | +| `derivative` | ⚠️ Python-only | Wrapper returning a numpy vector | +| `constraints` | ❌ No C++ doc | No `///` on `constraints()` in `path.hh` | +| `copy` | ✅ | `DocClassMethod(copy)` | +| `extract` | ✅ | `DocClassMethod(extract)` | +| `timeRange` | ✅ | `DocClassMethod(timeRange)` | +| `reverse` | ✅ | `DocClassMethod(reverse)` | +| `paramRange` | ✅ | `DocClassMethod(paramRange)` | +| `length` | ✅ | `DocClassMethod(length)` | +| `initial` | ✅ | `DocClassMethod(initial)` | +| `end` | ✅ | `DocClassMethod(end)` | +| `outputSize` | ✅ | `DocClassMethod(outputSize)` | +| `outputDerivativeSize` | ✅ | `DocClassMethod(outputDerivativeSize)` | +| `StraightPath.__init__` | ⚠️ Python-only | Factory wrapper for `StraightPath::create` | + +--- + +## problem.cc + +### Problem + +| Method / property | Status | Note | +|---|---|---| +| `robot` | ✅ | `DocClassMethod(robot)` | +| `setParameter` | ✅ | `DocClassMethod(setParameter)` | +| `getParameter` | ✅ | `DocClassMethod(getParameter)` | +| `addConfigValidation` | ✅ | `DocClassMethod(addConfigValidation)` | +| `clearConfigValidations` | ✅ | `DocClassMethod(clearConfigValidations)` | +| `initConfig` | ✅ | `DocClassMethod(initConfig)` | +| `addGoalConfig` | ✅ | `DocClassMethod(addGoalConfig)` | +| `resetGoalConfigs` | ✅ | `DocClassMethod(resetGoalConfigs)` | +| `steeringMethod` (getter/setter) | ❌ No C++ doc | Python wrapper struct; no `///` in `problem.hh` | +| `configValidation` (getter/setter) | ❌ No C++ doc | Same | +| `pathValidation` (getter/setter) | ❌ No C++ doc | Same | +| `pathProjector` (getter/setter) | ❌ No C++ doc | Same | +| `distance` (getter/setter) | ❌ No C++ doc | Same | +| `target` (getter/setter) | ❌ No C++ doc | Same | +| `configurationShooter` (getter/setter) | ❌ No C++ doc | Same | +| `errorThreshold` (def_readwrite) | ⚠️ Python-only | Python wrapper field — default threshold for ConfigProjector creation | +| `maxIterProjection` (def_readwrite) | ⚠️ Python-only | Same | +| `addPartialCom` | ⚠️ Python-only | | +| `getPartialCom` | ⚠️ Python-only | | +| `createRelativeComConstraint` | ⚠️ Python-only | | +| `createTransformationConstraint` (×2) | ⚠️ Python-only | | +| `setConstantRightHandSide` | ⚠️ Python-only | | +| `applyConstraints` | ⚠️ Python-only | | +| `isConfigValid` | ⚠️ Python-only | | +| `setConstraints` | ⚠️ Python-only | | +| `getConstraints` | ⚠️ Python-only | | +| `setRightHandSideFromConfig` | ⚠️ Python-only | | +| `addNumericalConstraintsToConfigProjector` (×2) | ⚠️ Python-only | | +| `createComBetweenFeet` | ⚠️ Python-only | | +| `directPath` | ⚠️ Signature diff. | Returns `(valid, path, report)`; hides output params | + +--- + +## roadmap.cc + +### Roadmap + +| Python method | Status | Note | +|---|---|---| +| `clear` | ✅ | `DocClassMethod(clear)` | +| `nodesWithinBall` | ✅ | `DocClassMethod(nodesWithinBall)` | +| `addEdges` | ✅ | `DocClassMethod(addEdges)` | +| `merge` | ✅ | `DocClassMethod(merge)` | +| `insertPathVector` | ✅ | `DocClassMethod(insertPathVector)` | +| `addGoalNode` | ✅ | `DocClassMethod(addGoalNode)` | +| `resetGoalNodes` | ✅ | `DocClassMethod(resetGoalNodes)` | +| `pathExists` | ✅ | `DocClassMethod(pathExists)` | +| `goalNodes` | ✅ | `DocClassMethod(goalNodes)` | +| `distance` | ✅ | `DocClassMethod(distance)` | +| `addNode` | ⚠️ Python-only | Wrapper on `roadmap.addNode(config)` | +| `nearestNode` (×4) | ⚠️ Python-only | Return `(config, minDistance)` | +| `nearestNodes` (×2) | ⚠️ Python-only | | +| `addNodeAndEdges` | ⚠️ Python-only | | +| `addNodeAndEdge` | ⚠️ Python-only | | +| `addEdge` (×2) | ⚠️ Python-only | | +| `nodes` | ⚠️ Python-only | Returns list of configurations | +| `nodesConnectedComponent` | ⚠️ Python-only | | +| `initNode` (×2) | ⚠️ Python-only | | +| `connectedComponents` | ⚠️ Python-only | | +| `numberConnectedComponents` | ⚠️ Python-only | | +| `getConnectedComponent` | ⚠️ Python-only | | +| `connectedComponentOfNode` | ⚠️ Python-only | | + +--- + +## path-planner.cc + +### PathPlanner + +| Python method | Status | Note | +|---|---|---| +| `roadmap` | ✅ | `DocClassMethod(roadmap)` | +| `problem` | ✅ | `DocClassMethod(problem)` | +| `startSolve` | ✅ | `DocClassMethod(startSolve)` | +| `solve` | ✅ | `DocClassMethod(solve)` | +| `tryConnectInitAndGoals` | ✅ | `DocClassMethod(tryConnectInitAndGoals)` | +| `oneStep` | ✅ | `DocClassMethod(oneStep)` | +| `finishSolve` | ✅ | `DocClassMethod(finishSolve)` | +| `interrupt` | ✅ | `DocClassMethod(interrupt)` | +| `stopWhenProblemIsSolved` | ✅ | `DocClassMethod(stopWhenProblemIsSolved)` | +| `computePath` | ✅ | `DocClassMethod(computePath)` | +| `maxIterations` (getter) | ✅ | `DOC_PP_MAXITER_GET` from `path-planner.hh` | +| `maxIterations` (setter) | ✅ | `DOC_PP_MAXITER_SET` (getter/setter pair → `DocClassMethod` unsafe on getter) | +| `timeOut` (getter) | ✅ | `DOC_PP_TIMEOUT_GET` from `path-planner.hh` | +| `timeOut` (setter) | ✅ | `DOC_PP_TIMEOUT_SET` | + +--- + +## path-optimizer.cc + +### PathOptimizer + +| Python method | Status | Note | +|---|---|---| +| `problem` | ✅ | `DocClassMethod(problem)` | +| `optimize` | ✅ | `DocClassMethod(optimize)` | +| `interrupt` | ✅ | `DocClassMethod(interrupt)` | +| `maxIterations` | ✅ | `DocClassMethod(maxIterations)` (setter-only, safe) | +| `timeOut` | ✅ | `DocClassMethod(timeOut)` (setter-only, safe) | + +### TrapezoidalTimeParameterization + +| Python property | Status | Note | +|---|---|---| +| `maxVelocity` | ✅ | `DOC_TTP_MAXVEL` from `trapezoidal-time-parameterization.hh` | +| `maxAcceleration` | ✅ | `DOC_TTP_MAXACC` | +| `minimumDuration` | ✅ | `DOC_TTP_MINDUR` | + +### SimpleTimeParameterization + +| Python property | Status | Note | +|---|---|---| +| `safety` | ❌ No C++ doc | `def_readwrite`; no `///` in `simple-time-parameterization.hh` | +| `order` | ❌ No C++ doc | Same | +| `maxAcceleration` | ❌ No C++ doc | Same | + +### SplineGradientBased (×3 templates) + +| Python property | Status | Note | +|---|---|---| +| `alphaInit`, `alwaysStopAtFirst`, `costOrder`, etc. | ❌ No C++ doc | `def_readwrite` on public fields without `///` | + +--- + +## steering-method.cc + +### SteeringMethod + +| Python method | Status | Note | +|---|---|---| +| `steer` | ✅ | `DocClassMethod(steer)` | +| `problem` | ✅ | `DocClassMethod(problem)` | +| `constraints` (setter) | ✅ | `DocClassMethod(constraints)` | +| `constraints` (getter) | ✅ | `"Get constraint set."` inline (getter/setter pair → `DocClassMethod` unsafe on getter) | +| `__call__` | ⚠️ Python-only | Wrapper on `operator()` | + +--- + +## node.cc + +### Node + +| Python method | Status | Note | +|---|---|---| +| `addOutEdge` | ✅ | `DocClassMethod(addOutEdge)` | +| `addInEdge` | ✅ | `DocClassMethod(addInEdge)` | +| `connectedComponent` (getter) | ❌ No C++ doc | No `///` on the getter in `node.hh` | +| `connectedComponent` (setter) | ✅ | `"Store the connected component the node belongs to."` inline | +| `outEdges` | ✅ | `DocClassMethod(outEdges)` | +| `inEdges` | ✅ | `DocClassMethod(inEdges)` | +| `isOutNeighbor` | ✅ | `DocClassMethod(isOutNeighbor)` | +| `isInNeighbor` | ✅ | `DocClassMethod(isInNeighbor)` | +| `configuration` | ✅ | `DocClassMethod(configuration)` | + +--- + +## constraint.cc + +### Constraint + +| Python method | Status | Note | +|---|---|---| +| `name` | ✅ | `DocClassMethod(name)` | +| `apply` | ⚠️ Signature diff. | Wrapper modifying `q` in-place | +| `isSatisfied` (×2) | ⚠️ Signature diff. | Wrappers modifying `error` in-place | +| `copy` | ⚠️ Python-only | Static wrapper | + +### ConstraintSet + +| Python method | Status | Note | +|---|---|---| +| `addConstraint` | ✅ | `DocClassMethod(addConstraint)` | +| `configProjector` | ✅ | `DocClassMethod(configProjector)` | + +### ConfigProjector + +| Method / property | Status | Note | +|---|---|---| +| `solver` | ✅ | `DocClassMethod(solver)` | +| `lineSearchType` (add_property) | ✅ | `DOC_CP_LINESEARCHTYPE` from `config-projector.hh` | +| `add` | ✅ | `DocClassMethod(add)` | +| `lastIsOptional` (getter/setter) | ❌ No C++ doc | No `///` in `config-projector.hh` | +| `maxIterations` (getter) | ✅ | `DOC_CP_MAXITER_GET` | +| `maxIterations` (setter) | ✅ | `DOC_CP_MAXITER_SET` | +| `errorThreshold` (getter) | ✅ | `DOC_CP_ERRTHRESH_GET` | +| `errorThreshold` (setter) | ✅ | `DOC_CP_ERRTHRESH_SET` | +| `residualError` | ✅ | `DocClassMethod(residualError)` | +| `sigma` | ✅ | `DocClassMethod(sigma)` | +| `setRightHandSideFromConfig` | ⚠️ Python-only | | +| `setRightHandSideOfConstraint` | ⚠️ Python-only | | +| `numericalConstraints` | ⚠️ Python-only | Returns a Python list | + +--- + +## distance.cc + +### Distance + +| Python method | Status | Note | +|---|---|---| +| `compute` | ✅ | `DocClassMethod(compute)` | + +### WeighedDistance + +| Python method | Status | Note | +|---|---|---| +| `asDistancePtr_t` | ⚠️ Python-only | | +| `getWeights` | ⚠️ Python-only | Wrapper on `dist->weights()` | +| `setWeights` | ⚠️ Python-only | Wrapper on `dist->weights(weights)` | + +--- + +## connected-component.cc + +### ConnectedComponent + +| Python method | Status | Note | +|---|---|---| +| `nodes` | ✅ | `DocClassMethod(nodes)` (note: Python wrapper returns configurations, not node objects) | +| `reachableFrom` | ✅ | `DocClassMethod(reachableFrom)` | +| `reachableTo` | ✅ | `DocClassMethod(reachableTo)` | +| `__eq__` | ⚠️ Python-only | Raw pointer comparison | + +--- + +## configuration-shooter.cc + +### ConfigurationShooter + +| Python method | Status | Note | +|---|---|---| +| `shoot` | ✅ | `DocClassMethod(shoot)` | + +--- + +## config-validation.cc + +### ConfigValidation + +| Python method | Status | Note | +|---|---|---| +| `validate` | ✅ | `DocClassMethod(validate)` via `PYHPP_DEFINE_METHOD2` | +| `validate` (py tuple) | ⚠️ Signature diff. | Returns `(bool, report)` | + +### ConfigValidations + +| Python method | Status | Note | +|---|---|---| +| `add` | ✅ | `DocClassMethod(add)` via `PYHPP_DEFINE_METHOD2` | +| `numberConfigValidations` | ✅ | `DocClassMethod(numberConfigValidations)` | +| `clear` | ❌ No C++ doc | No `///` in `config-validations.hh` | + +--- + +## parameter.cc + +### Parameter + +| Python method | Status | Note | +|---|---|---| +| `boolValue`, `intValue`, `floatValue`, `stringValue`, `vectorValue`, `matrixValue` | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring; no `///` in `parameter.hh` | +| `value` | ⚠️ Python-only | Converts to Python object by type | +| `create_bool` | ⚠️ Python-only | Factory for `bool` | + +--- + +## path-projector.cc + +### PathProjector + +| Python method | Status | Note | +|---|---|---| +| `apply` | ✅ | `DocClassMethod(apply)` | +| `apply` (py tuple) | ⚠️ Signature diff. | Returns `(bool, projPath)` | +| Factories (NoneProjector, ProgressiveProjector, etc.) | ⚠️ Python-only | | + +--- + +## path-validation.cc + +### PathValidation + +| Python method | Status | Note | +|---|---|---| +| `validate` | ✅ | `DocClassMethod(validate)` | +| `validate` (py tuple) | ⚠️ Signature diff. | Returns `(bool, validPart, report)` | +| `validateConfiguration` | ⚠️ Python-only | Returns `(bool, report)` | +| Factories (Discretized, Progressive, Dichotomy, etc.) | ⚠️ Python-only | | + +--- + +## problem-target.cc + +### ProblemTarget + +No methods exposed. + +--- + +## reports.cc + +### ValidationReport / CollisionValidationReport / JointBoundValidationReport / PathValidationReport + +| Exposed | Status | Note | +|---|---|---| +| All `def_readonly`/`def_readwrite` fields | ❌ No C++ doc | No `///` in the corresponding headers | + +--- + +## path/vector.cc + +### PathVector (exposed as `Vector`) + +| Python method | Status | Note | +|---|---|---| +| `__init__` | ✅ | Inline string | +| `numberPaths` | ✅ | `DocClassMethod(numberPaths)` | +| `pathAtRank` | ✅ | `DocClassMethod(pathAtRank)` | +| `rankAtParam` | ✅ | `DocClassMethod(rankAtParam)` | +| `appendPath` | ✅ | `DocClassMethod(appendPath)` | +| `concatenate` | ✅ | `DocClassMethod(concatenate)` | +| `flatten` | ✅ | `DocClassMethod(flatten)` | + +--- + +## path/spline.cc + +### SplineB1, SplineB3 + +| Python method | Status | Note | +|---|---|---| +| All methods | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring | + +--- + +## problem_target/goal-configurations.cc + +### GoalConfigurations + +| Python method | Status | Note | +|---|---|---| +| `computePath` | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring | +| `reached` | ❌ No C++ doc | Same | + +--- + +## path_optimization/spline-gradient-based-abstract.cc + +### SplineGradientBasedAbstractB1/B3, LinearConstraint, QuadraticProgram + +| Python method / property | Status | Note | +|---|---|---| +| All methods and properties | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` or `add_property`/`def_readwrite` without `///` | diff --git a/doc/manipulation_doc_todo.md b/doc/manipulation_doc_todo.md new file mode 100644 index 00000000..f131fcae --- /dev/null +++ b/doc/manipulation_doc_todo.md @@ -0,0 +1,237 @@ +# pyhpp.manipulation — documentation status + +## Legend + +- ✅ **Documented** — docstring present in the binding +- ❌ **No C++ doc** — C++ method has no `///` → `DocClassMethod` would produce an empty string +- ⚠️ **Python-only** — no direct C++ equivalent → doc to be written in the binding +- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert + +--- + +## graph.cc + +### State + +| Python method | Status | Note | +|---|---|---| +| `State.name` | ✅ | `DocClassMethod(name)` | +| `State.id` | ✅ | `DocClassMethod(id)` | +| `State.configConstraint` | ❌ No C++ doc | `State::configConstraint()` declared without `///` in `state.hh` | +| `State.neighborEdges` | ✅ | `DOC_NEIGHBOREDGES` inline | + +### Transition (Edge) + +| Python method | Status | Note | +|---|---|---| +| `Transition.id` | ✅ | `DocClassMethod(id)` | +| `Transition.name` | ✅ | `DocClassMethod(name)` | +| `Transition.isWaypointTransition` | ✅ | `DocClassMethod(isWaypointEdge)` | +| `Transition.nbWaypoints` | ✅ | `DocClassMethod(nbWaypoints)` | +| `Transition.waypoint` | ✅ | `DocClassMethod(waypoint)` | +| `Transition.pathValidation` | ✅ | `DocClassMethod(pathValidation)` | + +### Graph + +| Method / property | Status | Note | +|---|---|---| +| `Graph._get_native_graph` | ⚠️ Python-only | C++ capsule for external interop | +| `Graph.robot` | ⚠️ Python-only | `def_readwrite`, no `///` | +| `Graph.maxIterations` | ❌ No C++ doc | `Graph::maxIterations()` has `///` in `graph.hh` but exposed via `PYHPP_DEFINE_GETTER_SETTER` without docstring | +| `Graph.errorThreshold` | ❌ No C++ doc | Same | +| `Graph.createState` | ✅ | `DOC_CREATESTATE` inline | +| `Graph.createTransition` | ✅ | `DOC_CREATETRANSITION` inline | +| `Graph.createWaypointTransition` | ✅ | `DOC_CREATEWAYPOINTTRANSITION` inline | +| `Graph.createLevelSetTransition` | ✅ | `DOC_CREATELEVELSETTRANSITION` inline | +| `Graph.setContainingNode` | ✅ | `DOC_SETCONTAININGNODE` inline | +| `Graph.getContainingNode` | ✅ | `DOC_GETCONTAININGNODE` inline | +| `Graph.setShort` | ✅ | `DOC_SETSHORT` inline | +| `Graph.isShort` | ✅ | `DOC_ISSHORT` inline | +| `Graph.getNodesConnectedByTransition` | ✅ | `DOC_GETNODESCONNECTEDBYTRANSITION` inline | +| `Graph.setWeight` | ✅ | `DOC_SETWEIGHT` inline | +| `Graph.getWeight` | ✅ | `DOC_GETWEIGHT` inline | +| `Graph.setWaypoint` | ✅ | `DOC_SETWAYPOINT` inline | +| `Graph.getState` *(by name)* | ⚠️ Python-only | Lookup by name in internal map | +| `Graph.getTransition` *(by name)* | ⚠️ Python-only | Lookup by name in internal map | +| `Graph.getStates` | ⚠️ Python-only | Returns list of `PyWState` | +| `Graph.getTransitions` | ⚠️ Python-only | Returns list of `PyWEdge` | +| `Graph.getStateNames` | ⚠️ Python-only | Returns list of names | +| `Graph.getTransitionNames` | ⚠️ Python-only | Returns list of names | +| `Graph.getStateFromConfiguration` | ✅ | `DOC_GETSTATE` inline | +| `Graph.addNumericalConstraint` | ✅ | `DOC_ADDNUMERICALCONSTRAINT` inline | +| `Graph.addNumericalConstraintsToState` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOSTATE` inline | +| `Graph.addNumericalConstraintsToTransition` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOTRANSITION` inline | +| `Graph.addNumericalConstraintsToGraph` | ⚠️ Python-only | Takes a list, no direct equivalent | +| `Graph.addNumericalConstraintsForPath` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSFORPATH` inline | +| `Graph.getNumericalConstraintsForState` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFORSTATE` inline | +| `Graph.getNumericalConstraintsForEdge` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFOREDGE` inline | +| `Graph.getNumericalConstraintsForGraph` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFORGRAPH` inline | +| `Graph.resetConstraints` | ✅ | `DOC_RESETCONSTRAINTS` inline | +| `Graph.registerConstraints` | ✅ | `DOC_REGISTERCONSTRAINTS` inline | +| `Graph.createPlacementConstraint` | ✅ | `DOC_CREATEPLACEMENTCONSTRAINT` inline | +| `Graph.createPrePlacementConstraint` | ✅ | `DOC_CREATEPREPLACEMENTCONSTRAINT` inline | +| `Graph.createGraspConstraint` | ⚠️ Python-only | Wrapper around `Handle::createGrasp`, no `///` | +| `Graph.createPreGraspConstraint` | ⚠️ Python-only | Wrapper around `Handle::createPreGrasp` | +| `Graph.getConfigErrorForState` | ✅ | `DOC_GETCONFIGERRORFORSTATE` inline | +| `Graph.getConfigErrorForTransition` | ✅ | `DOC_GETCONFIGERRORFORTRANSITION` inline | +| `Graph.getConfigErrorForTransitionLeaf` | ✅ | `DOC_GETCONFIGERRORFORTRANSITIONLEAF` inline | +| `Graph.getConfigErrorForTransitionTarget` | ✅ | `DOC_GETCONFIGERRORFORTRANSITIONTARGET` inline | +| `Graph.applyStateConstraints` | ✅ | `DOC_APPLYSTATECONSTRAINTS` inline | +| `Graph.applyLeafConstraints` | ✅ | `DOC_APPLYLEAFCONSTRAINTS` inline | +| `Graph.generateTargetConfig` | ✅ | `DOC_GENERATETARGETCONFIG` inline | +| `Graph.addLevelSetFoliation` | ✅ | `DOC_ADDLEVELSETFOLIATION` inline | +| `Graph.getSecurityMarginMatrixForTransition` | ✅ | `DOC_GETSECURITYMARGINMATRIXFORTRANSITION` inline | +| `Graph.setSecurityMarginForTransition` | ✅ | `DOC_SETSECURITYMARGINFORTRANSITION` inline | +| `Graph.getRelativeMotionMatrix` | ✅ | `DOC_GETRELATIVEMOTIONMATRIX` inline | +| `Graph.removeCollisionPairFromTransition` | ✅ | `DOC_REMOVECOLLISIONPAIRFROMTRANSITION` inline | +| `Graph.createSubGraph` | ✅ | `DOC_CREATESUBGRAPH` inline | +| `Graph.setTargetNodeList` | ✅ | `DOC_SETTARGETNODELIST` inline | +| `Graph.displayStateConstraints` | ✅ | `DOC_DISPLAYSTATECONSTRAINTS` inline | +| `Graph.displayTransitionConstraints` | ✅ | `DOC_DISPLAYTRANSITIONCONSTRAINTS` inline | +| `Graph.displayTransitionTargetConstraints` | ✅ | `DOC_DISPLAYTRANSITIONTARGETCONSTRAINTS` inline | +| `Graph.display` | ✅ | `DOC_DISPLAY` inline | +| `Graph.initialize` | ✅ | `DOC_INITIALIZE` inline | +| `Graph.transitionAtParam` | ⚠️ Python-only | Static method, wraps `Graph::edgeAtParam` | + +--- + +## device.cc + +### Handle + +| Method / property | Status | Note | +|---|---|---| +| `Handle.name` | ✅ | Inline string from `handle.hh` | +| `Handle.localPosition` | ✅ | Inline string from `handle.hh` | +| `Handle.mask` | ✅ | Inline string from `handle.hh` | +| `Handle.maskComp` | ✅ | Inline string from `handle.hh` | +| `Handle.clearance` | ✅ | Inline string from `handle.hh` | +| `Handle.approachingDirection` | ✅ | Inline string from `handle.hh` | +| `Handle.createGrasp` | ✅ | `DocClassMethod(createGrasp)` | +| `Handle.createPreGrasp` | ✅ | `DocClassMethod(createPreGrasp)` | +| `Handle.createGraspComplement` | ✅ | `DocClassMethod(createGraspComplement)` | +| `Handle.createGraspAndComplement` | ✅ | `DocClassMethod(createGraspAndComplement)` | +| `Handle.getParentJointId` | ✅ | Inline string (Python-only) | + +### Device + +| Python method | Status | Note | +|---|---|---| +| `Device.setRobotRootPosition` | ❌ No C++ doc | Declared without `///` in `device.hh` | +| `Device.handles` | ❌ No C++ doc | Public member without `///` | +| `Device.grippers` | ❌ No C++ doc | Public member without `///` | +| `Device.getJointNames` | ⚠️ Python-only | Wrapper without direct C++ equivalent | +| `Device.getJointConfig` | ⚠️ Python-only | Wrapper without direct C++ equivalent | +| `Device.setJointBounds` | ⚠️ Python-only | No docstring in the binding | +| `Device.contactSurfaceNames` | ✅ | Inline string | +| `Device.contactSurfaces` | ✅ | Inline string | +| `Device.addHandle` | ✅ | Inline string | +| `Device.addGripper` | ✅ | Inline string | +| `Device.modelsInfo` | ✅ | Inline string | + +--- + +## problem.cc + +| Python method | Status | Note | +|---|---|---| +| `Problem.constraintGraph` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | +| `Problem.constraintGraph` (setter) | ✅ | Inline string | +| `Problem.checkProblem` | ✅ | `DocClassMethod(checkProblem)` | +| `Problem.steeringMethod` (getter/setter ×3) | ❌ No C++ doc | No `///` in `manipulation::Problem` | + +--- + +## path-planner.cc — TransitionPlanner + +| Python method | Status | Note | +|---|---|---| +| `innerPlanner` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | +| `innerPlanner` (setter) | ✅ | Inline string | +| `innerProblem` | ✅ | `DocClassMethod(innerProblem)` | +| `computePath` | ✅ | `DocClassMethod(computePath)` | +| `planPath` *(deprecated)* | ✅ | `DocClassMethod(planPath)` | +| `directPath` | ⚠️ Signature diff. | Hides `success` and `status` output params → kwargs count mismatch | +| `validateConfiguration` | ⚠️ Signature diff. | Hides `report` output param → kwargs count mismatch | +| `optimizePath` | ✅ | `DocClassMethod(optimizePath)` | +| `timeParameterization` | ✅ | `DocClassMethod(timeParameterization)` | +| `setEdge` *(deprecated)* | ✅ | `DocClassMethod(setEdge)` | +| `setTransition` | ✅ | `DocClassMethod(setEdge)` | +| `setReedsAndSheppSteeringMethod` | ✅ | `DocClassMethod(setReedsAndSheppSteeringMethod)` | +| `pathProjector` | ✅ | `DocClassMethod(pathProjector)` | +| `clearPathOptimizers` | ✅ | `DocClassMethod(clearPathOptimizers)` | +| `addPathOptimizer` | ✅ | `DocClassMethod(addPathOptimizer)` | + +## path-planner.cc — ManipulationPlanner / StatesPathFinder + +| Python class | Status | Note | +|---|---|---| +| `ManipulationPlanner` | ❌ No C++ doc | Constructor only, no class-level `///` | +| `StatesPathFinder` | ❌ No C++ doc | Same | + +## path-planner.cc — EndEffectorTrajectory *(deprecated)* + +| Python method | Status | Note | +|---|---|---| +| `nRandomConfig` (getter) | ✅ | Inline string (`DocClassMethod` produced empty string) | +| `nDiscreteSteps` (getter) | ✅ | Inline string | +| `checkFeasibilityOnly` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | + +--- + +## path-optimizer.cc + +| Python class / method | Status | Note | +|---|---|---| +| `EnforceTransitionSemantic` | ✅ | `DOC_ENFORCE_TRANSITION_SEMANTIC` inline | +| `RandomShortcut` | ❌ No C++ doc | No class-level `///` in `random-shortcut.hh` | +| `GraphRandomShortcut` | ⚠️ Python-only | Factory function, no direct equivalent | +| `GraphPartialShortcut` | ⚠️ Python-only | Same | +| `SplineGradientBased_bezier1/3` attributes (`alphaInit`, `alwaysStopAtFirst`, `costOrder`, `usePathLengthAsWeights`, `reorderIntervals`, `linearizeAtEachStep`, `checkJointBound`, `returnOptimum`, `costThreshold`, `guessThreshold`, `QPAccuracy`) | ❌ No C++ doc | `def_readwrite` without `///` | + +--- + +## path-projector.cc + +| Python function | Status | Note | +|---|---|---| +| `NoneProjector` | ⚠️ Python-only | | +| `ProgressiveProjector` | ⚠️ Python-only | Wraps `pathProjector::Progressive::create` | +| `DichotomyProjector` | ⚠️ Python-only | Wraps `pathProjector::Dichotomy::create` | +| `GlobalProjector` | ⚠️ Python-only | Wraps `pathProjector::Global::create` | +| `RecursiveHermiteProjector` | ⚠️ Python-only | Wraps `pathProjector::RecursiveHermite::create` | + +--- + +## steering-method.cc + +| Python class / method | Status | Note | +|---|---|---| +| `GraphSteeringMethod` (no methods) | ❌ No C++ doc | No class-level `///` | +| `EndEffectorTrajectorySteeringMethod.setTrajectoryConstraint` | ✅ | Inline string from `end-effector-trajectory.hh` | +| `EndEffectorTrajectorySteeringMethod.setTrajectory` | ✅ | Inline string from `end-effector-trajectory.hh` | + +--- + +## steering_method/cartesian.cc + +| Method / property | Status | Note | +|---|---|---| +| `Cartesian.maxIterations` | ✅ | Inline string | +| `Cartesian.errorThreshold` | ✅ | Inline string | +| `Cartesian.trajectoryConstraint` | ✅ | Inline string | +| `Cartesian.nDiscreteSteps` | ✅ | Inline string | +| `Cartesian.timeRange` | ✅ | Inline string | +| `Cartesian.setRightHandSide` (×2) | ✅ | Inline string | +| `Cartesian.getRightHandSide` | ✅ | Inline string | +| `Cartesian.planPath` | ✅ | Inline string | +| `makePiecewiseLinearTrajectory` | ✅ | Inline string | + +--- + +## urdf/util.cc + +| Python function | Status | Note | +|---|---|---| +| `loadModel` | ⚠️ Python-only | Overload that also calls `srdf::loadModelFromFile` | +| `loadModelFromString` | ⚠️ Python-only | Overload with XML string | diff --git a/src/pyhpp/constraints/by-substitution.cc b/src/pyhpp/constraints/by-substitution.cc index 30113e49..807fbc14 100644 --- a/src/pyhpp/constraints/by-substitution.cc +++ b/src/pyhpp/constraints/by-substitution.cc @@ -36,6 +36,30 @@ // DocNamespace(hpp::constraints::solver) +namespace { +const char* DOC_BS_ERRORTHRESHOLD = "Get error threshold."; +const char* DOC_BS_RHSFC1 = + "Compute right hand side of equality constraints from a configuration.\n\n" + "For each constraint of type Equality, set right hand side as rhs = f(q).\n" + "Only parameterizable constraints (type Equality) are set."; +const char* DOC_BS_RHSFC2 = + "Compute right hand side of a constraint from a configuration.\n\n" + "Set right hand side as rhs = f(q).\n" + "Only parameterizable constraints (type Equality) are set."; +const char* DOC_BS_RHS_SET1 = + "Set right hand side of a constraint.\n\n" + "Size of rhs should be equal to the total dimension of parameterizable\n" + "constraints (type Equality)."; +const char* DOC_BS_RHS_SET2 = + "Set the right hand side.\n\n" + "Size of rhs should be equal to the total dimension of parameterizable\n" + "constraints (type Equality)."; +const char* DOC_BS_RHS_GET = + "Get the right hand side.\n\n" + "Size of result is equal to total dimension of parameterizable\n" + "constraints (type Equality)."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -110,25 +134,32 @@ void exposeBySubstitution() { return_internal_reference<>(), DocClassMethod(explicitConstraintSet)) .def("rightHandSideFromConfig", static_cast( - &BySubstitution::rightHandSideFromConfig)) + &BySubstitution::rightHandSideFromConfig), + DOC_BS_RHSFC1) .def("rightHandSideFromConfig", static_cast( - &BySubstitution::rightHandSideFromConfig)) - .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &BySubstitution::rightHandSideFromConfig), + DOC_BS_RHSFC2) + .def("rightHandSide", + static_cast( + &HierarchicalIterative::rightHandSide), + DOC_BS_RHS_SET1) .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &HierarchicalIterative::rightHandSide), + DOC_BS_RHS_SET2) .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &HierarchicalIterative::rightHandSide), + DOC_BS_RHS_GET) .add_property("errorThreshold", static_cast( &BySubstitution::errorThreshold), static_cast( - &BySubstitution::errorThreshold)) + &BySubstitution::errorThreshold), + DOC_BS_ERRORTHRESHOLD) .def("describeError", &BySubstitution_describeError); } } // namespace constraints diff --git a/src/pyhpp/constraints/differentiable-function.cc b/src/pyhpp/constraints/differentiable-function.cc index 27eecfd7..021f2623 100644 --- a/src/pyhpp/constraints/differentiable-function.cc +++ b/src/pyhpp/constraints/differentiable-function.cc @@ -39,6 +39,18 @@ // DocNamespace(hpp::constraints) +namespace { +const char* DOC_DF_CALL = "Evaluate the function at a given parameter."; +const char* DOC_DF_NI = "Get dimension of input vector."; +const char* DOC_DF_NDI = + "Get dimension of input derivative vector.\n\n" + "The dimension of configuration vectors might differ from the dimension\n" + "of velocity vectors since some joints are represented by non minimal\n" + "size vectors: e.g. quaternion for SO(3)."; +const char* DOC_DF_NO = "Get dimension of output vector."; +const char* DOC_DF_NDO = "Get dimension of output derivative vector."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -105,15 +117,18 @@ void exposeDifferentiableFunction() { "DifferentiableFunction", no_init) // Pythonic API .def("__str__", &to_str) - .def("__call__", &DFWrapper::py_value) - .def("J", &DFWrapper::py_jacobian) + .def("__call__", &DFWrapper::py_value, DOC_DF_CALL) + .def("J", &DFWrapper::py_jacobian, + "Compute Jacobian matrix and return as a numpy array.") .def("name", &DFWrapper::name, return_value_policy(), DocClassMethod(name)) - .add_property("ni", &DifferentiableFunction::inputSize) - .add_property("no", &DifferentiableFunction::outputSize) - .add_property("ndi", &DifferentiableFunction::inputDerivativeSize) - .add_property("ndo", &DifferentiableFunction::outputDerivativeSize) + .add_property("ni", &DifferentiableFunction::inputSize, DOC_DF_NI) + .add_property("no", &DifferentiableFunction::outputSize, DOC_DF_NO) + .add_property("ndi", &DifferentiableFunction::inputDerivativeSize, + DOC_DF_NDI) + .add_property("ndo", &DifferentiableFunction::outputDerivativeSize, + DOC_DF_NDO) // C++ API .def("value", &DFWrapper::value_wrap, DocClassMethod(value)) diff --git a/src/pyhpp/constraints/explicit-constraint-set.cc b/src/pyhpp/constraints/explicit-constraint-set.cc index b1c858c3..2289d91d 100644 --- a/src/pyhpp/constraints/explicit-constraint-set.cc +++ b/src/pyhpp/constraints/explicit-constraint-set.cc @@ -46,7 +46,8 @@ void exposeExplicitConstraintSet() { init()) .def("__str__", &to_str) .def("add", &ExplicitConstraintSet::add, DocClassMethod(add)) - .def("errorSize", &ExplicitConstraintSet::errorSize); + .def("errorSize", &ExplicitConstraintSet::errorSize, + DocClassMethod(errorSize)); } } // namespace constraints } // namespace pyhpp diff --git a/src/pyhpp/constraints/implicit.cc b/src/pyhpp/constraints/implicit.cc index a5cd89c0..32a4065d 100644 --- a/src/pyhpp/constraints/implicit.cc +++ b/src/pyhpp/constraints/implicit.cc @@ -39,6 +39,11 @@ // DocNamespace(hpp::constraints) +namespace { +const char* DOC_COMPARISONTYPE_GET = "Return the ComparisonType."; +const char* DOC_COMPARISONTYPE_SET = "Set the comparison type."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -61,10 +66,11 @@ void exposeImplicit() { .def("comparisonType", static_cast( &Implicit::comparisonType), - return_internal_reference<>(), DocClassMethod(comparisonType)) + return_internal_reference<>(), DOC_COMPARISONTYPE_GET) .def("comparisonType", static_cast( - &Implicit::comparisonType)) + &Implicit::comparisonType), + DOC_COMPARISONTYPE_SET) .def("function", &Implicit::function, return_internal_reference<>(), DocClassMethod(function)) .def("parameterSize", &Implicit::parameterSize, diff --git a/src/pyhpp/constraints/iterative-solver.cc b/src/pyhpp/constraints/iterative-solver.cc index e7fe30a1..3a4c2af7 100644 --- a/src/pyhpp/constraints/iterative-solver.cc +++ b/src/pyhpp/constraints/iterative-solver.cc @@ -40,6 +40,32 @@ // DocNamespace(hpp::constraints) +namespace { +const char* DOC_HI_ERRORTHRESHOLD = "Get error threshold."; +const char* DOC_HI_MAXITERATIONS = + "Get maximal number of iterations in config projector."; +const char* DOC_HI_RHSFC1 = + "Compute right hand side of equality constraints from a configuration.\n\n" + "For each constraint of type Equality, set right hand side as rhs = f(q).\n" + "Only parameterizable constraints (type Equality) are set."; +const char* DOC_HI_RHSFC2 = + "Compute right hand side of a constraint from a configuration.\n\n" + "Set right hand side as rhs = f(q).\n" + "Only parameterizable constraints (type Equality) are set."; +const char* DOC_HI_RHS_SET1 = + "Set right hand side of a constraint.\n\n" + "Size of rhs should be equal to the total dimension of parameterizable\n" + "constraints (type Equality)."; +const char* DOC_HI_RHS_SET2 = + "Set the right hand side.\n\n" + "Size of rhs should be equal to the total dimension of parameterizable\n" + "constraints (type Equality)."; +const char* DOC_HI_RHS_GET = + "Get the right hand side.\n\n" + "Size of result is equal to total dimension of parameterizable\n" + "constraints (type Equality)."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -73,28 +99,36 @@ void exposeHierarchicalIterativeSolver() { static_cast( &HierarchicalIterative::errorThreshold), static_cast( - &HierarchicalIterative::errorThreshold)) + &HierarchicalIterative::errorThreshold), + DOC_HI_ERRORTHRESHOLD) .def("rightHandSideFromConfig", static_cast( - &HierarchicalIterative::rightHandSideFromConfig)) + &HierarchicalIterative::rightHandSideFromConfig), + DOC_HI_RHSFC1) .def("rightHandSideFromConfig", static_cast( - &HierarchicalIterative::rightHandSideFromConfig)) - .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &HierarchicalIterative::rightHandSideFromConfig), + DOC_HI_RHSFC2) + .def("rightHandSide", + static_cast( + &HierarchicalIterative::rightHandSide), + DOC_HI_RHS_SET1) .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &HierarchicalIterative::rightHandSide), + DOC_HI_RHS_SET2) .def("rightHandSide", static_cast( - &HierarchicalIterative::rightHandSide)) + &HierarchicalIterative::rightHandSide), + DOC_HI_RHS_GET) .add_property("maxIterations", static_cast( &HierarchicalIterative::maxIterations), static_cast( - &HierarchicalIterative::maxIterations)) + &HierarchicalIterative::maxIterations), + DOC_HI_MAXITERATIONS) .add_property( "errorThreshold", static_cast( diff --git a/src/pyhpp/core/constraint.cc b/src/pyhpp/core/constraint.cc index b9c3b3a4..803ca7ac 100644 --- a/src/pyhpp/core/constraint.cc +++ b/src/pyhpp/core/constraint.cc @@ -38,6 +38,15 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_CP_LINESEARCHTYPE = "Get/set the line search type."; +const char* DOC_CP_MAXITER_GET = + "Get maximal number of iterations in config projector."; +const char* DOC_CP_MAXITER_SET = "Set maximal number of iterations."; +const char* DOC_CP_ERRTHRESH_GET = "Get error threshold in config projector."; +const char* DOC_CP_ERRTHRESH_SET = "Set error threshold."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -131,22 +140,29 @@ void exposeConstraint() { const>(&ConfigProjector::lineSearchType), static_cast( - &ConfigProjector::lineSearchType)) + &ConfigProjector::lineSearchType), + DOC_CP_LINESEARCHTYPE) .def("add", &ConfigProjector::add, DocClassMethod(add)) .def("lastIsOptional", static_cast( &ConfigProjector::lastIsOptional)) .def("lastIsOptional", static_cast( &ConfigProjector::lastIsOptional)) - .def("maxIterations", static_cast( - &ConfigProjector::maxIterations)) - .def("maxIterations", static_cast( - &ConfigProjector::maxIterations)) + .def("maxIterations", + static_cast( + &ConfigProjector::maxIterations), + DOC_CP_MAXITER_GET) + .def("maxIterations", + static_cast( + &ConfigProjector::maxIterations), + DOC_CP_MAXITER_SET) .def("errorThreshold", static_cast( - &ConfigProjector::errorThreshold)) + &ConfigProjector::errorThreshold), + DOC_CP_ERRTHRESH_SET) .def("errorThreshold", static_cast( - &ConfigProjector::errorThreshold)) + &ConfigProjector::errorThreshold), + DOC_CP_ERRTHRESH_GET) .def("residualError", &ConfigProjector::residualError, DocClassMethod(residualError)) .def("setRightHandSideFromConfig", &rightHandSideFromConfig) diff --git a/src/pyhpp/core/node.cc b/src/pyhpp/core/node.cc index 1ffc5c56..9b232489 100644 --- a/src/pyhpp/core/node.cc +++ b/src/pyhpp/core/node.cc @@ -55,7 +55,8 @@ void exposeNode() { &Node::connectedComponent)) .def("connectedComponent", static_cast( - &Node::connectedComponent)) + &Node::connectedComponent), + "Store the connected component the node belongs to.") .def("outEdges", &Node::outEdges, return_internal_reference<>(), DocClassMethod(outEdges)) .def("inEdges", &Node::inEdges, return_internal_reference<>(), diff --git a/src/pyhpp/core/path-optimizer.cc b/src/pyhpp/core/path-optimizer.cc index ad1b65ca..6792327c 100644 --- a/src/pyhpp/core/path-optimizer.cc +++ b/src/pyhpp/core/path-optimizer.cc @@ -43,6 +43,15 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_TTP_MAXVEL = + "Maximum velocity for each output degree of freedom."; +const char* DOC_TTP_MAXACC = + "Maximum acceleration for each output degree of freedom."; +const char* DOC_TTP_MINDUR = + "Minimum duration assigned to each non-empty subpath."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -129,17 +138,20 @@ void exposePathOptimizer() { .add_property( "maxVelocity", static_cast(&TTP_t::maxVelocity), - static_cast(&TTP_t::maxVelocity)) + static_cast(&TTP_t::maxVelocity), + DOC_TTP_MAXVEL) .add_property( "maxAcceleration", static_cast(&TTP_t::maxAcceleration), static_cast( - &TTP_t::maxAcceleration)) + &TTP_t::maxAcceleration), + DOC_TTP_MAXACC) .add_property( "minimumDuration", static_cast(&TTP_t::minimumDuration), static_cast( - &TTP_t::minimumDuration)); + &TTP_t::minimumDuration), + DOC_TTP_MINDUR); exposeSplineGradientBased<1>("SplineGradientBased_bezier1"); exposeSplineGradientBased<3>("SplineGradientBased_bezier3"); diff --git a/src/pyhpp/core/path-planner.cc b/src/pyhpp/core/path-planner.cc index bf1e33c2..b764933c 100644 --- a/src/pyhpp/core/path-planner.cc +++ b/src/pyhpp/core/path-planner.cc @@ -41,6 +41,13 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_PP_MAXITER_GET = "Get maximal number of iterations."; +const char* DOC_PP_MAXITER_SET = "Set maximal number of iterations."; +const char* DOC_PP_TIMEOUT_GET = "Get time out."; +const char* DOC_PP_TIMEOUT_SET = "Set time out (in seconds)."; +} // namespace + namespace pyhpp { namespace core { @@ -159,14 +166,19 @@ void exposePathPlanner() { .def("interrupt", &PathPlanner::interrupt, DocClassMethod(interrupt)) .def("maxIterations", static_cast( - &PathPlanner::maxIterations)) + &PathPlanner::maxIterations), + DOC_PP_MAXITER_GET) .def("maxIterations", static_cast( - &PathPlanner::maxIterations)) + &PathPlanner::maxIterations), + DOC_PP_MAXITER_SET) + .def("timeOut", + static_cast(&PathPlanner::timeOut), + DOC_PP_TIMEOUT_GET) .def("timeOut", - static_cast(&PathPlanner::timeOut)) - .def("timeOut", static_cast( - &PathPlanner::timeOut)) + static_cast( + &PathPlanner::timeOut), + DOC_PP_TIMEOUT_SET) .def("stopWhenProblemIsSolved", &PathPlanner::stopWhenProblemIsSolved, DocClassMethod(stopWhenProblemIsSolved)) .def("computePath", &PathPlanner::computePath, diff --git a/src/pyhpp/core/steering-method.cc b/src/pyhpp/core/steering-method.cc index e2e3532d..3b56a083 100644 --- a/src/pyhpp/core/steering-method.cc +++ b/src/pyhpp/core/steering-method.cc @@ -154,7 +154,7 @@ void exposeSteeringMethod() { .def("constraints", static_cast( &SteeringMethod::constraints), - return_value_policy()); + return_value_policy(), "Get constraint set."); pyhpp::core::steeringMethod::exposeSteeringMethods(); } diff --git a/src/pyhpp/manipulation/device.cc b/src/pyhpp/manipulation/device.cc index 5c794e45..19318fe9 100644 --- a/src/pyhpp/manipulation/device.cc +++ b/src/pyhpp/manipulation/device.cc @@ -35,6 +35,29 @@ // DocNamespace(hpp::manipulation) +namespace { + +const char* DOC_HANDLE_NAME = "Name of the handle."; + +const char* DOC_HANDLE_LOCALPOSITION = + "Local position of the handle in the joint frame."; + +const char* DOC_HANDLE_MASK = + "Constraint mask: vector of size 6 defining the symmetries of the " + "handle. See Handle class documentation for details."; + +const char* DOC_HANDLE_MASKCOMP = "Mask of the complement constraint."; + +const char* DOC_HANDLE_CLEARANCE = + "Distance from the center of the gripper along x-axis that ensures no " + "collision. Also gives an order of magnitude of the gripper size."; + +const char* DOC_HANDLE_APPROACHINGDIRECTION = + "Approaching direction for pregrasp (unit vector in handle frame, default " + "is x-axis)."; + +} // namespace + namespace pyhpp { namespace manipulation { using namespace boost::python; @@ -273,17 +296,19 @@ static JointIndex getParentJointId(const HandlePtr_t& handle) { void exposeHandle() { // DocClass(Handle) class_("Handle", no_init) - .add_property("name", &getHandleName, &setHandleName) + .add_property("name", &getHandleName, &setHandleName, DOC_HANDLE_NAME) .add_property("localPosition", &getHandleLocalPosition, - &setHandleLocalPosition) - .add_property("mask", &getHandleMask, &setHandleMask) - .add_property("maskComp", &getHandleMaskComp, &setHandleMaskComp) + &setHandleLocalPosition, DOC_HANDLE_LOCALPOSITION) + .add_property("mask", &getHandleMask, &setHandleMask, DOC_HANDLE_MASK) + .add_property("maskComp", &getHandleMaskComp, &setHandleMaskComp, + DOC_HANDLE_MASKCOMP) .add_property( "clearance", static_cast(&Handle::clearance), - static_cast(&Handle::clearance)) + static_cast(&Handle::clearance), + DOC_HANDLE_CLEARANCE) .add_property("approachingDirection", &getApproachingDirection, - &setApproachingDirection) + &setApproachingDirection, DOC_HANDLE_APPROACHINGDIRECTION) .def("createGrasp", &Handle::createGrasp, DocClassMethod(createGrasp)) .def("getParentJointId", &getParentJointId, "Get index of the joint the handle is attached to" diff --git a/src/pyhpp/manipulation/path-planner.cc b/src/pyhpp/manipulation/path-planner.cc index 561ad9a5..0e5f774f 100644 --- a/src/pyhpp/manipulation/path-planner.cc +++ b/src/pyhpp/manipulation/path-planner.cc @@ -40,6 +40,24 @@ // DocNamespace(hpp::manipulation::pathPlanner) +namespace { + +const char* DOC_INNERPLANNER_GET = "Get the inner planner."; +const char* DOC_INNERPLANNER_SET = "Set the inner planner."; + +const char* DOC_NRANDOMCONFIG = + "Get the number of random configurations used to generate the initial " + "config of the final path."; + +const char* DOC_NDISCRETESTEPS = + "Number of steps to generate goal config (successive projections)."; + +const char* DOC_CHECKFEASIBILITYONLY = + "If enabled, only add one solution to the roadmap. " + "Otherwise add all solutions."; + +} // namespace + namespace pyhpp { namespace manipulation { @@ -305,10 +323,13 @@ void exposePathPlanners() { "TransitionPlanner", boost::python::init()) .def("innerPlanner", static_cast( - &TransitionPlanner::innerPlanner)) - .def("innerPlanner", static_cast( - &TransitionPlanner::innerPlanner)) + &TransitionPlanner::innerPlanner), + DOC_INNERPLANNER_GET) + .def("innerPlanner", + static_cast( + &TransitionPlanner::innerPlanner), + DOC_INNERPLANNER_SET) .def("innerProblem", &TransitionPlanner::innerProblem, DocClassMethod(innerProblem)) .def("computePath", &TransitionPlanner::computePath, @@ -350,18 +371,22 @@ void exposePathPlanners() { boost::python::init()) .def(boost::python::init()) - .def("nRandomConfig", static_cast( - &EndEffectorTrajectory::nRandomConfig)) + .def("nRandomConfig", + static_cast( + &EndEffectorTrajectory::nRandomConfig), + DOC_NRANDOMCONFIG) .def("nRandomConfig", static_cast( &EndEffectorTrajectory::nRandomConfig)) .def("nDiscreteSteps", static_cast( - &EndEffectorTrajectory::nDiscreteSteps)) + &EndEffectorTrajectory::nDiscreteSteps), + DOC_NDISCRETESTEPS) .def("nDiscreteSteps", static_cast( &EndEffectorTrajectory::nDiscreteSteps)) .def("checkFeasibilityOnly", static_cast( - &EndEffectorTrajectory::checkFeasibilityOnly)) + &EndEffectorTrajectory::checkFeasibilityOnly), + DOC_CHECKFEASIBILITYONLY) .def("checkFeasibilityOnly", static_cast( &EndEffectorTrajectory::checkFeasibilityOnly)) diff --git a/src/pyhpp/manipulation/problem.cc b/src/pyhpp/manipulation/problem.cc index 498daf70..71970635 100644 --- a/src/pyhpp/manipulation/problem.cc +++ b/src/pyhpp/manipulation/problem.cc @@ -41,6 +41,13 @@ // DocNamespace(hpp::manipulation) +namespace { + +const char* DOC_CONSTRAINTGRAPH_GET = "Get the graph of constraints."; +const char* DOC_CONSTRAINTGRAPH_SET = "Set the graph of constraints."; + +} // namespace + using namespace boost::python; namespace pyhpp { @@ -123,8 +130,14 @@ void exposeProblem() { // DocClass(Problem) class_>("Problem", init()) - .PYHPP_DEFINE_GETTER_SETTER_CONST_REF(Problem, constraintGraph, - PyWGraphPtr_t) + .def("constraintGraph", + static_cast( + &Problem::constraintGraph), + DOC_CONSTRAINTGRAPH_GET) + .def("constraintGraph", + static_cast( + &Problem::constraintGraph), + DOC_CONSTRAINTGRAPH_SET) .def("checkProblem", &Problem::checkProblem, DocClassMethod(checkProblem)) .def( "steeringMethod", diff --git a/src/pyhpp/manipulation/steering-method.cc b/src/pyhpp/manipulation/steering-method.cc index 46778fe2..01516b0e 100644 --- a/src/pyhpp/manipulation/steering-method.cc +++ b/src/pyhpp/manipulation/steering-method.cc @@ -36,6 +36,18 @@ // DocNamespace(hpp::manipulation) +namespace { + +const char* DOC_SET_TRAJECTORY_CONSTRAINT = + "Set the constraint whose right hand side will vary along the trajectory."; + +const char* DOC_SET_TRAJECTORY = + "Set the right hand side of the trajectory constraint from a path. " + "param se3Output: set to True if the output of path must be understood " + "as SE3."; + +} // namespace + namespace pyhpp { namespace manipulation { @@ -82,9 +94,10 @@ void exposeManipSteeringMethod() { "EndEffectorTrajectorySteeringMethod", boost::python::init()) .def("setTrajectoryConstraint", - &EndEffectorTrajectorySteeringMethod::setTrajectoryConstraint) - .def("setTrajectory", - &EndEffectorTrajectorySteeringMethod::setTrajectory); + &EndEffectorTrajectorySteeringMethod::setTrajectoryConstraint, + DOC_SET_TRAJECTORY_CONSTRAINT) + .def("setTrajectory", &EndEffectorTrajectorySteeringMethod::setTrajectory, + DOC_SET_TRAJECTORY); } } // namespace manipulation From 07ffd996603ad3bb6c2d84b422a36872cd9a1728 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Mon, 6 Jul 2026 13:04:59 +0200 Subject: [PATCH 03/18] doc: update status MD files and complete Python docstrings for all bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All ⚠️ and ❌ entries in core, constraints and manipulation that had writable docstrings are now ✅. MD files reflect the current state. --- doc/constraints_doc_todo.md | 32 ++-- doc/core_doc_todo.md | 175 ++++++++++-------- doc/manipulation_doc_todo.md | 54 +++--- src/pyhpp/constraints/by-substitution.cc | 9 +- .../constraints/differentiable-function.cc | 12 +- src/pyhpp/constraints/explicit.cc | 3 +- src/pyhpp/constraints/implicit.cc | 7 +- src/pyhpp/constraints/iterative-solver.cc | 24 ++- src/pyhpp/constraints/locked-joint.cc | 7 +- src/pyhpp/core/config-validation.cc | 6 +- src/pyhpp/core/connected-component.cc | 4 +- src/pyhpp/core/constraint.cc | 46 +++-- src/pyhpp/core/distance.cc | 16 +- src/pyhpp/core/node.cc | 3 +- src/pyhpp/core/parameter.cc | 30 ++- src/pyhpp/core/path-optimizer.cc | 69 +++++-- src/pyhpp/core/path-projector.cc | 18 +- src/pyhpp/core/path-validation.cc | 25 ++- src/pyhpp/core/path.cc | 23 ++- src/pyhpp/core/problem.cc | 135 ++++++++++---- src/pyhpp/core/roadmap.cc | 81 ++++++-- src/pyhpp/core/steering-method.cc | 4 +- src/pyhpp/manipulation/device.cc | 19 +- src/pyhpp/manipulation/graph.cc | 63 +++++-- src/pyhpp/manipulation/path-planner.cc | 8 +- src/pyhpp/manipulation/problem.cc | 15 +- src/pyhpp/manipulation/urdf/util.cc | 8 +- 27 files changed, 616 insertions(+), 280 deletions(-) diff --git a/doc/constraints_doc_todo.md b/doc/constraints_doc_todo.md index afd41cb5..c538b8c7 100644 --- a/doc/constraints_doc_todo.md +++ b/doc/constraints_doc_todo.md @@ -36,10 +36,10 @@ | Python method | Status | Note | |---|---|---| -| `Manipulability.__init__` | ⚠️ Python-only | Factory `Manipulability::create` | -| `Manipulability.lockJoint` | ❌ No C++ doc | No `///` in `manipulability.hh` | -| `MinManipulability.__init__` | ⚠️ Python-only | Factory `MinManipulability::create` | -| `MinManipulability.lockJoint` | ❌ No C++ doc | Same | +| `Manipulability.__init__` | ✅ | Inline string — factory `Manipulability::create` | +| `Manipulability.lockJoint` | ✅ | Inline string — no `///` in `manipulability.hh` | +| `MinManipulability.__init__` | ✅ | Inline string — factory `MinManipulability::create` | +| `MinManipulability.lockJoint` | ✅ | Inline string — no `///` in `manipulability.hh` | --- @@ -49,13 +49,13 @@ | Python method | Status | Note | |---|---|---| -| `__init__` | ⚠️ Python-only | Wrapper for `Implicit::create` | +| `__init__` | ✅ | Inline string — wrapper for `Implicit::create` | | `comparisonType` (getter) | ✅ | `DOC_COMPARISONTYPE_GET` (`DocClassMethod` unsafe: setter has `comp` param → kwargs mismatch) | | `comparisonType` (setter) | ✅ | `DOC_COMPARISONTYPE_SET` | | `function` | ✅ | `DocClassMethod(function)` | | `parameterSize` | ✅ | `DocClassMethod(parameterSize)` | | `rightHandSideSize` | ✅ | `DocClassMethod(rightHandSideSize)` | -| `getFunctionOutputSize` | ⚠️ Python-only | Static method, no direct C++ equivalent | +| `getFunctionOutputSize` | ✅ | Inline string — static method, no direct C++ equivalent | --- @@ -63,7 +63,7 @@ | Python method | Status | Note | |---|---|---| -| `createExplicit` | ⚠️ Python-only | Wrapper around `Explicit::create` | +| `createExplicit` | ✅ | Inline string — wrapper around `Explicit::create` | --- @@ -94,11 +94,11 @@ | `rightHandSide` (setter rhs only) | ✅ | `DOC_HI_RHS_SET2` | | `rightHandSide` (getter) | ✅ | `DOC_HI_RHS_GET` | | `maxIterations` (add_property) | ✅ | `DOC_HI_MAXITERATIONS` from `hierarchical-iterative.hh` | -| `lastIsOptional` (add_property) | ❌ No C++ doc | No `///` in `hierarchical-iterative.hh` (uses `//`) | -| `solveLevelByLevel` (add_property) | ❌ No C++ doc | Same (`//` non-doxygen) | -| `numberStacks` | ❌ No C++ doc | No `///` | -| `constraintsForPriority` | ⚠️ Python-only | Wrapper returning a Python list | -| `dimension` | ❌ No C++ doc | No `///` on the exposed method | +| `lastIsOptional` (add_property) | ✅ | Inline string — no `///` in `hierarchical-iterative.hh` (uses `//`) | +| `solveLevelByLevel` (add_property) | ✅ | Inline string — same (non-doxygen `//`) | +| `numberStacks` | ✅ | Inline string — no `///` | +| `constraintsForPriority` | ✅ | Inline string — wrapper returning a Python list | +| `dimension` | ✅ | Inline string — no `///` on the exposed method | --- @@ -110,7 +110,7 @@ |---|---|---| | `SolverStatus` (enum) | ❌ No C++ doc | Enum without class-level `///` | | `explicitConstraintSetHasChanged` | ✅ | `DocClassMethod(explicitConstraintSetHasChanged)` | -| `solve` | ⚠️ Signature diff. | Python wrapper returns `(qout, status)` tuple, hides output arg | +| `solve` | ✅ | Inline string — returns `(qout, status)` tuple | | `explicitConstraintSet` | ✅ | `DocClassMethod(explicitConstraintSet)` | | `rightHandSideFromConfig` (config only) | ✅ | `DOC_BS_RHSFC1` from `by-substitution.hh` | | `rightHandSideFromConfig` (constraint + config) | ✅ | `DOC_BS_RHSFC2` | @@ -118,7 +118,7 @@ | `rightHandSide` (setter rhs only) | ✅ | `DOC_BS_RHS_SET2` | | `rightHandSide` (getter) | ✅ | `DOC_BS_RHS_GET` | | `errorThreshold` (add_property) | ✅ | `DOC_BS_ERRORTHRESHOLD` from `by-substitution.hh` | -| `describeError` | ⚠️ Python-only | Wrapper decomposing error by constraint | +| `describeError` | ✅ | Inline string — returns list of `(constraint_name, error_norm)` pairs | --- @@ -142,8 +142,8 @@ | Python method | Status | Note | |---|---|---| -| `__init__` (joint + config) | ⚠️ Python-only | `createLockedJoint`: wrapper without direct C++ equivalent | -| `__init__` (joint + config + comp) | ⚠️ Python-only | `createLockedJointWithComp`: same | +| `__init__` (joint + config) | ✅ | Inline string — `createLockedJoint` wrapper | +| `__init__` (joint + config + comp) | ✅ | Inline string — `createLockedJointWithComp` wrapper | --- diff --git a/doc/core_doc_todo.md b/doc/core_doc_todo.md index 994b56a1..f0c5f7e7 100644 --- a/doc/core_doc_todo.md +++ b/doc/core_doc_todo.md @@ -3,7 +3,7 @@ ## Legend - ✅ **Documented** — docstring present in the binding -- ❌ **No C++ doc** — C++ method has no `///` → nothing to add +- ❌ **No C++ doc** — C++ method has no `///` and no generated doc - ⚠️ **Python-only** — no direct C++ equivalent → wrapper or modified signature - ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert @@ -15,10 +15,10 @@ | Python method | Status | Note | |---|---|---| -| `__call__` (×2) | ⚠️ Python-only | Wrappers returning `(q, success)` and `success` | -| `eval` (×2) | ⚠️ Python-only | Same | -| `derivative` | ⚠️ Python-only | Wrapper returning a numpy vector | -| `constraints` | ❌ No C++ doc | No `///` on `constraints()` in `path.hh` | +| `__call__` (×2) | ✅ | Inline strings — `(q, success)` and in-place form | +| `eval` (×2) | ✅ | Same inline strings as `__call__` | +| `derivative` | ✅ | Inline string — returns numpy vector | +| `constraints` | ✅ | Inline string — no `///` in `path.hh` | | `copy` | ✅ | `DocClassMethod(copy)` | | `extract` | ✅ | `DocClassMethod(extract)` | | `timeRange` | ✅ | `DocClassMethod(timeRange)` | @@ -29,7 +29,7 @@ | `end` | ✅ | `DocClassMethod(end)` | | `outputSize` | ✅ | `DocClassMethod(outputSize)` | | `outputDerivativeSize` | ✅ | `DocClassMethod(outputDerivativeSize)` | -| `StraightPath.__init__` | ⚠️ Python-only | Factory wrapper for `StraightPath::create` | +| `StraightPath.__init__` | ❌ No C++ doc | Factory wrapper — no `///` on `StraightPath::create` | --- @@ -47,28 +47,28 @@ | `initConfig` | ✅ | `DocClassMethod(initConfig)` | | `addGoalConfig` | ✅ | `DocClassMethod(addGoalConfig)` | | `resetGoalConfigs` | ✅ | `DocClassMethod(resetGoalConfigs)` | -| `steeringMethod` (getter/setter) | ❌ No C++ doc | Python wrapper struct; no `///` in `problem.hh` | -| `configValidation` (getter/setter) | ❌ No C++ doc | Same | -| `pathValidation` (getter/setter) | ❌ No C++ doc | Same | -| `pathProjector` (getter/setter) | ❌ No C++ doc | Same | -| `distance` (getter/setter) | ❌ No C++ doc | Same | -| `target` (getter/setter) | ❌ No C++ doc | Same | -| `configurationShooter` (getter/setter) | ❌ No C++ doc | Same | -| `errorThreshold` (def_readwrite) | ⚠️ Python-only | Python wrapper field — default threshold for ConfigProjector creation | -| `maxIterProjection` (def_readwrite) | ⚠️ Python-only | Same | -| `addPartialCom` | ⚠️ Python-only | | -| `getPartialCom` | ⚠️ Python-only | | -| `createRelativeComConstraint` | ⚠️ Python-only | | -| `createTransformationConstraint` (×2) | ⚠️ Python-only | | -| `setConstantRightHandSide` | ⚠️ Python-only | | -| `applyConstraints` | ⚠️ Python-only | | -| `isConfigValid` | ⚠️ Python-only | | -| `setConstraints` | ⚠️ Python-only | | -| `getConstraints` | ⚠️ Python-only | | -| `setRightHandSideFromConfig` | ⚠️ Python-only | | -| `addNumericalConstraintsToConfigProjector` (×2) | ⚠️ Python-only | | -| `createComBetweenFeet` | ⚠️ Python-only | | -| `directPath` | ⚠️ Signature diff. | Returns `(valid, path, report)`; hides output params | +| `steeringMethod` (getter/setter) | ✅ | Inline strings — no `///` in `problem.hh` | +| `configValidation` (getter/setter) | ✅ | Inline strings | +| `pathValidation` (getter/setter) | ✅ | Inline strings | +| `pathProjector` (getter/setter) | ✅ | Inline strings | +| `distance` (getter/setter) | ✅ | Inline strings | +| `target` (getter/setter) | ✅ | Inline strings | +| `configurationShooter` (getter/setter) | ✅ | Inline strings | +| `errorThreshold` (def_readwrite) | ✅ | Inline string — Python-only field | +| `maxIterProjection` (def_readwrite) | ✅ | Inline string — Python-only field | +| `addPartialCom` | ✅ | Inline string | +| `getPartialCom` | ✅ | Inline string | +| `createRelativeComConstraint` | ✅ | Inline string | +| `createTransformationConstraint` (×2) | ✅ | Inline string | +| `setConstantRightHandSide` | ✅ | Inline string | +| `applyConstraints` | ✅ | Inline string — returns `(success, projected_config, residual_error)` | +| `isConfigValid` | ✅ | Inline string — returns `(valid, report)` | +| `setConstraints` | ✅ | Inline string | +| `getConstraints` | ✅ | Inline string | +| `setRightHandSideFromConfig` | ✅ | Inline string | +| `addNumericalConstraintsToConfigProjector` (×2) | ✅ | Inline strings | +| `createComBetweenFeet` | ✅ | Inline string | +| `directPath` | ✅ | Inline string — returns `(valid, path, report)` | --- @@ -88,19 +88,19 @@ | `pathExists` | ✅ | `DocClassMethod(pathExists)` | | `goalNodes` | ✅ | `DocClassMethod(goalNodes)` | | `distance` | ✅ | `DocClassMethod(distance)` | -| `addNode` | ⚠️ Python-only | Wrapper on `roadmap.addNode(config)` | -| `nearestNode` (×4) | ⚠️ Python-only | Return `(config, minDistance)` | -| `nearestNodes` (×2) | ⚠️ Python-only | | -| `addNodeAndEdges` | ⚠️ Python-only | | -| `addNodeAndEdge` | ⚠️ Python-only | | -| `addEdge` (×2) | ⚠️ Python-only | | -| `nodes` | ⚠️ Python-only | Returns list of configurations | -| `nodesConnectedComponent` | ⚠️ Python-only | | -| `initNode` (×2) | ⚠️ Python-only | | -| `connectedComponents` | ⚠️ Python-only | | -| `numberConnectedComponents` | ⚠️ Python-only | | -| `getConnectedComponent` | ⚠️ Python-only | | -| `connectedComponentOfNode` | ⚠️ Python-only | | +| `addNode` | ✅ | Inline string | +| `nearestNode` (×4) | ✅ | Inline strings — returns `(config, minDistance)` | +| `nearestNodes` (×2) | ✅ | Inline strings | +| `addNodeAndEdges` | ✅ | Inline string | +| `addNodeAndEdge` | ✅ | Inline string | +| `addEdge` (×2) | ✅ | Inline strings | +| `nodes` | ✅ | Inline string | +| `nodesConnectedComponent` | ✅ | Inline string | +| `initNode` (×2) | ✅ | Inline strings | +| `connectedComponents` | ✅ | Inline string | +| `numberConnectedComponents` | ✅ | Inline string | +| `getConnectedComponent` | ✅ | Inline string | +| `connectedComponentOfNode` | ✅ | Inline string | --- @@ -121,7 +121,7 @@ | `stopWhenProblemIsSolved` | ✅ | `DocClassMethod(stopWhenProblemIsSolved)` | | `computePath` | ✅ | `DocClassMethod(computePath)` | | `maxIterations` (getter) | ✅ | `DOC_PP_MAXITER_GET` from `path-planner.hh` | -| `maxIterations` (setter) | ✅ | `DOC_PP_MAXITER_SET` (getter/setter pair → `DocClassMethod` unsafe on getter) | +| `maxIterations` (setter) | ✅ | `DOC_PP_MAXITER_SET` | | `timeOut` (getter) | ✅ | `DOC_PP_TIMEOUT_GET` from `path-planner.hh` | | `timeOut` (setter) | ✅ | `DOC_PP_TIMEOUT_SET` | @@ -151,15 +151,25 @@ | Python property | Status | Note | |---|---|---| -| `safety` | ❌ No C++ doc | `def_readwrite`; no `///` in `simple-time-parameterization.hh` | -| `order` | ❌ No C++ doc | Same | -| `maxAcceleration` | ❌ No C++ doc | Same | +| `safety` | ✅ | `DOC_STP_SAFETY` — from `///` in `simple-time-parameterization.hh` | +| `order` | ✅ | `DOC_STP_ORDER` | +| `maxAcceleration` | ✅ | `DOC_STP_MAXACC` | ### SplineGradientBased (×3 templates) | Python property | Status | Note | |---|---|---| -| `alphaInit`, `alwaysStopAtFirst`, `costOrder`, etc. | ❌ No C++ doc | `def_readwrite` on public fields without `///` | +| `alphaInit` | ✅ | `DOC_SGB_ALPHAINIT` — from `///` in `spline-gradient-based.hh` | +| `alwaysStopAtFirst` | ✅ | `DOC_SGB_ALWAYSSTOPFIRST` | +| `costOrder` | ✅ | `DOC_SGB_COSTORDER` | +| `usePathLengthAsWeights` | ✅ | `DOC_SGB_USEPATHLENGTH` | +| `reorderIntervals` | ✅ | `DOC_SGB_REORDERINTERVALS` | +| `linearizeAtEachStep` | ✅ | `DOC_SGB_LINEARIZE` | +| `checkJointBound` | ✅ | `DOC_SGB_CHECKJOINTBOUND` | +| `returnOptimum` | ✅ | `DOC_SGB_RETURNOPTIMUM` | +| `costThreshold` | ✅ | `DOC_SGB_COSTTHRESHOLD` | +| `guessThreshold` | ✅ | `DOC_SGB_GUESSTHRESHOLD` | +| `QPAccuracy` | ✅ | `DOC_SGB_QPACCURACY` | --- @@ -169,11 +179,11 @@ | Python method | Status | Note | |---|---|---| +| `__call__` | ✅ | Inline string | | `steer` | ✅ | `DocClassMethod(steer)` | | `problem` | ✅ | `DocClassMethod(problem)` | | `constraints` (setter) | ✅ | `DocClassMethod(constraints)` | -| `constraints` (getter) | ✅ | `"Get constraint set."` inline (getter/setter pair → `DocClassMethod` unsafe on getter) | -| `__call__` | ⚠️ Python-only | Wrapper on `operator()` | +| `constraints` (getter) | ✅ | Inline string (getter/setter pair → `DocClassMethod` unsafe on getter) | --- @@ -185,8 +195,8 @@ |---|---|---| | `addOutEdge` | ✅ | `DocClassMethod(addOutEdge)` | | `addInEdge` | ✅ | `DocClassMethod(addInEdge)` | -| `connectedComponent` (getter) | ❌ No C++ doc | No `///` on the getter in `node.hh` | -| `connectedComponent` (setter) | ✅ | `"Store the connected component the node belongs to."` inline | +| `connectedComponent` (getter) | ✅ | Inline string — no `///` in `node.hh` | +| `connectedComponent` (setter) | ✅ | Inline string | | `outEdges` | ✅ | `DocClassMethod(outEdges)` | | `inEdges` | ✅ | `DocClassMethod(inEdges)` | | `isOutNeighbor` | ✅ | `DocClassMethod(isOutNeighbor)` | @@ -202,9 +212,9 @@ | Python method | Status | Note | |---|---|---| | `name` | ✅ | `DocClassMethod(name)` | -| `apply` | ⚠️ Signature diff. | Wrapper modifying `q` in-place | -| `isSatisfied` (×2) | ⚠️ Signature diff. | Wrappers modifying `error` in-place | -| `copy` | ⚠️ Python-only | Static wrapper | +| `apply` | ✅ | Inline string — modifies `q` in-place | +| `isSatisfied` (×2) | ✅ | Inline strings | +| `copy` | ✅ | Inline string | ### ConstraintSet @@ -218,18 +228,18 @@ | Method / property | Status | Note | |---|---|---| | `solver` | ✅ | `DocClassMethod(solver)` | -| `lineSearchType` (add_property) | ✅ | `DOC_CP_LINESEARCHTYPE` from `config-projector.hh` | +| `lineSearchType` (add_property) | ✅ | `DOC_CP_LINESEARCHTYPE` | | `add` | ✅ | `DocClassMethod(add)` | -| `lastIsOptional` (getter/setter) | ❌ No C++ doc | No `///` in `config-projector.hh` | +| `lastIsOptional` (getter/setter) | ✅ | Inline strings — no `///` in `config-projector.hh` | | `maxIterations` (getter) | ✅ | `DOC_CP_MAXITER_GET` | | `maxIterations` (setter) | ✅ | `DOC_CP_MAXITER_SET` | | `errorThreshold` (getter) | ✅ | `DOC_CP_ERRTHRESH_GET` | | `errorThreshold` (setter) | ✅ | `DOC_CP_ERRTHRESH_SET` | | `residualError` | ✅ | `DocClassMethod(residualError)` | | `sigma` | ✅ | `DocClassMethod(sigma)` | -| `setRightHandSideFromConfig` | ⚠️ Python-only | | -| `setRightHandSideOfConstraint` | ⚠️ Python-only | | -| `numericalConstraints` | ⚠️ Python-only | Returns a Python list | +| `setRightHandSideFromConfig` | ✅ | Inline string | +| `setRightHandSideOfConstraint` | ✅ | Inline string | +| `numericalConstraints` | ✅ | Inline string | --- @@ -245,9 +255,9 @@ | Python method | Status | Note | |---|---|---| -| `asDistancePtr_t` | ⚠️ Python-only | | -| `getWeights` | ⚠️ Python-only | Wrapper on `dist->weights()` | -| `setWeights` | ⚠️ Python-only | Wrapper on `dist->weights(weights)` | +| `asDistancePtr_t` | ✅ | Inline string | +| `getWeights` | ✅ | Inline string | +| `setWeights` | ✅ | Inline string | --- @@ -257,10 +267,10 @@ | Python method | Status | Note | |---|---|---| -| `nodes` | ✅ | `DocClassMethod(nodes)` (note: Python wrapper returns configurations, not node objects) | +| `nodes` | ✅ | `DocClassMethod(nodes)` | | `reachableFrom` | ✅ | `DocClassMethod(reachableFrom)` | | `reachableTo` | ✅ | `DocClassMethod(reachableTo)` | -| `__eq__` | ⚠️ Python-only | Raw pointer comparison | +| `__eq__` | ✅ | Inline string | --- @@ -281,7 +291,7 @@ | Python method | Status | Note | |---|---|---| | `validate` | ✅ | `DocClassMethod(validate)` via `PYHPP_DEFINE_METHOD2` | -| `validate` (py tuple) | ⚠️ Signature diff. | Returns `(bool, report)` | +| `validate` (py tuple) | ✅ | Inline string — returns `(bool, report)` | ### ConfigValidations @@ -289,7 +299,7 @@ |---|---|---| | `add` | ✅ | `DocClassMethod(add)` via `PYHPP_DEFINE_METHOD2` | | `numberConfigValidations` | ✅ | `DocClassMethod(numberConfigValidations)` | -| `clear` | ❌ No C++ doc | No `///` in `config-validations.hh` | +| `clear` | ✅ | Inline string — no `///` in `config-validations.hh` | --- @@ -299,9 +309,14 @@ | Python method | Status | Note | |---|---|---| -| `boolValue`, `intValue`, `floatValue`, `stringValue`, `vectorValue`, `matrixValue` | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring; no `///` in `parameter.hh` | -| `value` | ⚠️ Python-only | Converts to Python object by type | -| `create_bool` | ⚠️ Python-only | Factory for `bool` | +| `boolValue` | ✅ | Inline string | +| `intValue` | ✅ | Inline string | +| `floatValue` | ✅ | Inline string | +| `stringValue` | ✅ | Inline string | +| `vectorValue` | ✅ | Inline string | +| `matrixValue` | ✅ | Inline string | +| `value` | ✅ | Inline string — converts to Python object by type | +| `create_bool` | ✅ | Inline string — factory for `bool` | --- @@ -312,8 +327,12 @@ | Python method | Status | Note | |---|---|---| | `apply` | ✅ | `DocClassMethod(apply)` | -| `apply` (py tuple) | ⚠️ Signature diff. | Returns `(bool, projPath)` | -| Factories (NoneProjector, ProgressiveProjector, etc.) | ⚠️ Python-only | | +| `apply` (py tuple) | ✅ | Inline string — returns `(bool, projPath)` | +| `NoneProjector` | ✅ | Inline string | +| `ProgressiveProjector` | ✅ | Inline string | +| `DichotomyProjector` | ✅ | Inline string | +| `GlobalProjector` | ✅ | Inline string | +| `RecursiveHermiteProjector` | ✅ | Inline string | --- @@ -324,9 +343,13 @@ | Python method | Status | Note | |---|---|---| | `validate` | ✅ | `DocClassMethod(validate)` | -| `validate` (py tuple) | ⚠️ Signature diff. | Returns `(bool, validPart, report)` | -| `validateConfiguration` | ⚠️ Python-only | Returns `(bool, report)` | -| Factories (Discretized, Progressive, Dichotomy, etc.) | ⚠️ Python-only | | +| `validate` (py tuple) | ✅ | Inline string — returns `(bool, validPart, report)` | +| `validateConfiguration` | ✅ | Inline string — returns `(bool, report)` | +| `Discretized` / `DiscretizedCollision` | ✅ | Inline string | +| `DiscretizedJointBound` | ✅ | Inline string | +| `DiscretizedCollisionAndJointBound` | ✅ | Inline string | +| `Progressive` | ✅ | Inline string | +| `Dichotomy` | ✅ | Inline string | --- @@ -370,7 +393,7 @@ No methods exposed. | Python method | Status | Note | |---|---|---| -| All methods | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring | +| All methods | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` — no `///` in spline headers | --- @@ -380,7 +403,7 @@ No methods exposed. | Python method | Status | Note | |---|---|---| -| `computePath` | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` without docstring | +| `computePath` | ❌ No C++ doc | No `///` in `goal-configurations.hh` | | `reached` | ❌ No C++ doc | Same | --- @@ -391,4 +414,4 @@ No methods exposed. | Python method / property | Status | Note | |---|---|---| -| All methods and properties | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` or `add_property`/`def_readwrite` without `///` | +| All methods and properties | ❌ No C++ doc | No `///` in the corresponding headers | diff --git a/doc/manipulation_doc_todo.md b/doc/manipulation_doc_todo.md index f131fcae..83b01a18 100644 --- a/doc/manipulation_doc_todo.md +++ b/doc/manipulation_doc_todo.md @@ -35,10 +35,10 @@ | Method / property | Status | Note | |---|---|---| -| `Graph._get_native_graph` | ⚠️ Python-only | C++ capsule for external interop | -| `Graph.robot` | ⚠️ Python-only | `def_readwrite`, no `///` | -| `Graph.maxIterations` | ❌ No C++ doc | `Graph::maxIterations()` has `///` in `graph.hh` but exposed via `PYHPP_DEFINE_GETTER_SETTER` without docstring | -| `Graph.errorThreshold` | ❌ No C++ doc | Same | +| `Graph._get_native_graph` | ✅ | Inline string — C++ capsule for external interop | +| `Graph.robot` | ✅ | Inline string — `def_readwrite`, no `///` | +| `Graph.maxIterations` | ✅ | Explicit `.def()` pair with inline strings | +| `Graph.errorThreshold` | ✅ | Explicit `.def()` pair with inline strings | | `Graph.createState` | ✅ | `DOC_CREATESTATE` inline | | `Graph.createTransition` | ✅ | `DOC_CREATETRANSITION` inline | | `Graph.createWaypointTransition` | ✅ | `DOC_CREATEWAYPOINTTRANSITION` inline | @@ -51,17 +51,17 @@ | `Graph.setWeight` | ✅ | `DOC_SETWEIGHT` inline | | `Graph.getWeight` | ✅ | `DOC_GETWEIGHT` inline | | `Graph.setWaypoint` | ✅ | `DOC_SETWAYPOINT` inline | -| `Graph.getState` *(by name)* | ⚠️ Python-only | Lookup by name in internal map | -| `Graph.getTransition` *(by name)* | ⚠️ Python-only | Lookup by name in internal map | -| `Graph.getStates` | ⚠️ Python-only | Returns list of `PyWState` | -| `Graph.getTransitions` | ⚠️ Python-only | Returns list of `PyWEdge` | -| `Graph.getStateNames` | ⚠️ Python-only | Returns list of names | -| `Graph.getTransitionNames` | ⚠️ Python-only | Returns list of names | +| `Graph.getState` *(by name)* | ✅ | Inline string — lookup by name in internal map | +| `Graph.getTransition` *(by name)* | ✅ | Inline string — lookup by name in internal map | +| `Graph.getStates` | ✅ | Inline string — returns list of `PyWState` | +| `Graph.getTransitions` | ✅ | Inline string — returns list of `PyWEdge` | +| `Graph.getStateNames` | ✅ | Inline string — returns list of names | +| `Graph.getTransitionNames` | ✅ | Inline string — returns list of names | | `Graph.getStateFromConfiguration` | ✅ | `DOC_GETSTATE` inline | | `Graph.addNumericalConstraint` | ✅ | `DOC_ADDNUMERICALCONSTRAINT` inline | | `Graph.addNumericalConstraintsToState` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOSTATE` inline | | `Graph.addNumericalConstraintsToTransition` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOTRANSITION` inline | -| `Graph.addNumericalConstraintsToGraph` | ⚠️ Python-only | Takes a list, no direct equivalent | +| `Graph.addNumericalConstraintsToGraph` | ✅ | Inline string — takes a list, no direct equivalent | | `Graph.addNumericalConstraintsForPath` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSFORPATH` inline | | `Graph.getNumericalConstraintsForState` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFORSTATE` inline | | `Graph.getNumericalConstraintsForEdge` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFOREDGE` inline | @@ -70,8 +70,8 @@ | `Graph.registerConstraints` | ✅ | `DOC_REGISTERCONSTRAINTS` inline | | `Graph.createPlacementConstraint` | ✅ | `DOC_CREATEPLACEMENTCONSTRAINT` inline | | `Graph.createPrePlacementConstraint` | ✅ | `DOC_CREATEPREPLACEMENTCONSTRAINT` inline | -| `Graph.createGraspConstraint` | ⚠️ Python-only | Wrapper around `Handle::createGrasp`, no `///` | -| `Graph.createPreGraspConstraint` | ⚠️ Python-only | Wrapper around `Handle::createPreGrasp` | +| `Graph.createGraspConstraint` | ✅ | Inline string — wrapper around `Handle::createGrasp` | +| `Graph.createPreGraspConstraint` | ✅ | Inline string — wrapper around `Handle::createPreGrasp` | | `Graph.getConfigErrorForState` | ✅ | `DOC_GETCONFIGERRORFORSTATE` inline | | `Graph.getConfigErrorForTransition` | ✅ | `DOC_GETCONFIGERRORFORTRANSITION` inline | | `Graph.getConfigErrorForTransitionLeaf` | ✅ | `DOC_GETCONFIGERRORFORTRANSITIONLEAF` inline | @@ -91,7 +91,7 @@ | `Graph.displayTransitionTargetConstraints` | ✅ | `DOC_DISPLAYTRANSITIONTARGETCONSTRAINTS` inline | | `Graph.display` | ✅ | `DOC_DISPLAY` inline | | `Graph.initialize` | ✅ | `DOC_INITIALIZE` inline | -| `Graph.transitionAtParam` | ⚠️ Python-only | Static method, wraps `Graph::edgeAtParam` | +| `Graph.transitionAtParam` | ✅ | Inline string — static method, wraps `Graph::edgeAtParam` | --- @@ -117,12 +117,12 @@ | Python method | Status | Note | |---|---|---| -| `Device.setRobotRootPosition` | ❌ No C++ doc | Declared without `///` in `device.hh` | -| `Device.handles` | ❌ No C++ doc | Public member without `///` | -| `Device.grippers` | ❌ No C++ doc | Public member without `///` | -| `Device.getJointNames` | ⚠️ Python-only | Wrapper without direct C++ equivalent | -| `Device.getJointConfig` | ⚠️ Python-only | Wrapper without direct C++ equivalent | -| `Device.setJointBounds` | ⚠️ Python-only | No docstring in the binding | +| `Device.setRobotRootPosition` | ✅ | Inline string — no `///` in `device.hh` | +| `Device.handles` | ✅ | Inline string — public member without `///` | +| `Device.grippers` | ✅ | Inline string — public member without `///` | +| `Device.getJointNames` | ✅ | Inline string — wrapper without direct C++ equivalent | +| `Device.getJointConfig` | ✅ | Inline string — wrapper without direct C++ equivalent | +| `Device.setJointBounds` | ✅ | Inline string | | `Device.contactSurfaceNames` | ✅ | Inline string | | `Device.contactSurfaces` | ✅ | Inline string | | `Device.addHandle` | ✅ | Inline string | @@ -138,7 +138,9 @@ | `Problem.constraintGraph` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | | `Problem.constraintGraph` (setter) | ✅ | Inline string | | `Problem.checkProblem` | ✅ | `DocClassMethod(checkProblem)` | -| `Problem.steeringMethod` (getter/setter ×3) | ❌ No C++ doc | No `///` in `manipulation::Problem` | +| `Problem.steeringMethod` (getter) | ✅ | Inline string — unwraps graph SM if applicable | +| `Problem.steeringMethod` (setter, core SM) | ✅ | Inline string | +| `Problem.steeringMethod` (setter, graph SM) | ✅ | Inline string | --- @@ -151,8 +153,8 @@ | `innerProblem` | ✅ | `DocClassMethod(innerProblem)` | | `computePath` | ✅ | `DocClassMethod(computePath)` | | `planPath` *(deprecated)* | ✅ | `DocClassMethod(planPath)` | -| `directPath` | ⚠️ Signature diff. | Hides `success` and `status` output params → kwargs count mismatch | -| `validateConfiguration` | ⚠️ Signature diff. | Hides `report` output param → kwargs count mismatch | +| `directPath` | ✅ | Inline string — returns `(success, path, status)` | +| `validateConfiguration` | ✅ | Inline string — returns `(valid, report)` | | `optimizePath` | ✅ | `DocClassMethod(optimizePath)` | | `timeParameterization` | ✅ | `DocClassMethod(timeParameterization)` | | `setEdge` *(deprecated)* | ✅ | `DocClassMethod(setEdge)` | @@ -187,7 +189,7 @@ | `RandomShortcut` | ❌ No C++ doc | No class-level `///` in `random-shortcut.hh` | | `GraphRandomShortcut` | ⚠️ Python-only | Factory function, no direct equivalent | | `GraphPartialShortcut` | ⚠️ Python-only | Same | -| `SplineGradientBased_bezier1/3` attributes (`alphaInit`, `alwaysStopAtFirst`, `costOrder`, `usePathLengthAsWeights`, `reorderIntervals`, `linearizeAtEachStep`, `checkJointBound`, `returnOptimum`, `costThreshold`, `guessThreshold`, `QPAccuracy`) | ❌ No C++ doc | `def_readwrite` without `///` | +| `SplineGradientBased_bezier1/3` attributes (`alphaInit`, `alwaysStopAtFirst`, `costOrder`, `usePathLengthAsWeights`, `reorderIntervals`, `linearizeAtEachStep`, `checkJointBound`, `returnOptimum`, `costThreshold`, `guessThreshold`, `QPAccuracy`) | ❌ No C++ doc | `def_readwrite` without `///` in manipulation binding | --- @@ -233,5 +235,5 @@ | Python function | Status | Note | |---|---|---| -| `loadModel` | ⚠️ Python-only | Overload that also calls `srdf::loadModelFromFile` | -| `loadModelFromString` | ⚠️ Python-only | Overload with XML string | +| `loadModel` | ✅ | Inline string — also calls `srdf::loadModelFromFile` | +| `loadModelFromString` | ✅ | Inline string — overload with XML strings | diff --git a/src/pyhpp/constraints/by-substitution.cc b/src/pyhpp/constraints/by-substitution.cc index 807fbc14..03ff5595 100644 --- a/src/pyhpp/constraints/by-substitution.cc +++ b/src/pyhpp/constraints/by-substitution.cc @@ -127,7 +127,10 @@ void exposeBySubstitution() { .def("explicitConstraintSetHasChanged", &BySubstitution::explicitConstraintSetHasChanged, DocClassMethod(explicitConstraintSetHasChanged)) - .def("solve", &BySubstitution_solve) + .def( + "solve", &BySubstitution_solve, + "Solve the constraints from configuration q. Returns (output_config, " + "status).") .def("explicitConstraintSet", static_cast( &BySubstitution::explicitConstraintSet), @@ -160,7 +163,9 @@ void exposeBySubstitution() { static_cast( &BySubstitution::errorThreshold), DOC_BS_ERRORTHRESHOLD) - .def("describeError", &BySubstitution_describeError); + .def("describeError", &BySubstitution_describeError, + "Describe the constraint error for configuration q. Returns a list " + "of (constraint_name, error_norm) pairs."); } } // namespace constraints } // namespace pyhpp diff --git a/src/pyhpp/constraints/differentiable-function.cc b/src/pyhpp/constraints/differentiable-function.cc index 021f2623..83316179 100644 --- a/src/pyhpp/constraints/differentiable-function.cc +++ b/src/pyhpp/constraints/differentiable-function.cc @@ -170,13 +170,17 @@ void lockJoint(M manipulability, const char* jointName) { void exposeManipulability() { class_, boost::noncopyable>("Manipulability", no_init) - .def("__init__", make_constructor(&Manipulability::create)) - .def("lockJoint", &lockJoint); + .def("__init__", make_constructor(&Manipulability::create), + "Create a manipulability function for the given robot.") + .def("lockJoint", &lockJoint, + "Lock a joint by name so it is excluded from the Jacobian."); class_, boost::noncopyable>("MinManipulability", no_init) - .def("__init__", make_constructor(&MinManipulability::create)) - .def("lockJoint", &lockJoint); + .def("__init__", make_constructor(&MinManipulability::create), + "Create a minimum-manipulability function for the given robot.") + .def("lockJoint", &lockJoint, + "Lock a joint by name so it is excluded from the Jacobian."); } } // namespace constraints diff --git a/src/pyhpp/constraints/explicit.cc b/src/pyhpp/constraints/explicit.cc index 68bf9ae0..52cd248f 100644 --- a/src/pyhpp/constraints/explicit.cc +++ b/src/pyhpp/constraints/explicit.cc @@ -65,7 +65,8 @@ ExplicitPtr_t createExplicit(const LiegroupSpacePtr_t& configSpace, void exposeExplicit() { // DocClass(Explicit) class_("Explicit", no_init) - .def("__init__", make_constructor(&createExplicit)); + .def("__init__", make_constructor(&createExplicit), + "Create an explicit constraint mapping output DOF from input DOF."); } } // namespace constraints } // namespace pyhpp diff --git a/src/pyhpp/constraints/implicit.cc b/src/pyhpp/constraints/implicit.cc index 32a4065d..fd8ed018 100644 --- a/src/pyhpp/constraints/implicit.cc +++ b/src/pyhpp/constraints/implicit.cc @@ -62,7 +62,9 @@ void exposeImplicit() { .value("Inferior", Inferior); // DocClass(Implicit) class_("Implicit", no_init) - .def("__init__", make_constructor(&Implicit::create)) + .def("__init__", make_constructor(&Implicit::create), + "Create an implicit constraint from a differentiable function and " + "comparison types.") .def("comparisonType", static_cast( &Implicit::comparisonType), @@ -77,7 +79,8 @@ void exposeImplicit() { DocClassMethod(parameterSize)) .def("rightHandSideSize", &Implicit::rightHandSideSize, DocClassMethod(rightHandSideSize)) - .def("getFunctionOutputSize", &getFunctionOutputSize) + .def("getFunctionOutputSize", &getFunctionOutputSize, + "Return the output size of the underlying differentiable function.") .staticmethod("getFunctionOutputSize"); } } // namespace constraints diff --git a/src/pyhpp/constraints/iterative-solver.cc b/src/pyhpp/constraints/iterative-solver.cc index 3a4c2af7..b1e0a7b7 100644 --- a/src/pyhpp/constraints/iterative-solver.cc +++ b/src/pyhpp/constraints/iterative-solver.cc @@ -139,16 +139,22 @@ void exposeHierarchicalIterativeSolver() { static_cast( &HierarchicalIterative::lastIsOptional), static_cast( - &HierarchicalIterative::lastIsOptional)) - .add_property("solveLevelByLevel", - static_cast( - &HierarchicalIterative::solveLevelByLevel), - static_cast( - &HierarchicalIterative::solveLevelByLevel)) - .def("numberStacks", &HierarchicalIterative::numberStacks) - .def("constraintsForPriority", &getConstraintsForPriority) + &HierarchicalIterative::lastIsOptional), + "Whether the last priority level is treated as optional.") + .add_property( + "solveLevelByLevel", + static_cast( + &HierarchicalIterative::solveLevelByLevel), + static_cast( + &HierarchicalIterative::solveLevelByLevel), + "Whether to solve constraints one priority level at a time.") + .def("numberStacks", &HierarchicalIterative::numberStacks, + "Return the number of priority stacks.") + .def("constraintsForPriority", &getConstraintsForPriority, + "Return list of constraints at the given priority level.") .def("dimension", &HierarchicalIterative::dimension, - return_value_policy()); + return_value_policy(), + "Return total dimension of the active constraints."); } } // namespace constraints } // namespace pyhpp diff --git a/src/pyhpp/constraints/locked-joint.cc b/src/pyhpp/constraints/locked-joint.cc index 7cd14c13..fe586491 100644 --- a/src/pyhpp/constraints/locked-joint.cc +++ b/src/pyhpp/constraints/locked-joint.cc @@ -66,8 +66,11 @@ LockedJointPtr_t createLockedJointWithComp(const DevicePtr_t& robot, void exposeLockedJoint() { class_, LockedJointPtr_t, boost::noncopyable>( "LockedJoint", no_init) - .def("__init__", make_constructor(&createLockedJoint)) - .def("__init__", make_constructor(&createLockedJointWithComp)); + .def("__init__", make_constructor(&createLockedJoint), + "Create a locked joint constraint fixing the named joint to the " + "given configuration.") + .def("__init__", make_constructor(&createLockedJointWithComp), + "Create a locked joint constraint with a custom comparison type."); } } // namespace constraints } // namespace pyhpp diff --git a/src/pyhpp/core/config-validation.cc b/src/pyhpp/core/config-validation.cc index 6f2c1a8c..f408807b 100644 --- a/src/pyhpp/core/config-validation.cc +++ b/src/pyhpp/core/config-validation.cc @@ -61,7 +61,8 @@ void exposeConfigValidation() { "ConfigValidation", no_init) .PYHPP_DEFINE_METHOD2(ConfigValidation, validate, DocClassMethod(validate)) - .def("validate", &CVWrapper::py_validate); + .def("validate", &CVWrapper::py_validate, + "Validate configuration; returns (valid, report)."); // DocClass (ConfigValidations) class_, @@ -69,7 +70,8 @@ void exposeConfigValidation() { .PYHPP_DEFINE_METHOD2(ConfigValidations, add, DocClassMethod(add)) .PYHPP_DEFINE_METHOD2(ConfigValidations, numberConfigValidations, DocClassMethod(numberConfigValidations)) - .def("clear", &ConfigValidations::clear); + .def("clear", &ConfigValidations::clear, + "Remove all config validations."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/connected-component.cc b/src/pyhpp/core/connected-component.cc index 399b5e13..06d41c15 100644 --- a/src/pyhpp/core/connected-component.cc +++ b/src/pyhpp/core/connected-component.cc @@ -82,7 +82,9 @@ void exposeConnectedComponent() { .def("reachableFrom", &CCWrapper::reachableFrom, DocClassMethod(reachableFrom)) .def("reachableTo", &CCWrapper::reachableTo, DocClassMethod(reachableTo)) - .def("__eq__", &CCWrapper::equality); + .def( + "__eq__", &CCWrapper::equality, + "Return true if both objects refer to the same connected component."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/constraint.cc b/src/pyhpp/core/constraint.cc index 803ca7ac..3f0e6a39 100644 --- a/src/pyhpp/core/constraint.cc +++ b/src/pyhpp/core/constraint.cc @@ -39,6 +39,24 @@ // DocNamespace(hpp::core) namespace { +const char* DOC_C_APPLY = + "Apply constraint to q in-place. Returns true on success."; +const char* DOC_C_ISSATISFIED1 = + "Return true if configuration q satisfies the constraint."; +const char* DOC_C_ISSATISFIED2 = + "Check if q satisfies the constraint; writes error into error. Returns " + "bool."; +const char* DOC_C_COPY = "Return a copy of this constraint."; +const char* DOC_CP_LASTISOPTIONAL = + "Get whether the last priority level is treated as optional."; +const char* DOC_CP_LASTISOPTIONAL_SET = + "Set whether the last priority level is treated as optional."; +const char* DOC_CP_SETRHSFROMCONFIG = + "Set right-hand side of all constraints from a configuration."; +const char* DOC_CP_SETRHSOFCONSTRAINT = + "Set right-hand side of a specific constraint from a configuration."; +const char* DOC_CP_NUMCONSTRAINTS = + "Return the list of numerical constraints held by this projector."; const char* DOC_CP_LINESEARCHTYPE = "Get/set the line search type."; const char* DOC_CP_MAXITER_GET = "Get maximal number of iterations in config projector."; @@ -107,10 +125,10 @@ void exposeConstraint() { .def("__str__", &to_str_from_operator) .def("name", &Constraint::name, return_internal_reference<>(), DocClassMethod(name)) - .def("apply", &CWrapper::apply) - .def("isSatisfied", &CWrapper::isSatisfied1) - .def("isSatisfied", &CWrapper::isSatisfied2) - .def("copy", &CWrapper::copy); + .def("apply", &CWrapper::apply, DOC_C_APPLY) + .def("isSatisfied", &CWrapper::isSatisfied1, DOC_C_ISSATISFIED1) + .def("isSatisfied", &CWrapper::isSatisfied2, DOC_C_ISSATISFIED2) + .def("copy", &CWrapper::copy, DOC_C_COPY); // DocClass(ConstraintSet) class_( - &ConfigProjector::lastIsOptional)) - .def("lastIsOptional", static_cast( - &ConfigProjector::lastIsOptional)) + .def("lastIsOptional", + static_cast( + &ConfigProjector::lastIsOptional), + DOC_CP_LASTISOPTIONAL) + .def("lastIsOptional", + static_cast( + &ConfigProjector::lastIsOptional), + DOC_CP_LASTISOPTIONAL_SET) .def("maxIterations", static_cast( &ConfigProjector::maxIterations), @@ -165,11 +187,13 @@ void exposeConstraint() { DOC_CP_ERRTHRESH_GET) .def("residualError", &ConfigProjector::residualError, DocClassMethod(residualError)) - .def("setRightHandSideFromConfig", &rightHandSideFromConfig) - .def("setRightHandSideOfConstraint", &setRightHandSideOfConstraint) + .def("setRightHandSideFromConfig", &rightHandSideFromConfig, + DOC_CP_SETRHSFROMCONFIG) + .def("setRightHandSideOfConstraint", &setRightHandSideOfConstraint, + DOC_CP_SETRHSOFCONSTRAINT) .def("sigma", &ConfigProjector::sigma, return_value_policy(), DocClassMethod(sigma)) - .def("numericalConstraints", &getNumConstraints); + .def("numericalConstraints", &getNumConstraints, DOC_CP_NUMCONSTRAINTS); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/distance.cc b/src/pyhpp/core/distance.cc index a96f3f40..1cf278c1 100644 --- a/src/pyhpp/core/distance.cc +++ b/src/pyhpp/core/distance.cc @@ -35,6 +35,15 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_WD_ASDISTANCE = + "Return a clone of this WeighedDistance as a Distance shared pointer."; +const char* DOC_WD_GETWEIGHTS = + "Return the weight vector used when computing distances."; +const char* DOC_WD_SETWEIGHTS = + "Set the weight vector used when computing distances."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -65,9 +74,10 @@ void exposeDistance() { class_, WeighedDistancePtr_t, boost::noncopyable>("WeighedDistance", no_init) .def("__init__", make_constructor(&WeighedDistance::create)) - .def("asDistancePtr_t", &DistanceWrapper::AsDistancePtr_t) - .def("getWeights", &DistanceWrapper::getWeights) - .def("setWeights", &DistanceWrapper::setWeights); + .def("asDistancePtr_t", &DistanceWrapper::AsDistancePtr_t, + DOC_WD_ASDISTANCE) + .def("getWeights", &DistanceWrapper::getWeights, DOC_WD_GETWEIGHTS) + .def("setWeights", &DistanceWrapper::setWeights, DOC_WD_SETWEIGHTS); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/node.cc b/src/pyhpp/core/node.cc index 9b232489..efc669c0 100644 --- a/src/pyhpp/core/node.cc +++ b/src/pyhpp/core/node.cc @@ -52,7 +52,8 @@ void exposeNode() { .def("addInEdge", &Node::addInEdge, DocClassMethod(addInEdge)) .def("connectedComponent", static_cast( - &Node::connectedComponent)) + &Node::connectedComponent), + "Return the connected component the node belongs to.") .def("connectedComponent", static_cast( &Node::connectedComponent), diff --git a/src/pyhpp/core/parameter.cc b/src/pyhpp/core/parameter.cc index f20525d8..a8a38662 100644 --- a/src/pyhpp/core/parameter.cc +++ b/src/pyhpp/core/parameter.cc @@ -33,6 +33,20 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_PAR_CREATEBOOL = "Create a Parameter holding a boolean value."; +const char* DOC_PAR_BOOL = "Return the boolean value of this parameter."; +const char* DOC_PAR_INT = "Return the integer value of this parameter."; +const char* DOC_PAR_FLOAT = + "Return the floating-point value of this parameter."; +const char* DOC_PAR_STRING = "Return the string value of this parameter."; +const char* DOC_PAR_VECTOR = "Return the vector value of this parameter."; +const char* DOC_PAR_MATRIX = "Return the matrix value of this parameter."; +const char* DOC_PAR_VALUE = + "Return the parameter value as a Python object (bool, int, float, str, " + "numpy array)."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -85,15 +99,15 @@ void exposeParameter() { // DocClass(Parameter) class_("Parameter", no_init) .def("__init__", &create) - .def("create_bool", &createBool) + .def("create_bool", &createBool, DOC_PAR_CREATEBOOL) .staticmethod("create_bool") - .PYHPP_DEFINE_METHOD(Parameter, boolValue) - .PYHPP_DEFINE_METHOD(Parameter, intValue) - .PYHPP_DEFINE_METHOD(Parameter, floatValue) - .PYHPP_DEFINE_METHOD(Parameter, stringValue) - .PYHPP_DEFINE_METHOD(Parameter, vectorValue) - .PYHPP_DEFINE_METHOD(Parameter, matrixValue) - .def("value", ¶meter_as_python_object); + .def("boolValue", &Parameter::boolValue, DOC_PAR_BOOL) + .def("intValue", &Parameter::intValue, DOC_PAR_INT) + .def("floatValue", &Parameter::floatValue, DOC_PAR_FLOAT) + .def("stringValue", &Parameter::stringValue, DOC_PAR_STRING) + .def("vectorValue", &Parameter::vectorValue, DOC_PAR_VECTOR) + .def("matrixValue", &Parameter::matrixValue, DOC_PAR_MATRIX) + .def("value", ¶meter_as_python_object, DOC_PAR_VALUE); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path-optimizer.cc b/src/pyhpp/core/path-optimizer.cc index 6792327c..18ab512a 100644 --- a/src/pyhpp/core/path-optimizer.cc +++ b/src/pyhpp/core/path-optimizer.cc @@ -50,6 +50,36 @@ const char* DOC_TTP_MAXACC = "Maximum acceleration for each output degree of freedom."; const char* DOC_TTP_MINDUR = "Minimum duration assigned to each non-empty subpath."; +const char* DOC_STP_SAFETY = "A scaling factor for the velocity bounds."; +const char* DOC_STP_ORDER = "The desired continuity order (0, 1, or 2)."; +const char* DOC_STP_MAXACC = + "The maximum acceleration for each degree of freedom. Not considered if " + "negative."; +const char* DOC_SGB_ALPHAINIT = + "In [0,1]. Initial value when interpolating between non-colliding current " + "solution and the optimal colliding trajectory."; +const char* DOC_SGB_ALWAYSSTOPFIRST = + "If true, consider only one (not all) collision constraint per iteration."; +const char* DOC_SGB_COSTORDER = + "Order of the derivative used for the optimized cost function (most likely " + "1, 2, or 3)."; +const char* DOC_SGB_USEPATHLENGTH = + "If true, the initial path length is used to weight the splines."; +const char* DOC_SGB_REORDERINTERVALS = + "If true, intervals in collision are checked first at the next iteration."; +const char* DOC_SGB_LINEARIZE = + "If true, collision constraint will be re-linearized at each iteration."; +const char* DOC_SGB_CHECKJOINTBOUND = "If true, joint bounds are enforced."; +const char* DOC_SGB_RETURNOPTIMUM = + "If true, returns the optimum regardless of collision (for debugging)."; +const char* DOC_SGB_COSTTHRESHOLD = + "Stop optimizing if the cost improves less than this threshold between two " + "iterations."; +const char* DOC_SGB_GUESSTHRESHOLD = + "Threshold to detect rows of zeros in the Jacobian (passive DoF). Negative " + "disables the check."; +const char* DOC_SGB_QPACCURACY = + "Accuracy of the QP solver (only used by proxqp)."; } // namespace using namespace boost::python; @@ -65,17 +95,25 @@ void exposeSplineGradientBased(const char* name) { class_, bases, boost::noncopyable>(name, no_init) .def("__init__", make_constructor(&SGB_t::create)) - .def_readwrite("alphaInit", &SGB_t::alphaInit) - .def_readwrite("alwaysStopAtFirst", &SGB_t::alwaysStopAtFirst) - .def_readwrite("costOrder", &SGB_t::costOrder) - .def_readwrite("usePathLengthAsWeights", &SGB_t::usePathLengthAsWeights) - .def_readwrite("reorderIntervals", &SGB_t::reorderIntervals) - .def_readwrite("linearizeAtEachStep", &SGB_t::linearizeAtEachStep) - .def_readwrite("checkJointBound", &SGB_t::checkJointBound) - .def_readwrite("returnOptimum", &SGB_t::returnOptimum) - .def_readwrite("costThreshold", &SGB_t::costThreshold) - .def_readwrite("guessThreshold", &SGB_t::guessThreshold) - .def_readwrite("QPAccuracy", &SGB_t::QPAccuracy); + .def_readwrite("alphaInit", &SGB_t::alphaInit, DOC_SGB_ALPHAINIT) + .def_readwrite("alwaysStopAtFirst", &SGB_t::alwaysStopAtFirst, + DOC_SGB_ALWAYSSTOPFIRST) + .def_readwrite("costOrder", &SGB_t::costOrder, DOC_SGB_COSTORDER) + .def_readwrite("usePathLengthAsWeights", &SGB_t::usePathLengthAsWeights, + DOC_SGB_USEPATHLENGTH) + .def_readwrite("reorderIntervals", &SGB_t::reorderIntervals, + DOC_SGB_REORDERINTERVALS) + .def_readwrite("linearizeAtEachStep", &SGB_t::linearizeAtEachStep, + DOC_SGB_LINEARIZE) + .def_readwrite("checkJointBound", &SGB_t::checkJointBound, + DOC_SGB_CHECKJOINTBOUND) + .def_readwrite("returnOptimum", &SGB_t::returnOptimum, + DOC_SGB_RETURNOPTIMUM) + .def_readwrite("costThreshold", &SGB_t::costThreshold, + DOC_SGB_COSTTHRESHOLD) + .def_readwrite("guessThreshold", &SGB_t::guessThreshold, + DOC_SGB_GUESSTHRESHOLD) + .def_readwrite("QPAccuracy", &SGB_t::QPAccuracy, DOC_SGB_QPACCURACY); } void exposePathOptimizer() { @@ -115,12 +153,15 @@ void exposePathOptimizer() { make_constructor( &pathOptimization::SimpleTimeParameterization::create)) .def_readwrite("safety", - &pathOptimization::SimpleTimeParameterization::safety) + &pathOptimization::SimpleTimeParameterization::safety, + DOC_STP_SAFETY) .def_readwrite("order", - &pathOptimization::SimpleTimeParameterization::order) + &pathOptimization::SimpleTimeParameterization::order, + DOC_STP_ORDER) .def_readwrite( "maxAcceleration", - &pathOptimization::SimpleTimeParameterization::maxAcceleration); + &pathOptimization::SimpleTimeParameterization::maxAcceleration, + DOC_STP_MAXACC); class_, diff --git a/src/pyhpp/core/path-projector.cc b/src/pyhpp/core/path-projector.cc index c6678db2..aab4b276 100644 --- a/src/pyhpp/core/path-projector.cc +++ b/src/pyhpp/core/path-projector.cc @@ -62,7 +62,8 @@ void exposePathProjector() { class_("PathProjector", no_init) .def("apply", &PPWrapper::apply, DocClassMethod(apply)) - .def("apply", &PPWrapper::py_apply); + .def("apply", &PPWrapper::py_apply, + "Apply projection to path; returns (success, projectedPath)."); class_, hpp::core::pathProjector::ProgressivePtr_t, boost::noncopyable>( @@ -86,7 +87,8 @@ void exposePathProjector() { const value_type&) -> PathProjectorPtr_t { return PathProjectorPtr_t(); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Return a null path projector (no projection)."); def( "ProgressiveProjector", @@ -96,7 +98,8 @@ void exposePathProjector() { return pathProjector::Progressive::create( distance, steeringMethodWrapper->obj, step); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Create a progressive path projector with the given step size."); def( "DichotomyProjector", @@ -106,7 +109,8 @@ void exposePathProjector() { return pathProjector::Dichotomy::create( distance, steeringMethodWrapper->obj, step); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Create a dichotomy-based path projector with the given step size."); def( "GlobalProjector", @@ -116,7 +120,8 @@ void exposePathProjector() { return pathProjector::Global::create(distance, steeringMethodWrapper->obj, step); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Create a global path projector with the given step size."); def( "RecursiveHermiteProjector", @@ -126,7 +131,8 @@ void exposePathProjector() { return pathProjector::RecursiveHermite::create( distance, steeringMethodWrapper->obj, step); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Create a recursive Hermite path projector with the given step size."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path-validation.cc b/src/pyhpp/core/path-validation.cc index b46db98e..b2e5dc51 100644 --- a/src/pyhpp/core/path-validation.cc +++ b/src/pyhpp/core/path-validation.cc @@ -85,8 +85,10 @@ void exposePathValidation() { class_( "PathValidation", no_init) .def("validate", &PVWrapper::validate, DocClassMethod(validate)) - .def("validate", &PVWrapper::py_validate) - .def("validateConfiguration", &PVWrapper::validateConfiguration); + .def("validate", &PVWrapper::py_validate, + "Validate path; returns (valid, validPart, report).") + .def("validateConfiguration", &PVWrapper::validateConfiguration, + "Validate a configuration; returns (valid, report)."); class_, hpp::core::pathValidation::DiscretizedPtr_t, boost::noncopyable>( @@ -101,19 +103,26 @@ void exposePathValidation() { "Dichotomy", no_init); def("Discretized", &pathValidation::createDiscretizedCollisionChecking, - (arg("robot"), arg("stepSize"))); + (arg("robot"), arg("stepSize")), + "Create a discretized collision-checking path validation."); def("DiscretizedCollision", &pathValidation::createDiscretizedCollisionChecking, - (arg("robot"), arg("stepSize"))); + (arg("robot"), arg("stepSize")), + "Create a discretized collision-checking path validation."); def("DiscretizedJointBound", &pathValidation::createDiscretizedJointBound, - (arg("robot"), arg("stepSize"))); + (arg("robot"), arg("stepSize")), + "Create a discretized joint-bound path validation."); def("DiscretizedCollisionAndJointBound", &PVWrapper::createDiscretizedJointBoundAndCollisionChecking, - (arg("robot"), arg("stepSize"))); + (arg("robot"), arg("stepSize")), + "Create a discretized path validation checking both collision and joint " + "bounds."); def("Progressive", &continuousValidation::Progressive::create, - (arg("robot"), arg("tolerance"))); + (arg("robot"), arg("tolerance")), + "Create a progressive continuous path validation."); def("Dichotomy", &continuousValidation::Dichotomy::create, - (arg("robot"), arg("tolerance"))); + (arg("robot"), arg("tolerance")), + "Create a dichotomy-based continuous path validation."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path.cc b/src/pyhpp/core/path.cc index 8110fc8b..5c7c49d0 100644 --- a/src/pyhpp/core/path.cc +++ b/src/pyhpp/core/path.cc @@ -38,6 +38,17 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_PATH_CALL1 = + "Evaluate path at parameter t. Returns (configuration, success)."; +const char* DOC_PATH_CALL2 = + "Evaluate path at parameter t into q. Returns success flag."; +const char* DOC_PATH_DERIVATIVE = + "Compute derivative of given order at parameter t. Returns a numpy vector."; +const char* DOC_PATH_CONSTRAINTS = + "Return the constraint set attached to the path, or None."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -147,11 +158,11 @@ void exposePath() { class_, boost::noncopyable>("Path", no_init) .def("__str__", &to_str_from_operator) - .def("__call__", &PathWrap::py_call1) - .def("__call__", &PathWrap::py_call2) - .def("eval", &PathWrap::py_call1) - .def("eval", &PathWrap::py_call2) - .def("derivative", &PathWrap::derivative) + .def("__call__", &PathWrap::py_call1, DOC_PATH_CALL1) + .def("__call__", &PathWrap::py_call2, DOC_PATH_CALL2) + .def("eval", &PathWrap::py_call1, DOC_PATH_CALL1) + .def("eval", &PathWrap::py_call2, DOC_PATH_CALL2) + .def("derivative", &PathWrap::derivative, DOC_PATH_DERIVATIVE) .def("copy", static_cast(&Path::copy), DocClassMethod(copy)) @@ -170,7 +181,7 @@ void exposePath() { .def("length", &Path::length, DocClassMethod(length)) .def("initial", &Path::initial, DocClassMethod(initial)) .def("end", &Path::end, DocClassMethod(end)) - .def("constraints", &PathWrap::constraints) + .def("constraints", &PathWrap::constraints, DOC_PATH_CONSTRAINTS) .def("outputSize", &Path::outputSize, DocClassMethod(outputSize)) .def("outputDerivativeSize", &Path::outputDerivativeSize, DocClassMethod(outputDerivativeSize)); diff --git a/src/pyhpp/core/problem.cc b/src/pyhpp/core/problem.cc index b6b6ee07..3b6c3db3 100644 --- a/src/pyhpp/core/problem.cc +++ b/src/pyhpp/core/problem.cc @@ -53,6 +53,63 @@ #include #include +namespace { +const char* DOC_P_SM_GET = "Get the steering method."; +const char* DOC_P_SM_SET = "Set the steering method."; +const char* DOC_P_CV_GET = "Get the config validations object."; +const char* DOC_P_CV_SET = "Set the config validations object."; +const char* DOC_P_PV_GET = "Get the path validation object."; +const char* DOC_P_PV_SET = "Set the path validation object."; +const char* DOC_P_PP_GET = "Get the path projector."; +const char* DOC_P_PP_SET = "Set the path projector."; +const char* DOC_P_DIST_GET = "Get the distance function."; +const char* DOC_P_DIST_SET = "Set the distance function."; +const char* DOC_P_TARGET_GET = "Get the problem target."; +const char* DOC_P_TARGET_SET = "Set the problem target."; +const char* DOC_P_CS_GET = "Get the configuration shooter."; +const char* DOC_P_CS_SET = "Set the configuration shooter."; +const char* DOC_P_ADDPARTIALCOM = + "Create a named partial COM computation from a list of joint names."; +const char* DOC_P_GETPARTIALCOM = + "Compute and return the position of the named partial center of mass."; +const char* DOC_P_CREATERELATIVECOM = + "Create an implicit constraint: relative COM equals a reference point " + "expressed in a joint frame."; +const char* DOC_P_CREATETRANSFORMATION = + "Create a relative or absolute transformation constraint between two " + "frames, with a 6-bool mask."; +const char* DOC_P_SETCONSTANTRHS = + "If constant=True, set comparison type to EqualToZero; otherwise to " + "Equality."; +const char* DOC_P_APPLYCONSTRAINTS = + "Project config onto active constraints. Returns (success, " + "projected_config, " + "residual_error)."; +const char* DOC_P_ISCONFIGVALID = + "Validate config against all config validations. Returns (valid, report)."; +const char* DOC_P_SETCONSTRAINTS = + "Set the active constraint set used by applyConstraints."; +const char* DOC_P_GETCONSTRAINTS = "Return the active constraint set."; +const char* DOC_P_SETRHS = + "Update right-hand side of the config projector from a configuration."; +const char* DOC_P_ADDNUMCONSTRAINTS1 = + "Add constraints with priorities to the config projector, creating it if " + "needed."; +const char* DOC_P_ADDNUMCONSTRAINTS2 = + "Add constraints at priority 0 to the config projector, creating it if " + "needed."; +const char* DOC_P_CREATECOMBETWEENFEET = + "Create an implicit constraint: COM lies between two contact points."; +const char* DOC_P_DIRECTPATH = + "Compute a direct path using the steering method. Returns (valid, path, " + "report)."; +const char* DOC_P_ERRTHRESH = + "Error threshold used when creating a new ConfigProjector."; +const char* DOC_P_MAXITERPROJ = + "Maximum iterations for projection used when creating a new " + "ConfigProjector."; +} // namespace + namespace pyhpp { namespace core { @@ -615,17 +672,19 @@ void exposeProblem() { DocClassMethod(getParameter)) .def("steeringMethod", static_cast( - &Problem::steeringMethod)) + &Problem::steeringMethod), + DOC_P_SM_GET) .def("steeringMethod", static_cast( - &Problem::steeringMethod)) + &Problem::steeringMethod), + DOC_P_SM_SET) .def("configValidation", static_cast(&Problem::configValidation), - return_value_policy()) + return_value_policy(), DOC_P_CV_GET) .def("configValidation", static_cast(&Problem::configValidation), - (arg("configValidation"))) + (arg("configValidation")), DOC_P_CV_SET) .def("addConfigValidation", &Problem::addConfigValidation, DocClassMethod(addConfigValidation)) @@ -633,55 +692,69 @@ void exposeProblem() { DocClassMethod(clearConfigValidations)) .def("pathValidation", - static_cast(&Problem::pathValidation)) + static_cast(&Problem::pathValidation), + DOC_P_PV_GET) .def("pathValidation", static_cast(&Problem::pathValidation), - (arg("pathValidation"))) + (arg("pathValidation")), DOC_P_PV_SET) .def("pathProjector", - static_cast(&Problem::pathProjector)) + static_cast(&Problem::pathProjector), DOC_P_PP_GET) .def("pathProjector", static_cast(&Problem::pathProjector), - (arg("pathProjector"))) + (arg("pathProjector")), DOC_P_PP_SET) - .def("distance", static_cast(&Problem::distance)) + .def("distance", static_cast(&Problem::distance), + DOC_P_DIST_GET) .def("distance", static_cast(&Problem::distance), - (arg("distance"))) + (arg("distance")), DOC_P_DIST_SET) .def("target", static_cast(&Problem::target), - return_value_policy()) - .def("target", static_cast(&Problem::target), (arg("target"))) + return_value_policy(), DOC_P_TARGET_GET) + .def("target", static_cast(&Problem::target), (arg("target")), + DOC_P_TARGET_SET) .def("configurationShooter", - static_cast(&Problem::configurationShooter)) + static_cast(&Problem::configurationShooter), + DOC_P_CS_GET) .def("configurationShooter", static_cast(&Problem::configurationShooter), - (arg("configurationShooter"))) + (arg("configurationShooter")), DOC_P_CS_SET) .def("initConfig", &Problem::initConfig, DocClassMethod(initConfig)) .def("addGoalConfig", &Problem::addGoalConfig, DocClassMethod(addGoalConfig)) .def("resetGoalConfigs", &Problem::resetGoalConfigs, DocClassMethod(resetGoalConfigs)) - .PYHPP_DEFINE_METHOD(Problem, addPartialCom) - .PYHPP_DEFINE_METHOD(Problem, getPartialCom) - .PYHPP_DEFINE_METHOD(Problem, createRelativeComConstraint) + .def("addPartialCom", &Problem::addPartialCom, DOC_P_ADDPARTIALCOM) + .def("getPartialCom", &Problem::getPartialCom, DOC_P_GETPARTIALCOM) + .def("createRelativeComConstraint", &Problem::createRelativeComConstraint, + DOC_P_CREATERELATIVECOM) .def("createTransformationConstraint", - &Problem::createTransformationConstraint) + &Problem::createTransformationConstraint, DOC_P_CREATETRANSFORMATION) .def("createTransformationConstraint", - &Problem::createTransformationConstraint2) - .PYHPP_DEFINE_METHOD(Problem, setConstantRightHandSide) - .PYHPP_DEFINE_METHOD(Problem, applyConstraints) - .PYHPP_DEFINE_METHOD(Problem, isConfigValid) - .PYHPP_DEFINE_METHOD(Problem, setConstraints) - .PYHPP_DEFINE_METHOD(Problem, getConstraints) - .PYHPP_DEFINE_METHOD(Problem, setRightHandSideFromConfig) + &Problem::createTransformationConstraint2, + DOC_P_CREATETRANSFORMATION) + .def("setConstantRightHandSide", &Problem::setConstantRightHandSide, + DOC_P_SETCONSTANTRHS) + .def("applyConstraints", &Problem::applyConstraints, + DOC_P_APPLYCONSTRAINTS) + .def("isConfigValid", &Problem::isConfigValid, DOC_P_ISCONFIGVALID) + .def("setConstraints", &Problem::setConstraints, DOC_P_SETCONSTRAINTS) + .def("getConstraints", &Problem::getConstraints, DOC_P_GETCONSTRAINTS) + .def("setRightHandSideFromConfig", &Problem::setRightHandSideFromConfig, + DOC_P_SETRHS) .def("addNumericalConstraintsToConfigProjector", - &Problem::addNumericalConstraintsToConfigProjector1) + &Problem::addNumericalConstraintsToConfigProjector1, + DOC_P_ADDNUMCONSTRAINTS1) .def("addNumericalConstraintsToConfigProjector", - &Problem::addNumericalConstraintsToConfigProjector2) - .def_readwrite("errorThreshold", &Problem::errorThreshold_) - .def_readwrite("maxIterProjection", &Problem::maxIterProjection_) - .PYHPP_DEFINE_METHOD(Problem, createComBetweenFeet) - .PYHPP_DEFINE_METHOD(Problem, directPath); + &Problem::addNumericalConstraintsToConfigProjector2, + DOC_P_ADDNUMCONSTRAINTS2) + .def_readwrite("errorThreshold", &Problem::errorThreshold_, + DOC_P_ERRTHRESH) + .def_readwrite("maxIterProjection", &Problem::maxIterProjection_, + DOC_P_MAXITERPROJ) + .def("createComBetweenFeet", &Problem::createComBetweenFeet, + DOC_P_CREATECOMBETWEENFEET) + .def("directPath", &Problem::directPath, DOC_P_DIRECTPATH); register_problem_converters(); } diff --git a/src/pyhpp/core/roadmap.cc b/src/pyhpp/core/roadmap.cc index 3e846343..44efb8e7 100644 --- a/src/pyhpp/core/roadmap.cc +++ b/src/pyhpp/core/roadmap.cc @@ -35,6 +35,45 @@ // DocNamespace(hpp::core) +namespace { +const char* DOC_R_ADDNODE = + "Add a node with the given configuration to the roadmap."; +const char* DOC_R_NEAREST1 = + "Find nearest node to configuration (optionally searching in reverse " + "direction). Returns (configuration, minDistance)."; +const char* DOC_R_NEAREST3 = + "Find nearest node within a connected component. Returns (configuration, " + "minDistance)."; +const char* DOC_R_NEARESTNODES1 = + "Find the k nearest nodes to configuration. Returns a list of nodes."; +const char* DOC_R_NEARESTNODES2 = + "Find the k nearest nodes within a connected component."; +const char* DOC_R_ADDNODEEDGES = + "Add node at 'to' and edges from the node nearest to 'from'."; +const char* DOC_R_ADDNODEEDGE = + "Add node at 'to' and a single directed edge from the node nearest to " + "'from'."; +const char* DOC_R_ADDEDGE1 = + "Add nodes at from and to, then add a directed edge between them."; +const char* DOC_R_ADDEDGE2 = + "Add nodes at from and to; if bothEdges is true also adds the reverse " + "edge."; +const char* DOC_R_NODES = + "Return list of all node configurations in the roadmap."; +const char* DOC_R_NODESCC = + "Return list of configurations in the connected component identified by " + "connectedComponentId."; +const char* DOC_R_INITNODE1 = + "Set the initial node to the given configuration."; +const char* DOC_R_INITNODE2 = "Return the current initial node object."; +const char* DOC_R_CCS = "Return list of all connected components."; +const char* DOC_R_NUMCCS = "Return the number of connected components."; +const char* DOC_R_GETCC = "Return connected component by index."; +const char* DOC_R_CCOFNODE = + "Return the connected component of the node nearest to the given " + "configuration."; +} // namespace + using namespace boost::python; namespace pyhpp { @@ -226,21 +265,21 @@ void exposeRoadmap() { .def("__str__", &to_str) .def("clear", &Roadmap::clear, DocClassMethod(clear)) .def("addNode", &RWrapper::addNode, - return_value_policy()) - .def("nearestNode", &RWrapper::nearestNode1) - .def("nearestNode", &RWrapper::nearestNode2) - .def("nearestNode", &RWrapper::nearestNode3) - .def("nearestNode", &RWrapper::nearestNode4) - .def("nearestNodes", &RWrapper::nearestNodes1) - .def("nearestNodes", &RWrapper::nearestNodes2) + return_value_policy(), DOC_R_ADDNODE) + .def("nearestNode", &RWrapper::nearestNode1, DOC_R_NEAREST1) + .def("nearestNode", &RWrapper::nearestNode2, DOC_R_NEAREST1) + .def("nearestNode", &RWrapper::nearestNode3, DOC_R_NEAREST3) + .def("nearestNode", &RWrapper::nearestNode4, DOC_R_NEAREST3) + .def("nearestNodes", &RWrapper::nearestNodes1, DOC_R_NEARESTNODES1) + .def("nearestNodes", &RWrapper::nearestNodes2, DOC_R_NEARESTNODES2) .def("nodesWithinBall", &Roadmap::nodesWithinBall, DocClassMethod(nodesWithinBall)) .def("addNodeAndEdges", &RWrapper::addNodeAndEdges, - return_value_policy()) + return_value_policy(), DOC_R_ADDNODEEDGES) .def("addNodeAndEdge", &RWrapper::addNodeAndEdge, - return_value_policy()) - .def("addEdge", &RWrapper::addEdge) - .def("addEdge", &RWrapper::addEdge2) + return_value_policy(), DOC_R_ADDNODEEDGE) + .def("addEdge", &RWrapper::addEdge, DOC_R_ADDEDGE1) + .def("addEdge", &RWrapper::addEdge2, DOC_R_ADDEDGE2) .def("addEdges", &Roadmap::addEdges, DocClassMethod(addEdges)) .def("merge", static_cast(&Roadmap::merge), @@ -253,19 +292,23 @@ void exposeRoadmap() { .def("resetGoalNodes", &Roadmap::resetGoalNodes, DocClassMethod(resetGoalNodes)) .def("pathExists", &Roadmap::pathExists, DocClassMethod(pathExists)) - .def("nodes", &RWrapper::nodes) - .def("nodesConnectedComponent", &RWrapper::nodesConnectedComponent) - .def("initNode", &RWrapper::initNode1) + .def("nodes", &RWrapper::nodes, DOC_R_NODES) + .def("nodesConnectedComponent", &RWrapper::nodesConnectedComponent, + DOC_R_NODESCC) + .def("initNode", &RWrapper::initNode1, DOC_R_INITNODE1) .def("initNode", &RWrapper::initNode2, - return_value_policy()) + return_value_policy(), DOC_R_INITNODE2) .def("goalNodes", &Roadmap::goalNodes, return_internal_reference<>(), DocClassMethod(goalNodes)) - .def("connectedComponents", &RWrapper::connectedComponents) + .def("connectedComponents", &RWrapper::connectedComponents, DOC_R_CCS) .def("distance", &Roadmap::distance, return_internal_reference<>(), DocClassMethod(distance)) - .def("numberConnectedComponents", &RWrapper::numberConnectedComponents) - .def("getConnectedComponent", &RWrapper::getConnectedComponent) - .def("connectedComponentOfNode", &RWrapper::connectedComponentOfNode); + .def("numberConnectedComponents", &RWrapper::numberConnectedComponents, + DOC_R_NUMCCS) + .def("getConnectedComponent", &RWrapper::getConnectedComponent, + DOC_R_GETCC) + .def("connectedComponentOfNode", &RWrapper::connectedComponentOfNode, + DOC_R_CCOFNODE); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/steering-method.cc b/src/pyhpp/core/steering-method.cc index 3b56a083..a36965f4 100644 --- a/src/pyhpp/core/steering-method.cc +++ b/src/pyhpp/core/steering-method.cc @@ -144,7 +144,9 @@ void exposeSteeringMethod() { register_ptr_to_python>(); // DocClass(SteeringMethod) class_("SteeringMethod", no_init) - .def("__call__", &SteeringMethod::operator()) + .def("__call__", &SteeringMethod::operator(), + "Compute a path between two configurations using the steering " + "method.") .def("steer", &SteeringMethod::steer, DocClassMethod(steer)) .def("problem", &SteeringMethod::problem, DocClassMethod(problem)) .def("constraints", diff --git a/src/pyhpp/manipulation/device.cc b/src/pyhpp/manipulation/device.cc index 19318fe9..70aead02 100644 --- a/src/pyhpp/manipulation/device.cc +++ b/src/pyhpp/manipulation/device.cc @@ -344,13 +344,20 @@ void exposeDevice() { // DocClass(Device) class_, boost::shared_ptr, boost::noncopyable>("Device", init()) - .def("setRobotRootPosition", &Device::setRobotRootPosition) - .def("handles", &Device::handles) - .def("grippers", &Device::grippers) + .def("setRobotRootPosition", &Device::setRobotRootPosition, + "Set the root position of a sub-robot (identified by name) relative " + "to its parent joint.") + .def("handles", &Device::handles, + "Return a map from handle name to Handle object.") + .def("grippers", &Device::grippers, + "Return a map from gripper name to Gripper object.") .def("asPinDevice", &asPinDevice) - .def("getJointNames", &Device::getJointNames) - .def("getJointConfig", &Device::getJointConfig) - .def("setJointBounds", &Device::setJointBounds) + .def("getJointNames", &Device::getJointNames, + "Return list of all joint names in the Pinocchio model.") + .def("getJointConfig", &Device::getJointConfig, + "Return current configuration values of the named joint.") + .def("setJointBounds", &Device::setJointBounds, + "Set joint bounds from a flat list [min0, max0, min1, max1, ...].") .def("contactSurfaceNames", &Device::contactSurfaceNames, "Return list of contact surface names registered on device") .def("contactSurfaces", &Device::contactSurfaces, diff --git a/src/pyhpp/manipulation/graph.cc b/src/pyhpp/manipulation/graph.cc index de31775b..614e345f 100644 --- a/src/pyhpp/manipulation/graph.cc +++ b/src/pyhpp/manipulation/graph.cc @@ -1313,13 +1313,30 @@ void exposeGraph() { "Graph", init()) - .def("_get_native_graph", &getGraphCapsule) - .def_readwrite("robot", &PyWGraph::robot) + .def( + "_get_native_graph", &getGraphCapsule, + "Return a capsule wrapping the native C++ Graph object (for external " + "interop).") + .def_readwrite("robot", &PyWGraph::robot, + "The robot device of the constraint graph.") // Configuration methods - .PYHPP_DEFINE_GETTER_SETTER(PyWGraph, maxIterations, - hpp::manipulation::size_type) - .PYHPP_DEFINE_GETTER_SETTER_CONST_REF(PyWGraph, errorThreshold, - hpp::manipulation::value_type) + .def("maxIterations", + static_cast( + &PyWGraph::maxIterations), + "Get maximal number of iterations in config projector.") + .def("maxIterations", + static_cast( + &PyWGraph::maxIterations), + "Set maximal number of iterations.") + .def("errorThreshold", + static_cast( + &PyWGraph::errorThreshold), + "Get error threshold in config projector.") + .def( + "errorThreshold", + static_cast( + &PyWGraph::errorThreshold), + "Set error threshold.") // Graph construction .PYHPP_DEFINE_METHOD1(PyWGraph, createState, DOC_CREATESTATE) @@ -1339,12 +1356,18 @@ void exposeGraph() { .PYHPP_DEFINE_METHOD1(PyWGraph, setWeight, DOC_SETWEIGHT) .PYHPP_DEFINE_METHOD1(PyWGraph, getWeight, DOC_GETWEIGHT) .PYHPP_DEFINE_METHOD1(PyWGraph, setWaypoint, DOC_SETWAYPOINT) - .PYHPP_DEFINE_METHOD(PyWGraph, getState) - .PYHPP_DEFINE_METHOD(PyWGraph, getTransition) - .PYHPP_DEFINE_METHOD(PyWGraph, getStates) - .PYHPP_DEFINE_METHOD(PyWGraph, getTransitions) - .PYHPP_DEFINE_METHOD(PyWGraph, getStateNames) - .PYHPP_DEFINE_METHOD(PyWGraph, getTransitionNames) + .def("getState", &PyWGraph::getState, + "Return the state with the given name.") + .def("getTransition", &PyWGraph::getTransition, + "Return the transition with the given name.") + .def("getStates", &PyWGraph::getStates, + "Return a list of all states in the constraint graph.") + .def("getTransitions", &PyWGraph::getTransitions, + "Return a list of all transitions in the constraint graph.") + .def("getStateNames", &PyWGraph::getStateNames, + "Return a list of state names.") + .def("getTransitionNames", &PyWGraph::getTransitionNames, + "Return a list of transition names.") // State queries .PYHPP_DEFINE_METHOD1(PyWGraph, getStateFromConfiguration, DOC_GETSTATE) @@ -1356,7 +1379,10 @@ void exposeGraph() { DOC_ADDNUMERICALCONSTRAINTSTOSTATE) .PYHPP_DEFINE_METHOD1(PyWGraph, addNumericalConstraintsToTransition, DOC_ADDNUMERICALCONSTRAINTSTOTRANSITION) - .PYHPP_DEFINE_METHOD(PyWGraph, addNumericalConstraintsToGraph) + .def("addNumericalConstraintsToGraph", + &PyWGraph::addNumericalConstraintsToGraph, + "Add a list of numerical constraints to all transitions in the " + "graph.") .PYHPP_DEFINE_METHOD1(PyWGraph, addNumericalConstraintsForPath, DOC_ADDNUMERICALCONSTRAINTSFORPATH) .PYHPP_DEFINE_METHOD1(PyWGraph, getNumericalConstraintsForState, @@ -1379,8 +1405,11 @@ void exposeGraph() { .def("createPrePlacementConstraint", &PyWGraph::createPrePlacementConstraint2, DOC_CREATEPREPLACEMENTCONSTRAINT) - .def("createGraspConstraint", &PyWGraph::createGraspConstraint) - .def("createPreGraspConstraint", &PyWGraph::createPreGraspConstraint) + .def("createGraspConstraint", &PyWGraph::createGraspConstraint, + "Create grasp, complement and hold constraints for a gripper-handle " + "pair. Returns a list [grasp, complement, hold].") + .def("createPreGraspConstraint", &PyWGraph::createPreGraspConstraint, + "Create a pre-grasp constraint for a gripper-handle pair.") // Configuration error checking .PYHPP_DEFINE_METHOD1(PyWGraph, getConfigErrorForState, DOC_GETCONFIGERRORFORSTATE) @@ -1430,7 +1459,9 @@ void exposeGraph() { .PYHPP_DEFINE_METHOD1(PyWGraph, initialize, DOC_INITIALIZE) // Transition at parameter. - .def("transitionAtParam", &PyWGraph::transitionAtParam) + .def("transitionAtParam", &PyWGraph::transitionAtParam, + "Return the transition used at a given parameter on a path (static " + "method).") .staticmethod("transitionAtParam"); } diff --git a/src/pyhpp/manipulation/path-planner.cc b/src/pyhpp/manipulation/path-planner.cc index 0e5f774f..e7e8bcd8 100644 --- a/src/pyhpp/manipulation/path-planner.cc +++ b/src/pyhpp/manipulation/path-planner.cc @@ -335,8 +335,12 @@ void exposePathPlanners() { .def("computePath", &TransitionPlanner::computePath, DocClassMethod(computePath)) .def("planPath", &TransitionPlanner::planPath, DocClassMethod(planPath)) - .def("directPath", &TransitionPlanner::directPath) - .def("validateConfiguration", &TransitionPlanner::validateConfiguration) + .def("directPath", &TransitionPlanner::directPath, + "Compute a direct path on a transition. Returns (success, path, " + "status).") + .def("validateConfiguration", &TransitionPlanner::validateConfiguration, + "Validate configuration against the graph state identified by id. " + "Returns (valid, report).") .def("optimizePath", &TransitionPlanner::optimizePath, DocClassMethod(optimizePath)) .def("timeParameterization", &TransitionPlanner::timeParameterization, diff --git a/src/pyhpp/manipulation/problem.cc b/src/pyhpp/manipulation/problem.cc index 71970635..5ec1f2d7 100644 --- a/src/pyhpp/manipulation/problem.cc +++ b/src/pyhpp/manipulation/problem.cc @@ -142,14 +142,19 @@ void exposeProblem() { .def( "steeringMethod", static_cast( - &Problem::steeringMethod)) - .def("steeringMethod", static_cast( - &Problem::steeringMethod)) + &Problem::steeringMethod), + "Get the inner steering method (unwrapped from the graph steering " + "method if applicable).") + .def("steeringMethod", + static_cast( + &Problem::steeringMethod), + "Set the steering method.") .def("steeringMethod", static_cast( - &Problem::graphSteeringMethod)) + &Problem::graphSteeringMethod), + "Set the graph steering method.") // .PYHPP_DEFINE_GETTER_SETTER_CONST_REF(Problem, pathValidation, // PathValidationPtr_t) .PYHPP_DEFINE_METHOD(Problem, // manipulationSteeringMethod) .PYHPP_DEFINE_METHOD(Problem, diff --git a/src/pyhpp/manipulation/urdf/util.cc b/src/pyhpp/manipulation/urdf/util.cc index 69821515..e73e2542 100644 --- a/src/pyhpp/manipulation/urdf/util.cc +++ b/src/pyhpp/manipulation/urdf/util.cc @@ -74,8 +74,12 @@ void loadModelFromString(const Device& robot, const FrameIndex& baseFrame, } void exposeUtil() { - def("loadModel", &loadModel); - def("loadModelFromString", &loadModelFromString); + def("loadModel", &loadModel, + "Load a robot model from URDF/SRDF files into a manipulation Device, " + "also parsing grasp/contact data from the SRDF."); + def("loadModelFromString", &loadModelFromString, + "Load a robot model from URDF/SRDF XML strings into a manipulation " + "Device, also parsing grasp/contact data from the SRDF."); } } // namespace urdf } // namespace manipulation From 5df8fe155897aa2fe86f25ee81cd182fee145cd1 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Mon, 6 Jul 2026 13:12:38 +0200 Subject: [PATCH 04/18] doc: update README with doc status files and const char* docstring pattern Veuillez saisir le message de validation pour vos modifications. Les lignes --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 08382919..ed74758d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ hpp-python/ ├── include/pyhpp/ # C++ headers shared by the binding translation units ├── doc/ │ ├── configure.py # Injects Doxygen-extracted docstrings into the .cc binding sources -│ └── doxygen_xml_parser.py # Parses Doxygen XML to feed configure.py +│ ├── doxygen_xml_parser.py # Parses Doxygen XML to feed configure.py +│ ├── core_doc_todo.md # Docstring coverage status for pyhpp.core bindings +│ ├── constraints_doc_todo.md # Docstring coverage status for pyhpp.constraints bindings +│ └── manipulation_doc_todo.md # Docstring coverage status for pyhpp.manipulation bindings ├── src/pyhpp/ │ ├── __init__.py # Top-level package init (imports eigenpy, silences converter warnings) │ ├── pinocchio/ # hpp-pinocchio bindings: Device, Gripper, Lie-group utilities @@ -258,6 +261,20 @@ Doxygen comments in the upstream C++ libraries can be injected into the Python d See the header comment of `doc/configure.py` for the full syntax and a worked example. +For binding methods that have no upstream `///` Doxygen comment (Python-only wrappers, modified signatures that hide output parameters, etc.), docstrings are written directly in the `.cc` binding source using `const char*` constants in an anonymous namespace placed before `namespace pyhpp {`: + +```cpp +namespace { +const char* DOC_MY_METHOD = "Describe what the method does from the Python caller's perspective."; +} // namespace + +namespace pyhpp { + // ... + .def("myMethod", &MyClass::myMethod, DOC_MY_METHOD) +``` + +The documentation coverage for each module is tracked in `doc/core_doc_todo.md`, `doc/constraints_doc_todo.md` and `doc/manipulation_doc_todo.md`, using a `✅ / ❌ / ⚠️` legend per method. + > **TODO** (tracked in the source): use Doxygen to generate XML documentation of the headers included by a given binding file, then use that generated XML to auto-update the documentation comments in `src/pyhpp` (Doxygen's `INCLUDE_PATH` / `SEARCH_INCLUDES` options may help here). ## License From 2b835affcd9a89ea99600d575d29ff92d96212ca Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 8 Jul 2026 14:58:23 +0200 Subject: [PATCH 05/18] fix xml parser and bidings to getclass documentations --- doc/configure.py | 34 ++++++++++++++++--- doc/doxygen_xml_parser.py | 6 +++- src/pyhpp/constraints/by-substitution.cc | 2 +- .../constraints/differentiable-function.cc | 2 +- .../constraints/explicit-constraint-set.cc | 2 +- src/pyhpp/constraints/explicit.cc | 3 +- src/pyhpp/constraints/implicit.cc | 3 +- src/pyhpp/constraints/iterative-solver.cc | 2 +- src/pyhpp/constraints/locked-joint.cc | 3 +- src/pyhpp/constraints/relative-com.cc | 2 +- src/pyhpp/core/config-validation.cc | 4 +-- src/pyhpp/core/configuration-shooter.cc | 2 +- src/pyhpp/core/connected-component.cc | 2 +- src/pyhpp/core/constraint.cc | 7 ++-- src/pyhpp/core/distance.cc | 5 +-- src/pyhpp/core/node.cc | 3 +- src/pyhpp/core/parameter.cc | 2 +- src/pyhpp/core/path-optimizer.cc | 4 +-- src/pyhpp/core/path-planner.cc | 2 +- src/pyhpp/core/path-projector.cc | 4 +-- src/pyhpp/core/path-validation.cc | 2 +- src/pyhpp/core/path.cc | 3 +- src/pyhpp/core/problem-target.cc | 4 +-- src/pyhpp/core/problem.cc | 2 +- src/pyhpp/core/reports.cc | 13 ++++--- src/pyhpp/core/roadmap.cc | 3 +- src/pyhpp/core/steering-method.cc | 2 +- src/pyhpp/manipulation/device.cc | 5 +-- src/pyhpp/manipulation/graph.cc | 6 ++-- src/pyhpp/manipulation/path-planner.cc | 5 +-- src/pyhpp/manipulation/problem.cc | 2 +- .../manipulation/steering_method/cartesian.cc | 3 +- src/pyhpp/pinocchio/device.cc | 6 ++-- src/pyhpp/pinocchio/liegroup.cc | 8 ++--- 34 files changed, 101 insertions(+), 57 deletions(-) diff --git a/doc/configure.py b/doc/configure.py index c7da0855..43a4756c 100755 --- a/doc/configure.py +++ b/doc/configure.py @@ -138,14 +138,16 @@ def indexFromNamespace(ns): def escape(s): - # return s.replace ('\n', r'\n') - return s.replace("\n", r"\n") + s = s.replace("\\", "\\\\") # backslash → \\ + s = s.replace('"', '\\"') # double-quote → \" + s = s.replace("\n", "\\n") # newline → \n + return s def make_doc_string(brief, detailled): if len(brief) == 0 or brief.isspace(): return '"' + detailled + '"' - return '"' + brief + "\n" + detailled + '"' + return '"' + brief + "\\n" + detailled + '"' def make_args_string(args): @@ -155,16 +157,40 @@ def make_args_string(args): def substitute(istr, ostr): nsPattern = re.compile(r"DocNamespace\s*\(\s*(?P[\w:]+)\s*\)") classPattern = re.compile(r"DocClass\s*\(\s*(?P[\w:]+)\s*\)") + dcdPattern = re.compile(r"DocClassDoc\s*\(\s*(?P[\w:]+\s*)?\)") dcmPattern = re.compile( r"DocClassMethod\s*\(\s*(?P[\w:]+)\s*(,\s*(?P[\w:]+)\s*)?\)" ) classDoc = None + currentClass = None + currentNamespace = None + index = None for line in map(lambda s: s.rstrip(), istr): for match in nsPattern.finditer(line): currentNamespace = match.group("namespace") index = indexFromNamespace(currentNamespace) for match in classPattern.finditer(line): currentClass = match.group("class") + for match in dcdPattern.finditer(line): + cn_arg = match.group("class") + cn = ( + currentNamespace + + "::" + + (cn_arg.strip() if cn_arg and cn_arg.strip() else currentClass) + ) + try: + if classDoc is None or classDoc.classname != cn: + classDoc = index.classDoc(cn) + b, d = classDoc.getClassDoc() + line = line.replace( + match.group(0), make_doc_string(escape(b), escape(d)) + ) + except Exception as e: + print( + "Failed to find class doc for {0}: {1}".format(cn, e), + file=sys.stderr, + ) + line = line.replace(match.group(0), '"' + cn + '"') for match in dcmPattern.finditer(line): if match.group("class") is not None: cn = currentNamespace + "::" + match.group("class") @@ -177,7 +203,7 @@ def substitute(istr, ostr): b, d, args = classDoc.getClassMethodDoc(mn) line = line.replace( match.group(0), - escape(make_doc_string(b, d)) + make_doc_string(escape(b), escape(d)) + ", " + ((make_args_string(args)) if len(args) > 0 else '""'), ) diff --git a/doc/doxygen_xml_parser.py b/doc/doxygen_xml_parser.py index c4aa0400..26812e70 100644 --- a/doc/doxygen_xml_parser.py +++ b/doc/doxygen_xml_parser.py @@ -124,7 +124,11 @@ def _getMember(self, sectionKind, memberDefKind, name): raise IndexError(msg) def getClassDoc(self): - return self._getDoc(self.compound) + b_el = self.compound.find("briefdescription") + d_el = self.compound.find("detaileddescription") + brief = etree.tostring(b_el, method="text", encoding="unicode").strip() + detailed = etree.tostring(d_el, method="text", encoding="unicode").strip() + return brief, detailed def getClassMemberDoc(self, membername): # member = self.compound.find ("sectiondef[@kind='public-attrib']/memberdef[@kind='variable' and name='" + methodname + "']") diff --git a/src/pyhpp/constraints/by-substitution.cc b/src/pyhpp/constraints/by-substitution.cc index 03ff5595..10b0153e 100644 --- a/src/pyhpp/constraints/by-substitution.cc +++ b/src/pyhpp/constraints/by-substitution.cc @@ -123,7 +123,7 @@ void exposeBySubstitution() { // DocClass(BySubstitution) class_ >( - "BySubstitution", init()) + "BySubstitution", DocClassDoc(), init()) .def("explicitConstraintSetHasChanged", &BySubstitution::explicitConstraintSetHasChanged, DocClassMethod(explicitConstraintSetHasChanged)) diff --git a/src/pyhpp/constraints/differentiable-function.cc b/src/pyhpp/constraints/differentiable-function.cc index 83316179..0c32e02a 100644 --- a/src/pyhpp/constraints/differentiable-function.cc +++ b/src/pyhpp/constraints/differentiable-function.cc @@ -114,7 +114,7 @@ void exposeDifferentiableFunction() { // class_ class_( - "DifferentiableFunction", no_init) + "DifferentiableFunction", DocClassDoc(), no_init) // Pythonic API .def("__str__", &to_str) .def("__call__", &DFWrapper::py_value, DOC_DF_CALL) diff --git a/src/pyhpp/constraints/explicit-constraint-set.cc b/src/pyhpp/constraints/explicit-constraint-set.cc index 2289d91d..053317b3 100644 --- a/src/pyhpp/constraints/explicit-constraint-set.cc +++ b/src/pyhpp/constraints/explicit-constraint-set.cc @@ -42,7 +42,7 @@ using namespace hpp::constraints; void exposeExplicitConstraintSet() { // DocClass(ExplicitConstraintSet) - class_("ExplicitConstraintSet", + class_("ExplicitConstraintSet", DocClassDoc(), init()) .def("__str__", &to_str) .def("add", &ExplicitConstraintSet::add, DocClassMethod(add)) diff --git a/src/pyhpp/constraints/explicit.cc b/src/pyhpp/constraints/explicit.cc index 52cd248f..ba8a1c37 100644 --- a/src/pyhpp/constraints/explicit.cc +++ b/src/pyhpp/constraints/explicit.cc @@ -64,7 +64,8 @@ ExplicitPtr_t createExplicit(const LiegroupSpacePtr_t& configSpace, void exposeExplicit() { // DocClass(Explicit) - class_("Explicit", no_init) + class_("Explicit", DocClassDoc(), + no_init) .def("__init__", make_constructor(&createExplicit), "Create an explicit constraint mapping output DOF from input DOF."); } diff --git a/src/pyhpp/constraints/implicit.cc b/src/pyhpp/constraints/implicit.cc index fd8ed018..91a39744 100644 --- a/src/pyhpp/constraints/implicit.cc +++ b/src/pyhpp/constraints/implicit.cc @@ -61,7 +61,8 @@ void exposeImplicit() { .value("Superior", Superior) .value("Inferior", Inferior); // DocClass(Implicit) - class_("Implicit", no_init) + class_("Implicit", DocClassDoc(), + no_init) .def("__init__", make_constructor(&Implicit::create), "Create an implicit constraint from a differentiable function and " "comparison types.") diff --git a/src/pyhpp/constraints/iterative-solver.cc b/src/pyhpp/constraints/iterative-solver.cc index b1e0a7b7..ee7cca81 100644 --- a/src/pyhpp/constraints/iterative-solver.cc +++ b/src/pyhpp/constraints/iterative-solver.cc @@ -89,7 +89,7 @@ void exposeHierarchicalIterativeSolver() { .def(vector_indexing_suite()); // DocClass(solver::HierarchicalIterative) - class_("HierarchicalIterative", + class_("HierarchicalIterative", DocClassDoc(), init()) .def("__str__", &to_str) .def("add", &HierarchicalIterative::add, DocClassMethod(add)) diff --git a/src/pyhpp/constraints/locked-joint.cc b/src/pyhpp/constraints/locked-joint.cc index fe586491..ed5f840f 100644 --- a/src/pyhpp/constraints/locked-joint.cc +++ b/src/pyhpp/constraints/locked-joint.cc @@ -64,8 +64,9 @@ LockedJointPtr_t createLockedJointWithComp(const DevicePtr_t& robot, } void exposeLockedJoint() { + // DocClass(LockedJoint) class_, LockedJointPtr_t, boost::noncopyable>( - "LockedJoint", no_init) + "LockedJoint", DocClassDoc(), no_init) .def("__init__", make_constructor(&createLockedJoint), "Create a locked joint constraint fixing the named joint to the " "given configuration.") diff --git a/src/pyhpp/constraints/relative-com.cc b/src/pyhpp/constraints/relative-com.cc index 7288d0f4..b069aac1 100644 --- a/src/pyhpp/constraints/relative-com.cc +++ b/src/pyhpp/constraints/relative-com.cc @@ -69,7 +69,7 @@ static RelativeComPtr_t create3(const std::string& name, void exposeRelativeCom() { // DocClass(RelativeCom) class_, - boost::noncopyable>("RelativeCom", no_init) + boost::noncopyable>("RelativeCom", DocClassDoc(), no_init) .def("__init__", &create1, DocClassMethod(create)) .def("__init__", &create2) .def("__init__", &create3); diff --git a/src/pyhpp/core/config-validation.cc b/src/pyhpp/core/config-validation.cc index f408807b..3411a43e 100644 --- a/src/pyhpp/core/config-validation.cc +++ b/src/pyhpp/core/config-validation.cc @@ -58,7 +58,7 @@ struct CVWrapper { void exposeConfigValidation() { // DocClass (ConfigValidation) class_( - "ConfigValidation", no_init) + "ConfigValidation", DocClassDoc(), no_init) .PYHPP_DEFINE_METHOD2(ConfigValidation, validate, DocClassMethod(validate)) .def("validate", &CVWrapper::py_validate, @@ -66,7 +66,7 @@ void exposeConfigValidation() { // DocClass (ConfigValidations) class_, - boost::noncopyable>("ConfigValidations", no_init) + boost::noncopyable>("ConfigValidations", DocClassDoc(), no_init) .PYHPP_DEFINE_METHOD2(ConfigValidations, add, DocClassMethod(add)) .PYHPP_DEFINE_METHOD2(ConfigValidations, numberConfigValidations, DocClassMethod(numberConfigValidations)) diff --git a/src/pyhpp/core/configuration-shooter.cc b/src/pyhpp/core/configuration-shooter.cc index a6807200..13998bc0 100644 --- a/src/pyhpp/core/configuration-shooter.cc +++ b/src/pyhpp/core/configuration-shooter.cc @@ -47,7 +47,7 @@ struct CSWrapper { void exposeConfigurationShooter() { // DocClass(ConfigurationShooter) class_( - "ConfigurationShooter", no_init) + "ConfigurationShooter", DocClassDoc(), no_init) .def("shoot", &CSWrapper::shoot, DocClassMethod(shoot)); } } // namespace core diff --git a/src/pyhpp/core/connected-component.cc b/src/pyhpp/core/connected-component.cc index 06d41c15..2e1778c9 100644 --- a/src/pyhpp/core/connected-component.cc +++ b/src/pyhpp/core/connected-component.cc @@ -77,7 +77,7 @@ struct CCWrapper { void exposeConnectedComponent() { // DocClass(ConnectedComponent) class_( - "ConnectedComponent", no_init) + "ConnectedComponent", DocClassDoc(), no_init) .def("nodes", &CCWrapper::nodes, DocClassMethod(nodes)) .def("reachableFrom", &CCWrapper::reachableFrom, DocClassMethod(reachableFrom)) diff --git a/src/pyhpp/core/constraint.cc b/src/pyhpp/core/constraint.cc index 3f0e6a39..a68e0f69 100644 --- a/src/pyhpp/core/constraint.cc +++ b/src/pyhpp/core/constraint.cc @@ -121,7 +121,8 @@ static boost::python::list getNumConstraints(ConfigProjector& cp) { void exposeConstraint() { // DocClass(Constraint) - class_("Constraint", no_init) + class_( + "Constraint", DocClassDoc(), no_init) .def("__str__", &to_str_from_operator) .def("name", &Constraint::name, return_internal_reference<>(), DocClassMethod(name)) @@ -132,7 +133,7 @@ void exposeConstraint() { // DocClass(ConstraintSet) class_ >("ConstraintSet", no_init) + bases >("ConstraintSet", DocClassDoc(), no_init) .def("__init__", make_constructor(&createConstraintSet)) .def("addConstraint", &ConstraintSet::addConstraint, DocClassMethod(addConstraint)) @@ -146,7 +147,7 @@ void exposeConstraint() { .value("Constant", ConfigProjector::Constant); // DocClass(ConfigProjector) class_ >("ConfigProjector", no_init) + bases >("ConfigProjector", DocClassDoc(), no_init) .def("__init__", make_constructor(&createConfigProjector)) .def("solver", static_cast( diff --git a/src/pyhpp/core/distance.cc b/src/pyhpp/core/distance.cc index 1cf278c1..b5c33c0b 100644 --- a/src/pyhpp/core/distance.cc +++ b/src/pyhpp/core/distance.cc @@ -67,12 +67,13 @@ struct DistanceWrapper { void exposeDistance() { // DocClass(Distance) - class_("Distance", no_init) + class_("Distance", DocClassDoc(), + no_init) .def("compute", &DistanceWrapper::compute, DocClassMethod(compute)); // DocClass(WeighedDistance) class_, WeighedDistancePtr_t, - boost::noncopyable>("WeighedDistance", no_init) + boost::noncopyable>("WeighedDistance", DocClassDoc(), no_init) .def("__init__", make_constructor(&WeighedDistance::create)) .def("asDistancePtr_t", &DistanceWrapper::AsDistancePtr_t, DOC_WD_ASDISTANCE) diff --git a/src/pyhpp/core/node.cc b/src/pyhpp/core/node.cc index efc669c0..7b067ce3 100644 --- a/src/pyhpp/core/node.cc +++ b/src/pyhpp/core/node.cc @@ -45,7 +45,8 @@ using namespace hpp::core; void exposeNode() { // DocClass(Node) - class_, boost::noncopyable>("Node", no_init) + class_, boost::noncopyable>( + "Node", DocClassDoc(), no_init) .def(init()) .def(init()) .def("addOutEdge", &Node::addOutEdge, DocClassMethod(addOutEdge)) diff --git a/src/pyhpp/core/parameter.cc b/src/pyhpp/core/parameter.cc index a8a38662..b0120bde 100644 --- a/src/pyhpp/core/parameter.cc +++ b/src/pyhpp/core/parameter.cc @@ -97,7 +97,7 @@ Parameter createBool(bool param) { return Parameter(param); } void exposeParameter() { // DocClass(Parameter) - class_("Parameter", no_init) + class_("Parameter", DocClassDoc(), no_init) .def("__init__", &create) .def("create_bool", &createBool, DOC_PAR_CREATEBOOL) .staticmethod("create_bool") diff --git a/src/pyhpp/core/path-optimizer.cc b/src/pyhpp/core/path-optimizer.cc index 18ab512a..47d9a455 100644 --- a/src/pyhpp/core/path-optimizer.cc +++ b/src/pyhpp/core/path-optimizer.cc @@ -118,8 +118,8 @@ void exposeSplineGradientBased(const char* name) { void exposePathOptimizer() { // DocClass(PathOptimizer) - class_("PathOptimizer", - no_init) + class_( + "PathOptimizer", DocClassDoc(), no_init) .def("problem", &PathOptimizer::problem, DocClassMethod(problem)) .def("optimize", &PathOptimizer::optimize, DocClassMethod(optimize)) .def("interrupt", &PathOptimizer::interrupt, DocClassMethod(interrupt)) diff --git a/src/pyhpp/core/path-planner.cc b/src/pyhpp/core/path-planner.cc index b764933c..32526847 100644 --- a/src/pyhpp/core/path-planner.cc +++ b/src/pyhpp/core/path-planner.cc @@ -152,7 +152,7 @@ PathVectorPtr_t PathPlanner::computePath() const { return obj->computePath(); } void exposePathPlanner() { // DocClass(PathPlanner) - class_("PathPlanner", no_init) + class_("PathPlanner", DocClassDoc(), no_init) .def("roadmap", &PathPlanner::roadmap, return_value_policy(), DocClassMethod(roadmap)) .def("problem", &PathPlanner::problem, DocClassMethod(problem)) diff --git a/src/pyhpp/core/path-projector.cc b/src/pyhpp/core/path-projector.cc index aab4b276..60abd479 100644 --- a/src/pyhpp/core/path-projector.cc +++ b/src/pyhpp/core/path-projector.cc @@ -59,8 +59,8 @@ struct PPWrapper { void exposePathProjector() { // DocClass(PathProjector) - class_("PathProjector", - no_init) + class_( + "PathProjector", DocClassDoc(), no_init) .def("apply", &PPWrapper::apply, DocClassMethod(apply)) .def("apply", &PPWrapper::py_apply, "Apply projection to path; returns (success, projectedPath)."); diff --git a/src/pyhpp/core/path-validation.cc b/src/pyhpp/core/path-validation.cc index b2e5dc51..b3cb7a77 100644 --- a/src/pyhpp/core/path-validation.cc +++ b/src/pyhpp/core/path-validation.cc @@ -83,7 +83,7 @@ struct PVWrapper { void exposePathValidation() { // DocClass(PathValidation) class_( - "PathValidation", no_init) + "PathValidation", DocClassDoc(), no_init) .def("validate", &PVWrapper::validate, DocClassMethod(validate)) .def("validate", &PVWrapper::py_validate, "Validate path; returns (valid, validPart, report).") diff --git a/src/pyhpp/core/path.cc b/src/pyhpp/core/path.cc index 5c7c49d0..b2e4969e 100644 --- a/src/pyhpp/core/path.cc +++ b/src/pyhpp/core/path.cc @@ -155,7 +155,8 @@ struct PathWrap : PathWrapper, wrapper { void exposePath() { // DocClass(Path) - class_, boost::noncopyable>("Path", no_init) + class_, boost::noncopyable>("Path", DocClassDoc(), + no_init) .def("__str__", &to_str_from_operator) .def("__call__", &PathWrap::py_call1, DOC_PATH_CALL1) diff --git a/src/pyhpp/core/problem-target.cc b/src/pyhpp/core/problem-target.cc index 8256081a..a3a6eb74 100644 --- a/src/pyhpp/core/problem-target.cc +++ b/src/pyhpp/core/problem-target.cc @@ -42,8 +42,8 @@ using namespace hpp::core; void exposeProblemTarget() { // DocClass(ProblemTarget) - class_("ProblemTarget", - no_init) + class_( + "ProblemTarget", DocClassDoc(), no_init) ; } diff --git a/src/pyhpp/core/problem.cc b/src/pyhpp/core/problem.cc index 3b6c3db3..474b4aaa 100644 --- a/src/pyhpp/core/problem.cc +++ b/src/pyhpp/core/problem.cc @@ -661,7 +661,7 @@ void exposeProblem() { class_("CppCoreProblem", no_init); // DocClass(Problem) - class_("Problem", init()) + class_("Problem", DocClassDoc(), init()) .def("robot", &Problem::robot, return_value_policy(), DocClassMethod(robot)) .def("setParameter", &Problem::setParameter, DocClassMethod(setParameter)) diff --git a/src/pyhpp/core/reports.cc b/src/pyhpp/core/reports.cc index b12e1a42..b6d0e8f8 100644 --- a/src/pyhpp/core/reports.cc +++ b/src/pyhpp/core/reports.cc @@ -48,19 +48,21 @@ using namespace hpp::core; void exposeReports() { // DocClass(ValidationReport) class_( - "ValidationReport", no_init) + "ValidationReport", DocClassDoc(), no_init) .def("__str__", &to_str); // DocClass(CollisionValidationReport) class_ >("CollisionValidationReport", no_init) + bases >("CollisionValidationReport", DocClassDoc(), + no_init) .def_readonly("object1", &CollisionValidationReport::object1) .def_readonly("object2", &CollisionValidationReport::object2) .def_readonly("result", &CollisionValidationReport::result); // DocClass(JointBoundValidationReport) class_ >("JointBoundValidationReport", no_init) + bases >("JointBoundValidationReport", DocClassDoc(), + no_init) .def_readonly("joint_", &JointBoundValidationReport::joint_) .def_readonly("rank_", &JointBoundValidationReport::rank_) .def_readonly("lowerBound_", &JointBoundValidationReport::lowerBound_) @@ -69,7 +71,8 @@ void exposeReports() { // DocClass(PathValidationReport) class_ >("PathValidationReport", no_init) + bases >("PathValidationReport", DocClassDoc(), + no_init) .def_readwrite("parameter", &PathValidationReport::parameter) .def_readwrite("configurationReport", &PathValidationReport::configurationReport); @@ -77,7 +80,7 @@ void exposeReports() { // DocClass(CollisionPathValidationReport) class_ >("CollisionPathValidationReport", - no_init) + DocClassDoc(), no_init) .def("__str__", &to_str); } } // namespace core diff --git a/src/pyhpp/core/roadmap.cc b/src/pyhpp/core/roadmap.cc index 44efb8e7..25032c19 100644 --- a/src/pyhpp/core/roadmap.cc +++ b/src/pyhpp/core/roadmap.cc @@ -260,7 +260,8 @@ struct RWrapper { void exposeRoadmap() { // DocClass(Roadmap) - class_("Roadmap", no_init) + class_("Roadmap", DocClassDoc(), + no_init) .def("__init__", make_constructor(&Roadmap::create)) .def("__str__", &to_str) .def("clear", &Roadmap::clear, DocClassMethod(clear)) diff --git a/src/pyhpp/core/steering-method.cc b/src/pyhpp/core/steering-method.cc index a36965f4..f8f33424 100644 --- a/src/pyhpp/core/steering-method.cc +++ b/src/pyhpp/core/steering-method.cc @@ -143,7 +143,7 @@ const ConstraintSetPtr_t& SteeringMethod::constraints() const { void exposeSteeringMethod() { register_ptr_to_python>(); // DocClass(SteeringMethod) - class_("SteeringMethod", no_init) + class_("SteeringMethod", DocClassDoc(), no_init) .def("__call__", &SteeringMethod::operator(), "Compute a path between two configurations using the steering " "method.") diff --git a/src/pyhpp/manipulation/device.cc b/src/pyhpp/manipulation/device.cc index 70aead02..e66d37b6 100644 --- a/src/pyhpp/manipulation/device.cc +++ b/src/pyhpp/manipulation/device.cc @@ -295,7 +295,7 @@ static JointIndex getParentJointId(const HandlePtr_t& handle) { void exposeHandle() { // DocClass(Handle) - class_("Handle", no_init) + class_("Handle", DocClassDoc(), no_init) .add_property("name", &getHandleName, &setHandleName, DOC_HANDLE_NAME) .add_property("localPosition", &getHandleLocalPosition, &setHandleLocalPosition, DOC_HANDLE_LOCALPOSITION) @@ -343,7 +343,8 @@ void exposeDevice() { // DocClass(Device) class_, boost::shared_ptr, - boost::noncopyable>("Device", init()) + boost::noncopyable>("Device", DocClassDoc(), + init()) .def("setRobotRootPosition", &Device::setRobotRootPosition, "Set the root position of a sub-robot (identified by name) relative " "to its parent joint.") diff --git a/src/pyhpp/manipulation/graph.cc b/src/pyhpp/manipulation/graph.cc index 614e345f..37ad8cfe 100644 --- a/src/pyhpp/manipulation/graph.cc +++ b/src/pyhpp/manipulation/graph.cc @@ -1290,14 +1290,14 @@ using namespace boost::python; void exposeGraph() { // DocClass(State) - class_("State", no_init) + class_("State", DocClassDoc(), no_init) .def("name", &PyWState::name, DocClassMethod(name)) .def("id", &PyWState::id, DocClassMethod(id)) .def("configConstraint", &PyWState::configConstraint) .PYHPP_DEFINE_METHOD1(PyWState, neighborEdges, DOC_NEIGHBOREDGES); // DocClass(Edge) - class_("Transition", no_init) + class_("Transition", DocClassDoc(), no_init) .def("id", &PyWEdge::id, DocClassMethod(id)) .def("name", &PyWEdge::name, DocClassMethod(name)) .def("isWaypointTransition", &PyWEdge::isWaypointTransition, @@ -1310,7 +1310,7 @@ void exposeGraph() { // DocClass(Graph) class_( - "Graph", + "Graph", DocClassDoc(), init()) .def( diff --git a/src/pyhpp/manipulation/path-planner.cc b/src/pyhpp/manipulation/path-planner.cc index e7e8bcd8..d4c2d522 100644 --- a/src/pyhpp/manipulation/path-planner.cc +++ b/src/pyhpp/manipulation/path-planner.cc @@ -320,7 +320,8 @@ void exposePathPlanners() { // DocClass(TransitionPlanner) boost::python::class_>( - "TransitionPlanner", boost::python::init()) + "TransitionPlanner", DocClassDoc(), + boost::python::init()) .def("innerPlanner", static_cast( &TransitionPlanner::innerPlanner), @@ -371,7 +372,7 @@ void exposePathPlanners() { // DocClass(EndEffectorTrajectory) boost::python::class_>( - "EndEffectorTrajectory", + "EndEffectorTrajectory", DocClassDoc(), boost::python::init()) .def(boost::python::init()) diff --git a/src/pyhpp/manipulation/problem.cc b/src/pyhpp/manipulation/problem.cc index 5ec1f2d7..860bff6f 100644 --- a/src/pyhpp/manipulation/problem.cc +++ b/src/pyhpp/manipulation/problem.cc @@ -128,7 +128,7 @@ void Problem::graphSteeringMethod( void exposeProblem() { // DocClass(Problem) - class_>("Problem", + class_>("Problem", DocClassDoc(), init()) .def("constraintGraph", static_cast( diff --git a/src/pyhpp/manipulation/steering_method/cartesian.cc b/src/pyhpp/manipulation/steering_method/cartesian.cc index 2fe4e928..07887824 100644 --- a/src/pyhpp/manipulation/steering_method/cartesian.cc +++ b/src/pyhpp/manipulation/steering_method/cartesian.cc @@ -82,7 +82,8 @@ boost::python::tuple Cartesian::planPath(const Configuration_t& q_init) { void exposeCartesian() { // DocClass(Cartesian) boost::python::class_( - "Cartesian", boost::python::init()) + "Cartesian", DocClassDoc(), + boost::python::init()) .add_property("maxIterations", &Cartesian::getMaxIterations, &Cartesian::setMaxIterations, "Maximal number of iterations of numerical solver.") diff --git a/src/pyhpp/pinocchio/device.cc b/src/pyhpp/pinocchio/device.cc index 24797888..f0689a8f 100644 --- a/src/pyhpp/pinocchio/device.cc +++ b/src/pyhpp/pinocchio/device.cc @@ -226,7 +226,7 @@ static JointIndex getParentJointId(const GripperPtr_t& gripper) { void exposeGripper() { // DocClass(Gripper) - class_("Gripper", no_init) + class_("Gripper", DocClassDoc(), no_init) .add_property("localPosition", &getObjectPositionInJoint) .add_property( "clearance", @@ -313,8 +313,8 @@ void exposeDevice() { void (Device::*cfk)(int) = &Device::computeForwardKinematics; // DocClass(Device) - class_, boost::noncopyable>("Device", - no_init) + class_, boost::noncopyable>( + "Device", DocClassDoc(), no_init) .def("__init__", make_constructor(&createDevice)) .def("name", &Device::name, return_value_policy(), DocClassMethod(name)) diff --git a/src/pyhpp/pinocchio/liegroup.cc b/src/pyhpp/pinocchio/liegroup.cc index 1c973878..91af62f0 100644 --- a/src/pyhpp/pinocchio/liegroup.cc +++ b/src/pyhpp/pinocchio/liegroup.cc @@ -157,8 +157,8 @@ struct LgERWrapper { void exposeLiegroup() { // DocClass(LiegroupSpace) - class_("LiegroupSpace", - no_init) + class_( + "LiegroupSpace", DocClassDoc(), no_init) .def("__str__", &to_str_from_operator) .def("name", &LiegroupSpace::name, return_value_policy(), DocClassMethod(name)) @@ -193,7 +193,7 @@ void exposeLiegroup() { .def("__mul__", &LgSWrapper::times); // DocClass(LiegroupElement) - class_("LiegroupElement", + class_("LiegroupElement", DocClassDoc(), init()) .def(init()) // Pythonic API @@ -212,7 +212,7 @@ void exposeLiegroup() { .def(self + vector_t()); // DocClass(LiegroupElementRef) - class_("LiegroupElementRef", + class_("LiegroupElementRef", DocClassDoc(), init()) // Pythonic API .def("__str__", &to_str_from_operator) From b3351e857cc2b8901a51ad9c41f1acd17ae6d3a3 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 8 Jul 2026 17:17:14 +0200 Subject: [PATCH 06/18] fix(pyhpp): expose factory functions as real constructors to fix name collisions (path validation and path projector) --- src/pyhpp/core/path-projector.cc | 99 ++++++++++++++++--------------- src/pyhpp/core/path-validation.cc | 40 ++++++++----- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/pyhpp/core/path-projector.cc b/src/pyhpp/core/path-projector.cc index 60abd479..f350c6f0 100644 --- a/src/pyhpp/core/path-projector.cc +++ b/src/pyhpp/core/path-projector.cc @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -56,7 +57,6 @@ struct PPWrapper { return boost::python::make_tuple(res, projPath); } }; - void exposePathProjector() { // DocClass(PathProjector) class_( @@ -67,19 +67,64 @@ void exposePathProjector() { class_, hpp::core::pathProjector::ProgressivePtr_t, boost::noncopyable>( - "ProgressiveProjector", no_init); + "ProgressiveProjector", DocClassDoc(), no_init) + .def("__init__", + make_constructor( + +[](const DistancePtr_t& distance, + const PyWSteeringMethodPtr_t& steeringMethodWrapper, + const value_type& step) { + return pathProjector::Progressive::create( + distance, steeringMethodWrapper->obj, step); + }, + default_call_policies(), + (arg("distance"), arg("steeringMethod"), arg("step"))), + "Create a progressive path projector with the given step size."); class_, hpp::core::pathProjector::DichotomyPtr_t, boost::noncopyable>( - "DichotomyProjector", no_init); + "DichotomyProjector", DocClassDoc(), no_init) + .def("__init__", + make_constructor( + +[](const DistancePtr_t& distance, + const PyWSteeringMethodPtr_t& steeringMethodWrapper, + const value_type& step) { + return pathProjector::Dichotomy::create( + distance, steeringMethodWrapper->obj, step); + }, + default_call_policies(), + (arg("distance"), arg("steeringMethod"), arg("step"))), + "Create a dichotomy-based path projector with the given step size."); class_, hpp::core::pathProjector::GlobalPtr_t, boost::noncopyable>( - "GlobalProjector", no_init); + "GlobalProjector", DocClassDoc(), no_init) + .def("__init__", + make_constructor( + +[](const DistancePtr_t& distance, + const PyWSteeringMethodPtr_t& steeringMethodWrapper, + const value_type& step) { + return pathProjector::Global::create( + distance, steeringMethodWrapper->obj, step); + }, + default_call_policies(), + (arg("distance"), arg("steeringMethod"), arg("step"))), + "Create a global path projector with the given step size."); class_, hpp::core::pathProjector::RecursiveHermitePtr_t, boost::noncopyable>( - "RecursiveHermiteProjector", no_init); + "RecursiveHermiteProjector", DocClassDoc(), no_init) + .def("__init__", + make_constructor( + +[](const DistancePtr_t& distance, + const PyWSteeringMethodPtr_t& steeringMethodWrapper, + const value_type& step) { + return pathProjector::RecursiveHermite::create( + distance, steeringMethodWrapper->obj, step); + }, + default_call_policies(), + (arg("distance"), arg("steeringMethod"), arg("step"))), + "Create a recursive Hermite path projector with the given step " + "size."); def( "NoneProjector", @@ -89,50 +134,6 @@ void exposePathProjector() { }, (arg("distance"), arg("steeringMethod"), arg("step")), "Return a null path projector (no projection)."); - - def( - "ProgressiveProjector", - +[](const DistancePtr_t& distance, - const PyWSteeringMethodPtr_t& steeringMethodWrapper, - const value_type& step) { - return pathProjector::Progressive::create( - distance, steeringMethodWrapper->obj, step); - }, - (arg("distance"), arg("steeringMethod"), arg("step")), - "Create a progressive path projector with the given step size."); - - def( - "DichotomyProjector", - +[](const DistancePtr_t& distance, - const PyWSteeringMethodPtr_t& steeringMethodWrapper, - const value_type& step) { - return pathProjector::Dichotomy::create( - distance, steeringMethodWrapper->obj, step); - }, - (arg("distance"), arg("steeringMethod"), arg("step")), - "Create a dichotomy-based path projector with the given step size."); - - def( - "GlobalProjector", - +[](const DistancePtr_t& distance, - const PyWSteeringMethodPtr_t& steeringMethodWrapper, - const value_type& step) { - return pathProjector::Global::create(distance, - steeringMethodWrapper->obj, step); - }, - (arg("distance"), arg("steeringMethod"), arg("step")), - "Create a global path projector with the given step size."); - - def( - "RecursiveHermiteProjector", - +[](const DistancePtr_t& distance, - const PyWSteeringMethodPtr_t& steeringMethodWrapper, - const value_type& step) { - return pathProjector::RecursiveHermite::create( - distance, steeringMethodWrapper->obj, step); - }, - (arg("distance"), arg("steeringMethod"), arg("step")), - "Create a recursive Hermite path projector with the given step size."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path-validation.cc b/src/pyhpp/core/path-validation.cc index b3cb7a77..ab8f8647 100644 --- a/src/pyhpp/core/path-validation.cc +++ b/src/pyhpp/core/path-validation.cc @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,6 @@ #include #include #include - // DocNamespace(hpp::core) using namespace boost::python; @@ -79,7 +79,6 @@ struct PVWrapper { }); } }; - void exposePathValidation() { // DocClass(PathValidation) class_( @@ -92,19 +91,38 @@ void exposePathValidation() { class_, hpp::core::pathValidation::DiscretizedPtr_t, boost::noncopyable>( - "Discretized", no_init); + "Discretized", DocClassDoc(), no_init) + .def("__init__", + make_constructor( + +[](const DevicePtr_t& robot, const value_type& stepSize) { + return pathValidation::createDiscretizedCollisionChecking( + robot, stepSize); + }, + default_call_policies(), (arg("robot"), arg("stepSize"))), + "Create a discretized collision-checking path validation."); + hpp::core::continuousValidation::ProgressivePtr_t (*ProgressiveConstructor)( + const DevicePtr_t&, const value_type&) = + &continuousValidation::Progressive::create; class_, hpp::core::continuousValidation::ProgressivePtr_t, boost::noncopyable>( - "Progressive", no_init); + "Progressive", DocClassDoc(), no_init) + .def("__init__", + make_constructor(ProgressiveConstructor, default_call_policies(), + (arg("robot"), arg("tolerance"))), + "Create a progressive continuous path validation."); + hpp::core::continuousValidation::DichotomyPtr_t (*DichotomyConstructor)( + const DevicePtr_t&, const value_type&) = + &continuousValidation::Dichotomy::create; class_, hpp::core::continuousValidation::DichotomyPtr_t, boost::noncopyable>( - "Dichotomy", no_init); + "Dichotomy", DocClassDoc(), no_init) + .def("__init__", + make_constructor(DichotomyConstructor, default_call_policies(), + (arg("robot"), arg("tolerance"))), + "Create a dichotomy-based continuous path validation."); - def("Discretized", &pathValidation::createDiscretizedCollisionChecking, - (arg("robot"), arg("stepSize")), - "Create a discretized collision-checking path validation."); def("DiscretizedCollision", &pathValidation::createDiscretizedCollisionChecking, (arg("robot"), arg("stepSize")), @@ -117,12 +135,6 @@ void exposePathValidation() { (arg("robot"), arg("stepSize")), "Create a discretized path validation checking both collision and joint " "bounds."); - def("Progressive", &continuousValidation::Progressive::create, - (arg("robot"), arg("tolerance")), - "Create a progressive continuous path validation."); - def("Dichotomy", &continuousValidation::Dichotomy::create, - (arg("robot"), arg("tolerance")), - "Create a dichotomy-based continuous path validation."); } } // namespace core } // namespace pyhpp From a38e509f8913fb8c4ebeda9fec5ebb8d8cb56fd5 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 8 Jul 2026 17:42:16 +0200 Subject: [PATCH 07/18] using float instead of int for default margin of securtiyMargins manipulation --- src/pyhpp/manipulation/security_margins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyhpp/manipulation/security_margins.py b/src/pyhpp/manipulation/security_margins.py index 8f5227fb..cf687817 100644 --- a/src/pyhpp/manipulation/security_margins.py +++ b/src/pyhpp/manipulation/security_margins.py @@ -30,7 +30,7 @@ class SecurityMargins: - defaultMargin = 0 + defaultMargin = 0.0 separators = ["/"] def __init__(self, problem, factory, robotsAndObjects, robot): From d6272ac7ed221704070472e7b7e90a94ceda0092 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Thu, 9 Jul 2026 16:04:15 +0200 Subject: [PATCH 08/18] doc: add fix_stubs package to repair Boost.Python .pyi signatures from docstrings --- doc/fix_stubs.py | 16 + doc/fix_stubs/__init__.py | 13 + doc/fix_stubs/__main__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 667 bytes .../__pycache__/__main__.cpython-313.pyc | Bin 0 -> 286 bytes .../__pycache__/_cli.cpython-313.pyc | Bin 0 -> 2971 bytes .../__pycache__/_fixer.cpython-313.pyc | Bin 0 -> 1480 bytes .../__pycache__/_imports.cpython-313.pyc | Bin 0 -> 3492 bytes .../__pycache__/_models.cpython-313.pyc | Bin 0 -> 10707 bytes .../__pycache__/_overrides.cpython-313.pyc | Bin 0 -> 4720 bytes .../__pycache__/_parser.cpython-313.pyc | Bin 0 -> 14614 bytes .../__pycache__/_patterns.cpython-313.pyc | Bin 0 -> 1607 bytes .../__pycache__/_renderer.cpython-313.pyc | Bin 0 -> 1048 bytes .../__pycache__/_text_utils.cpython-313.pyc | Bin 0 -> 3430 bytes .../__pycache__/_type_utils.cpython-313.pyc | Bin 0 -> 1656 bytes doc/fix_stubs/_cli.py | 72 ++++ doc/fix_stubs/_fixer.py | 38 ++ doc/fix_stubs/_imports.py | 75 ++++ doc/fix_stubs/_models.py | 197 ++++++++++ doc/fix_stubs/_overrides.py | 107 ++++++ doc/fix_stubs/_parser.py | 348 ++++++++++++++++++ doc/fix_stubs/_patterns.py | 48 +++ doc/fix_stubs/_renderer.py | 22 ++ doc/fix_stubs/_text_utils.py | 46 +++ doc/fix_stubs/_type_utils.py | 39 ++ src/pyhpp/manipulation/device.cc | 1 + 26 files changed, 1027 insertions(+) create mode 100644 doc/fix_stubs.py create mode 100644 doc/fix_stubs/__init__.py create mode 100644 doc/fix_stubs/__main__.py create mode 100644 doc/fix_stubs/__pycache__/__init__.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/__main__.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_cli.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_imports.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_models.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_parser.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_patterns.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_text_utils.cpython-313.pyc create mode 100644 doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc create mode 100644 doc/fix_stubs/_cli.py create mode 100644 doc/fix_stubs/_fixer.py create mode 100644 doc/fix_stubs/_imports.py create mode 100644 doc/fix_stubs/_models.py create mode 100644 doc/fix_stubs/_overrides.py create mode 100644 doc/fix_stubs/_parser.py create mode 100644 doc/fix_stubs/_patterns.py create mode 100644 doc/fix_stubs/_renderer.py create mode 100644 doc/fix_stubs/_text_utils.py create mode 100644 doc/fix_stubs/_type_utils.py diff --git a/doc/fix_stubs.py b/doc/fix_stubs.py new file mode 100644 index 00000000..4cb231a6 --- /dev/null +++ b/doc/fix_stubs.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Backward-compatible entry point — logic lives in the fix_stubs/ package. + +Usage (unchanged): + python fix_stubs.py path/to/site-packages/pyhpp/ + python fix_stubs.py some/module/bindings.pyi another.pyi + python fix_stubs.py --setter-overrides overrides.json path/to/pyhpp/ + +Or via the package: + python -m fix_stubs ... +""" + +from fix_stubs._cli import main + +if __name__ == "__main__": + main() diff --git a/doc/fix_stubs/__init__.py b/doc/fix_stubs/__init__.py new file mode 100644 index 00000000..9d4201f1 --- /dev/null +++ b/doc/fix_stubs/__init__.py @@ -0,0 +1,13 @@ +"""fix_stubs — Fix Boost.Python .pyi stubs by restoring proper signatures from docstrings. + +Public API: + fix_file(path, setter_overrides=None) -> int + main() (CLI entry point) + SETTER_TYPE_OVERRIDES (built-in setter type table) +""" + +from ._cli import main +from ._fixer import fix_file +from ._overrides import SETTER_TYPE_OVERRIDES + +__all__ = ["fix_file", "main", "SETTER_TYPE_OVERRIDES"] diff --git a/doc/fix_stubs/__main__.py b/doc/fix_stubs/__main__.py new file mode 100644 index 00000000..0037d438 --- /dev/null +++ b/doc/fix_stubs/__main__.py @@ -0,0 +1,5 @@ +"""Allow `python -m fix_stubs` execution.""" + +from ._cli import main + +main() diff --git a/doc/fix_stubs/__pycache__/__init__.cpython-313.pyc b/doc/fix_stubs/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f50fc92f9aa8317634640bb44b4ac067f20ccff GIT binary patch literal 667 zcmah`&59F25bpWOBnkGXo|AA_$@j>W+{^vN00`PtDAvleElC58XYIx#>IT z6L|6!d;?!$U@z;*n-aWRJ?`Q~u@2Q=cXfSL_0{3%$R}u@Kc2n*ViEGyT&~Q%1c!SF zK9HD55?ir7v!=GR!C+_ROkL@A*q!yJp7el(B<{ViWPj@g{rWbNZup)mU8f2z-oA%t zd=F2AP)~w@^t@F$s!q%S6}s4jU;7Sc^<)^FfV$e^IA7 z&*1T5^1uf)Q)}9lEPEMovA7sO{=D8m6^0G6~O~pVbY0d)w8%jk2 zt94CJcn+?HL45tMRRTx4m?ItIdg$iCpzTwN5>eW^G|PDxYdsWYj~bZYt^Y@lF3Dsh zc~0pka=MOW1_b_4^Kr_7dW{JcU83%>h7HHLnSkF9aY^LLo6Daj=N2U zl=6~mN^!aE;9()wb#~=<2FhLogm4J+?48aUV48e@SOx}!MOhrrz3^7bf45m;e z3JmGYnk-dnjyXB`*jNyP~YsTHZor6rm9d3rAyf!Z_~ zZ?WViX6E^6GT&m4k59=@j*q{^5}%xtS;P#KUdiwoWcDpT{fzwFRQ;5K%)FAslJv|R zeV6>?(%jU%l4AXgf`Xv@Vk5uI3jN}uWDr*uVzWNbG<~?$`tk7~i{j(;3My}L*yQG? nl;)(`6>$QM2RWoz07!gbW@Kc%$)IzWLF^#|ODkIuJ5Ue+R##2L literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_cli.cpython-313.pyc b/doc/fix_stubs/__pycache__/_cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f78a806e43dcb28d8b61e4c268faedecaed93285 GIT binary patch literal 2971 zcmb6bO>Y~=b(Y*+Qv8r8iLz8zk;k-)ijGJkk=;~m8o;)wC=?_)T+v8PyX=a)q}EyP zvNOwwVh}*+NiGUlEgDomAZRW05FH&LKw2Pad!+0DQZ{lMAnhSH8A^eoz4XmeYcfLG zx(n>goA=(#`Pab6nO#Jea}W zIWO@#Gf#M!`8gpg5)o*EB+mJ=e&Tmv-&{{NKmyqy3BtNZ@}EYxc@nC4@ZB%gaD0l3T zCkOyyj!ULE1LaXp=%%z`$t&>%(Zw^jO!4?X@%p+rk>r3NP~$m(1m=DYn43K zZB^dii==^k4~)@C{%LG`?G@)le*Ptdf(UH=ZF}zW)KH#>^G@Xj7>6f?r#ZEP&Z5G3 z*8(4s&*URO#^TTPo?J>C=o{Ba$x=!=ol(5NpRI3=WRR=6C6SImHvqUYT85PQ^0Hkt>Xf;*_ zY@r4vfZE)%&6l0?SOK6Is)wQxwFGBsu(V88Oz?O&P6laIl}TwuRX}{HPN?>Vs;|Z4 zURy9^QlU0)t<_YUUs3g1b0Cpu*Nw!Ja?X0FX{_#Gb||jOwHh-AkC%1X!shaAP&SRNOk_spdF0{-VHp|#rQSowJ)2)PNU@5b-l*tvBesC>QT)dviLA!Ca z0V;rAST(?(;F?vA{ z=$kR3TFl_3w!yKQOynvBOra1nm@_7hX}272+4X6gT}&1j4TPpzVhM2H#QD?!8PZ z^(tKVHMS&n2!gW+R0l_RcI^R&lOZXAX8yM* zBDEB-#}+kOgxrF)Ni0>d1ya$?Wx_h2-IHmQR3}g3L0fbr5u0~pIbxUuTdue zCJrfGvjr!cZLVx{hRvyzDS|tnolUKnRW+qRD#5FuqUot=v*Z{?Q!BMv&ZN&TXpIyl zCALh|oVt^OVv=HS1*i0+iX~lx`?L9~tQq72NMNmzZbLI%M+f1dz3}-5;q&{^n~%Mo zUU7ZqP((s-k00LUhxh$`oAjONe)#&KfP|69$Sa74{x0(9I~RANhrB2JBks`WaQJ{D z@^u`AuX2P2_JX6k!O@4ovGwV{4o}=$+Dly7Ot^Ry(qDNmp-`h-i-&5*+-F)e;o?I^iRs(2p{$aGD-fUWMJm3@1yUI z&5Zi~Fe(5Hi77%=sT0HI7feHqhizZ62tiOR+JR!RTxWfzSS0Y?LnO)u_Rg?PzGzEZO6VVi*p+9LIfv go_&O3pQFA{QQ#38`5PMj3!47BnB;!Qkx5|sH#AW4t^fc4 literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc b/doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f023e70d487a4fd81fae44972133823673a9312 GIT binary patch literal 1480 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 literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc b/doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..750c8556eee26c286ba426cbcea7a0f93bd74520 GIT binary patch literal 4720 zcmdT{OKcm*8J=A}MT(S6iI%MAM6xW=rkAZGH@5P&q|}O{N4aE>mR1%kawV<3++}8$ zj>W`)qC={STbLb(59s;C?0_kB@88xw0fdJ_x=*5nlLxUdr&n{mQ zoIcP~m*DKb|IGX|^FO}-pMy{+fZ&<`<^xvRJ3-95*31*T_FuafV^8N%{ zqpRGO1dNB?z@c%$ZyeXk`AL8eT@Lc$QCN#_gI_y;jF0dgd?)86t$Y;vj@x}*d^dlB zV@b&7_1L@tzPATmag#7V$oJV)8;|*Zn`&Qo#hwsDE)XjjhD5NjS&*=HO(I0jNqQXT zC1XR&VO?HVMWaX#^@(Z@uZp^avx=zeSTC;XhHMlKSyOc`5a4H1soA8E`u_5)u=L7o zGC4Oh%Lgz#7d1^cxaCa{t&Zwg%;kgv(FzhVHpgOEm0)?iE~yd`4GFqcEEWyzY-0~= z8f2Chg!5uS$3xTf z42xo3;%Wlu#}GdIENnp44A@G}mTzlFH;AmR$8i>pT2!D5zp){~J{w!ov8v&MNOXIR zoR)Q_fL<16QcC~{Ud zup;XQHN_Z_j5yAWfda(5pn@jjR2e{1^~K_tLDu{93*yVd?dvd`(m&)d1i90^$tC7%NycJ@x2C_;xRg#)jp+1PW86 z7;Cw%N}HAoENi(JsbjI02V@i_LP4R_31AvQqP!20JDaQiv@h9JlhAncu0;K$9q5%7olPK}h1snLY47uH8t`ez z{Z2Ko%r1lx3L=cufZCGwCQmdZ)u>nBo~-KvqlH;#{HtQxMYHxo!TF~4(^(9EwPg)F zU;GUictIlT5RP$b5XYQ;LN*SPxBRKN!QH|>_@^ri$_%@0%f%^^qwLsNJtDH$oi z{h>G>va^5=%uHs8h8E@7eJf5hk)y_>H3->tCHB>Zu+#}99CwI9b}_`Okh=gzq?`n>VJprqI#!FaVw{y#r^?_;15;p+0>BLv3!-BY2QA`w zT{E!d0$o{NM?Hkb)E6|AVASC&kKHlK-z&fVe(J}{ZhU(CMKd~6X>GqXe{;UvQ4am7_0&Tj zI^O@lkGgud^>=~~+fdigg9vJkY%To#%NBHemeHwsK0zJR=lqAV1Wl1HnkC%eK>t2V ztmXl)|6el%^SBI=Zo*ZHpOA@v>p81_MIi!a+y|n40O+Rf0 zps~EgLJsaUF&7!8ypxpY29BtM?;r?ba+#O)-~%z8}C}zXA=!oF9cF_adWv zp)ot=c70v@Z5_8tZ;-fufmKKTpx{kD?(**3W%JUC+5g&JL@-;0ia&fSbTf22chBEj2_D;8IDpmAA!nzKYdd=! z2yk{fznR+Zf?0$LJOU%g6VTA&p5A433cA&^cd9f?Dso8@XqRALbIAk@ zSckt(Ym+Us9~{0P9Ni6$?zs1Y6Q+BDd<*z7x2@K%gY?3H-@^2EV0i)Ii$o<9DXRm> zB+!JGS_BFP${nXP4Pl2%sn{{gCkTLyfZzc^SS!+txF8UELfh1qk7BnXuM(;bf}ycI z_T9>s1e5BuxsylQ1fl6>1EOrNv6Vs5lu+~92mJuq$8mhY|8!o<6&2}O@(lD-Md=h` z9=I5W`H~GY?ym+AGyFLU{snbE2!Kc29x1!Zsk5?*&J9f`xhO?=#%Ead9(jQrEmPhi)QbWX3zQ0 zaBOGMJoA(}__R6jOa-6X889y_m}86P;F3A8>@dd8n}bwxrGMhXq}g}g?7i?X8a%_A zsNIZ=9-tt@KIlW8xY9LT>AP4N97$-v9sr literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_parser.cpython-313.pyc b/doc/fix_stubs/__pycache__/_parser.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa312c589dde9420bed9bb0239a820d3deb49e53 GIT binary patch literal 14614 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 literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc b/doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0d05d517124e944e6910d4e3ee8f2b7a5cc71915 GIT binary patch literal 1048 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 literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc b/doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9619047fe448dc77db5be1cd2f053923d8e6640e GIT binary patch literal 1656 zcmb7^&1>976u?LOy;`pwlccl_q|;znt2kO{8Zd4N#ElPe8Yhmtv>^vEmS%S+NHg-x z$n}QM8hqK%LVO7rdg-Ny`~&?vT5{P6dMZ8SPzboa_l;yXags|1qHvzK`x# zWYueWwOpzV>W=+jy{B2kG@}alg~TyQ62=F3SqP=w+ao;`9P3d+vC$>w96X-2^c8%A@1WE}cZ}a_%PU_#I+-J{(mUbrReNX}dDTnhIpowx0}O9c zjVafX;z7!yh{jYaJP+OYn#d4K+NYB8fI3ims*7f9r3~{9RT?Km;gsV|@YIZB#o{EQ z3w9z>iRm`eRN}3aLbD_r40SgiB_pgM8pR>S5ryNGNJEIt;+TevXb8_Wo~JuYn2T6} zH^YI5BBr3b9QJIoO#_HY;lg-edTx%2!Ou8gl48!bDg4ADOd*#%V}(P)^Gr!KQ~)>spqZop+{6(ePqn_2fV>w;5~RcLWmlLmt1sg+q;nGWM@y*m^! z?S=pi-2Z?@-D@I9O$n+yOp-f7eZ0nYx=IG-^J0<%)eS|^?Xw+U>2yG9S$*q0`v!zJ!3$yP{x@`GfDRh;AEc&z zuPrm3myagcs~UqG$R%)F+0l|PZSW}W5EH;75z^3B;?cj@7@Y^gpc88~NGIDuHq$H!lFjv9Wh6HHf&q44Go;RceXo-NU zcFcK(5j>yg5s83A3;8gFog!7tX+V`COj74WatYDHml=9V#5i5P-B_lScC;Zj-vb|KZ+4qi#(-UVOB;|M|h}v(~wT ztG~9+|575+xV0=Hb zeZQZA8})r@{`Rt_4^09nxGE|%^Ur+C`XKlG7o@wtQvd(} literal 0 HcmV?d00001 diff --git a/doc/fix_stubs/_cli.py b/doc/fix_stubs/_cli.py new file mode 100644 index 00000000..658b0032 --- /dev/null +++ b/doc/fix_stubs/_cli.py @@ -0,0 +1,72 @@ +"""Command-line interface for fix_stubs.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from ._fixer import fix_file +from ._overrides import load_method_overrides, load_setter_overrides + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Fix Boost.Python .pyi stubs: restore proper signatures from docstrings" + ) + parser.add_argument( + "paths", + nargs="+", + type=Path, + help=".pyi files or directories to fix (directories are searched recursively)", + ) + parser.add_argument( + "--setter-overrides", + type=Path, + default=None, + help=( + "Optional JSON file with additional/overriding setter type mappings " + '(flat object: {"ClassName.propName": "type", ...}). ' + "Merged on top of the built-in SETTER_TYPE_OVERRIDES table (external entries win)." + ), + ) + parser.add_argument( + "--method-overrides", + type=Path, + default=None, + help=( + "Optional JSON file with additional/overriding method signature mappings " + '({"ClassName.method": {"ret": "Type", "params": [["name", "Type"], ...]}}). ' + "Merged on top of the built-in METHOD_OVERRIDES table (external entries win)." + ), + ) + args = parser.parse_args() + + setter_overrides = load_setter_overrides(args.setter_overrides) + method_overrides = load_method_overrides(args.method_overrides) + + total = 0 + total_files = 0 + for p in args.paths: + if not p.exists(): + print(f"warning: path not found, skipping: {p}", file=sys.stderr) + continue + files = sorted(p.rglob("*.pyi")) if p.is_dir() else [p] + for f in files: + try: + n = fix_file( + f, + setter_overrides=setter_overrides, + method_overrides=method_overrides, + ) + except Exception as e: + print(f"error: failed to fix {f}: {e}", file=sys.stderr) + continue + total_files += 1 + if n: + print(f"{f}: {n} signature(s) fixed", file=sys.stderr) + total += n + print( + f"Total: {total} signature(s) fixed across {total_files} file(s)", + file=sys.stderr, + ) diff --git a/doc/fix_stubs/_fixer.py b/doc/fix_stubs/_fixer.py new file mode 100644 index 00000000..fde98b5b --- /dev/null +++ b/doc/fix_stubs/_fixer.py @@ -0,0 +1,38 @@ +"""Orchestrate parsing, import merging, and rendering for a single .pyi file.""" + +from __future__ import annotations + +from pathlib import Path + +from ._imports import collect_required_imports, merge_imports +from ._parser import extract_tree +from ._renderer import render_file + + +def fix_file( + path: Path, + setter_overrides: dict[str, str] | None = None, + method_overrides: dict | None = None, +) -> int: + """Fix a single .pyi file in-place. Returns the number of signatures fixed.""" + header, tree = extract_tree( + path, + setter_overrides=setter_overrides, + method_overrides=method_overrides, + ) + + required_modules = collect_required_imports(tree) + header = merge_imports(header, required_modules) + + fixed = 0 + for cls in tree: + for fn in cls.functions: + if fn.from_docstring: + fixed += 1 + elif fn.setter_overloads: + fixed += 1 + + new_content = render_file(header, tree) + path.write_text(new_content) + + return fixed diff --git a/doc/fix_stubs/_imports.py b/doc/fix_stubs/_imports.py new file mode 100644 index 00000000..4f7f96e4 --- /dev/null +++ b/doc/fix_stubs/_imports.py @@ -0,0 +1,75 @@ +"""Collect required imports from the parsed tree and merge them into the file header.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from ._type_utils import type_module + +if TYPE_CHECKING: + from ._models import ClassI + +IMPORT_MODULE_RE = re.compile(r"^\s*import\s+([A-Za-z_][A-Za-z0-9_.]*)") +FROM_IMPORT_RE = re.compile(r"^\s*from\s+([A-Za-z_][A-Za-z0-9_.]*)\s+import") + + +def collect_required_imports(tree: list[ClassI]) -> list[str]: + """Parcourt tout l'arbre et renvoie la liste triee des modules a importer, + a partir des types utilises dans les signatures (params + retour, getter + setter).""" + modules: set[str] = set() + + def _scan(ov): + m = type_module(ov.ret) + if m: + modules.add(m) + for _, t in ov.params: + m = type_module(t) + if m: + modules.add(m) + + for cls in tree: + for fn in cls.functions: + for ov in fn.overloads: + _scan(ov) + for ov in fn.setter_overloads: + _scan(ov) + + return sorted(modules) + + +def merge_imports(header: str, required_modules: list[str]) -> str: + """Ajoute au header les `import X` manquants parmi required_modules, + sans dupliquer ceux deja presents (via `import X` ou `from X import ...`).""" + header_lines = header.splitlines() if header else [] + + already_covered: set[str] = set() + for line in header_lines: + m = IMPORT_MODULE_RE.match(line) + if m: + already_covered.add(m.group(1)) + continue + m = FROM_IMPORT_RE.match(line) + if m: + already_covered.add(m.group(1)) + + missing = [mod for mod in required_modules if mod not in already_covered] + if not missing: + return header + + new_import_lines = [f"import {mod}" for mod in missing] + + # inserer juste apres le dernier import existant, ou apres "from __future__", sinon en tete + insert_at = 0 + for idx, line in enumerate(header_lines): + if ( + IMPORT_MODULE_RE.match(line) + or FROM_IMPORT_RE.match(line) + or line.startswith("from __future__") + ): + insert_at = idx + 1 + + new_header_lines = ( + header_lines[:insert_at] + new_import_lines + header_lines[insert_at:] + ) + return "\n".join(new_header_lines) diff --git a/doc/fix_stubs/_models.py b/doc/fix_stubs/_models.py new file mode 100644 index 00000000..884b21dd --- /dev/null +++ b/doc/fix_stubs/_models.py @@ -0,0 +1,197 @@ +"""IR (Intermediate Representation) data models for a parsed .pyi file. + +Each model knows how to render itself to valid Python stub syntax via __str__. +""" + +from __future__ import annotations + +from ._text_utils import indent_block +from ._type_utils import quote_type + + +class Overload: + """A single signature variant (useful when a docstring contains several overloads).""" + + def __init__( + self, ret: str, params: list[tuple[str, str]], description: str, is_method: bool + ): + self.ret = ret + self.params = params # "self" already stripped when is_method=True + self.description = description + self.is_method = is_method + + +class Function: + def __init__(self): + self.name = "" + self.overloads: list[Overload] = [] + self.from_docstring = False + self.raw_blocks: list[list[str]] = [] + self.is_property = False + self.setter_overloads: list[Overload] = [] + self.setter_raw_blocks: list[list[str]] = [] + self.setter_from_override = False + self.in_class = True + + # -- backward-compat shims for single-block/single-overload access -- + @property + def raw_lines(self): + return self.raw_blocks[0] if self.raw_blocks else [] + + @raw_lines.setter + def raw_lines(self, value): + self.raw_blocks = [value] if value else [] + + @property + def type_func(self): + return self.overloads[0].ret if self.overloads else "" + + @property + def param(self): + return self.overloads[0].params if self.overloads else [] + + @property + def is_method(self): + return self.overloads[0].is_method if self.overloads else False + + @property + def docstring(self): + return self.overloads[0].description if self.overloads else "" + + def _render_one( + self, ov: Overload, overload_decorator: bool, decorator: str = "" + ) -> str: + param_names = ["self"] if ov.is_method else [] + param_names += [f"{n}: {quote_type(t)}" for n, t in ov.params] + params_str = ", ".join(param_names) + ret = quote_type(ov.ret) if ov.ret else "None" + prefix = "" + if overload_decorator: + prefix += "@typing.overload\n" + if decorator: + prefix += decorator + "\n" + elif not ov.is_method and self.in_class: + # @staticmethod n'a de sens qu'a l'interieur d'une classe; + # une fonction de niveau module ne doit jamais le recevoir + prefix += "@staticmethod\n" + header = f"{prefix}def {self.name}({params_str}) -> {ret}:" + body = f'"""\n{ov.description}\n"""' if ov.description else "..." + return header + "\n" + indent_block(body, 4) + + def __str__(self): + if not self.from_docstring: + all_blocks = list(self.raw_blocks) + # si un setter a ete rencontre sans docstring parsable, rendre ses lignes brutes + # (sauf si setter_overloads est deja rempli via SETTER_TYPE_OVERRIDES, pour eviter un doublon) + if not self.setter_overloads: + all_blocks += self.setter_raw_blocks + multi = len(all_blocks) > 1 + parts = [] + for block in all_blocks: + indents = [len(ln) - len(ln.lstrip()) for ln in block if ln.strip()] + min_indent = min(indents) if indents else 0 + dedented = [ + ln[min_indent:] if len(ln) >= min_indent else ln for ln in block + ] + already_has_overload = any( + ln.strip() == "@typing.overload" for ln in dedented + ) + is_property_pair = any( + ln.strip() == "@property" or ln.strip().endswith(".setter") + for ln in dedented + ) + if multi and not already_has_overload and not is_property_pair: + dedented = ["@typing.overload"] + dedented + parts.append("\n".join(dedented)) + rendered = "\n\n".join(parts) + + # getter sans docstring parsable mais setter type via override: + # on ajoute quand meme le setter type, sinon l'override serait silencieusement perdu + if self.setter_overloads: + multi_set = len(self.setter_overloads) > 1 + setter_parts = [ + self._render_one( + ov, + overload_decorator=multi_set, + decorator=f"@{self.name}.setter", + ) + for ov in self.setter_overloads + ] + rendered = rendered + "\n\n" + "\n\n".join(setter_parts) + return rendered + + if self.is_property: + parts = [] + multi_get = len(self.overloads) > 1 + for ov in self.overloads: + parts.append( + self._render_one( + ov, overload_decorator=multi_get, decorator="@property" + ) + ) + multi_set = len(self.setter_overloads) > 1 + for ov in self.setter_overloads: + parts.append( + self._render_one( + ov, + overload_decorator=multi_set, + decorator=f"@{self.name}.setter", + ) + ) + if not self.setter_overloads: + for block in self.setter_raw_blocks: + indents = [len(ln) - len(ln.lstrip()) for ln in block if ln.strip()] + min_indent = min(indents) if indents else 0 + dedented = [ + ln[min_indent:] if len(ln) >= min_indent else ln for ln in block + ] + parts.append("\n".join(dedented)) + return "\n\n".join(parts) + + multi = len(self.overloads) > 1 + return "\n\n".join( + self._render_one(ov, overload_decorator=multi) for ov in self.overloads + ) + + def __repr__(self): + return self.__str__() + + +class ClassI: + def __init__(self, is_module: bool = False): + self.name = "" + self.bases: list[str] = [] + self.docstring = "" + self.functions: list[Function] = [] + self.is_module = is_module + self.extra_lines: list[str] = [] + + def __str__(self): + if self.is_module: + parts = [] + for fn in self.functions: + parts.append(str(fn)) + parts.append("") + return "\n".join(parts) + + bases_str = f"({', '.join(self.bases)})" if self.bases else "" + lines = [f"class {self.name}{bases_str}:"] + if self.docstring: + lines.append(indent_block(f'"""\n{self.docstring}\n"""', 4)) + + # __slots__ = () ferme la classe a toute assignation d'attribut arbitraire tout en + # heritant correctement des slots declares par les classes de base. On ne liste jamais + # les @property dans __slots__: avoir les deux pour le meme nom est un conflit Python + # (ValueError a la definition) et cause des faux positifs Pyright. + lines.append(indent_block("__slots__ = ()", 4)) + + for extra in self.extra_lines: + lines.append(indent_block(extra, 4)) + + for fn in self.functions: + lines.append(indent_block(str(fn), 4)) + lines.append("") + return "\n".join(lines) + + def __repr__(self): + return self.__str__() diff --git a/doc/fix_stubs/_overrides.py b/doc/fix_stubs/_overrides.py new file mode 100644 index 00000000..e738af31 --- /dev/null +++ b/doc/fix_stubs/_overrides.py @@ -0,0 +1,107 @@ +"""Setter type overrides, method signature overrides, and base class substitutions. + +SETTER_TYPE_OVERRIDES + Boost.Python's add_property() never generates an auto-signature for the setter half of a + property (only the getter gets one). This table maps "ClassName.propertyName" -> annotated + type string, consulted when a setter has no parsable docstring. + +METHOD_OVERRIDES + map_indexing_suite / vector_indexing_suite generate __getitem__, __setitem__, __iter__, etc. + with generic "object" types and no docstrings. This table maps "ClassName.method" to an + explicit list of (ret, [(param_name, param_type), ...]) tuples (one per overload). + +BASE_CLASS_SUBSTITUTIONS + Replaces Boost.Python artifact base classes (unresolvable by Pyright) with native equivalents. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +# Each entry: list of (ret_type, [(param_name, param_type), ...]) +# All entries are instance methods (is_method=True implicitly). +# Use "typing.Iterator[str]" etc. for complex type expressions -- quote_type won't double-quote them. +_MethodSig = list[tuple[str, list[tuple[str, str]]]] + +METHOD_OVERRIDES: dict[str, _MethodSig] = { + # HandleMap = std::map via map_indexing_suite (manipulation) + "HandleMap.__getitem__": [("Handle", [("key", "str")])], + "HandleMap.__setitem__": [("None", [("key", "str"), ("value", "Handle")])], + "HandleMap.__delitem__": [("None", [("key", "str")])], + "HandleMap.__iter__": [("typing.Iterator[str]", [])], + # GripperMap = std::map via map_indexing_suite (pinocchio) + "GripperMap.__getitem__": [("Gripper", [("key", "str")])], + "GripperMap.__setitem__": [("None", [("key", "str"), ("value", "Gripper")])], + "GripperMap.__delitem__": [("None", [("key", "str")])], + "GripperMap.__iter__": [("typing.Iterator[str]", [])], +} + +SETTER_TYPE_OVERRIDES: dict[str, str] = { + "Handle.mask": "list[bool]", + "Handle.maskComp": "list[bool]", + "Handle.localPosition": "pyhpp.pinocchio.bindings.Transform3s", + "Handle.approachingDirection": "numpy.ndarray", + "Handle.clearance": "float", + "Handle.name": "str", +} + +# Pyright cannot resolve these Boost.Python internal base classes; replace with native equivalents. +# "Boost.Python.enum" behaves like IntEnum at the Python level. +BASE_CLASS_SUBSTITUTIONS: dict[str, str] = { + "Boost.Python.instance": "object", + "Boost.Python.enum": "int", +} + + +def load_method_overrides(path: Path | None) -> dict[str, _MethodSig]: + """Merge METHOD_OVERRIDES with an optional external JSON file. + + JSON format -- each entry is a single overload or a list of overloads:: + + { + "ClassName.method": {"ret": "RetType", "params": [["name", "Type"], ...]}, + "ClassName.method2": [ + {"ret": "RetType", "params": [["name", "Type"]]}, + {"ret": "RetType2", "params": []} + ] + } + + External entries take precedence over the built-in table.""" + merged: dict[str, _MethodSig] = dict(METHOD_OVERRIDES) + if path is None: + return merged + + raw = json.loads(path.read_text()) + if not isinstance(raw, dict): + raise ValueError(f"{path}: expected a JSON object, got {type(raw).__name__}") + + for key, value in raw.items(): + entries = value if isinstance(value, list) else [value] + sigs: _MethodSig = [] + for entry in entries: + ret = entry["ret"] + params = [(p[0], p[1]) for p in entry.get("params", [])] + sigs.append((ret, params)) + merged[key] = sigs + + return merged + + +def load_setter_overrides(path: Path | None) -> dict[str, str]: + """Merge SETTER_TYPE_OVERRIDES with an optional external JSON file. + The JSON file must be a flat object: {"Class.prop": "type", ...}. + External entries take precedence over the built-in table.""" + merged = dict(SETTER_TYPE_OVERRIDES) + if path is not None: + data = json.loads(path.read_text()) + if not isinstance(data, dict): + raise ValueError( + f"{path}: expected a flat JSON object, got {type(data).__name__}" + ) + merged.update(data) + return merged + + +def normalize_base_class(base: str) -> str: + return BASE_CLASS_SUBSTITUTIONS.get(base, base) diff --git a/doc/fix_stubs/_parser.py b/doc/fix_stubs/_parser.py new file mode 100644 index 00000000..c54cd63a --- /dev/null +++ b/doc/fix_stubs/_parser.py @@ -0,0 +1,348 @@ +"""Parse a .pyi file into a (header, tree) pair ready for rendering. + +The tree is a list of ClassI objects. The first entry may be a pseudo-class +(is_module=True) that carries top-level functions. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from ._models import ClassI, Function, Overload +from ._overrides import METHOD_OVERRIDES, SETTER_TYPE_OVERRIDES, normalize_base_class +from ._patterns import ( + ARG_RE, + CLASS_ATTR_RE, + CLASS_RE, + DEF_RE, + DOCSTRING_OPEN_RE, + PROPERTY_RE, + SETTER_RE, + SIG_RE, + STATICMETHOD_RE, +) +from ._text_utils import clean_description, read_docstring + + +def parse_all_signatures( + docstring: str, method_name: str +) -> list[tuple[str, list[tuple[str, str]], str]] | None: + """Cherche TOUTES les lignes 'methodName( (Type)arg, ...) -> Ret' dans la docstring. + + Une docstring Boost.Python peut contenir plusieurs signatures (overloads) separees par + une ligne vide. Pour les properties/setters, Boost.Python ecrit parfois "None" a la place + du vrai nom de methode: on accepte aussi ce cas. + + Retourne une liste de (type_retour, [(nom, type), ...], description_nettoyee), + une entree par signature, ou None si aucune signature n'est parsable.""" + lines = docstring.splitlines() + + sig_indices = [] + for idx, line in enumerate(lines): + stripped = line.strip() + if stripped.startswith(method_name + "("): + matched_name = method_name + elif stripped.startswith("None("): + matched_name = "None" + else: + continue + m = SIG_RE.match(stripped) + if m and m.group("name") == matched_name: + sig_indices.append(idx) + + if not sig_indices: + return None + + results = [] + for k, idx in enumerate(sig_indices): + stripped = lines[idx].strip() + m = SIG_RE.match(stripped) + ret = m.group("ret").strip() + raw_args = [a.strip() for a in m.group("args").split(",") if a.strip()] + params: list[tuple[str, str]] = [] + for raw in raw_args: + am = ARG_RE.match(raw) + if am: + pname = am.group("name") + if pname == "self": + # certains bindings nomment (par erreur) un parametre "self" alors + # que ce n'est pas le premier -> collision avec le vrai self injecte + pname = "self_" + params.append((pname, am.group("type"))) + else: + return None # signature illisible -> abandonner le parsing complet + + next_idx = sig_indices[k + 1] if k + 1 < len(sig_indices) else len(lines) + description = clean_description(lines[idx + 1 : next_idx]) + results.append((ret, params, description)) + + return results + + +def _parse_def_body( + lines: list[str], i: int, n: int, fc_name: str +) -> tuple[list | None, int, int, str | None]: + """Parse ce qui suit une ligne 'def name(...):' a l'index i. + Retourne (parsed_or_None, index_apres, body_end, raw_doc).""" + j = i + 1 + while j < n and lines[j].strip() == "": + j += 1 + + if j < n and DOCSTRING_OPEN_RE.match(lines[j]): + raw_doc, after = read_docstring(lines, j) + parsed = parse_all_signatures(raw_doc, fc_name) + return parsed, after, after, raw_doc + if j < n and lines[j].strip() == "...": + return None, j + 1, j + 1, None + return None, j, j, None + + +def _make_overloads(parsed: list, class_name: str | None) -> list[Overload]: + overloads = [] + for ret, params, description in parsed: + is_method = False + if params and class_name: + first_type = params[0][1] + if first_type == class_name or first_type.rsplit(".", 1)[-1] == class_name: + is_method = True + params = params[1:] + overloads.append(Overload(ret, params, description, is_method)) + return overloads + + +def _make_setter_override_overload(type_str: str) -> Overload: + """Fabrique un Overload synthetique pour un setter de property dont on connait le + type uniquement via SETTER_TYPE_OVERRIDES (aucune docstring Boost.Python ne le documente). + Toujours: 1 argument "value", retour None, is_method=True.""" + return Overload( + ret="None", params=[("value", type_str)], description="", is_method=True + ) + + +def _apply_method_override( + fc: Function, override_key: str, method_overrides: dict +) -> bool: + """Applique un METHOD_OVERRIDES sur fc si la cle est presente. + Retourne True si un override a ete applique.""" + sigs = method_overrides.get(override_key) + if sigs is None: + return False + fc.overloads = [Overload(ret, params, "", True) for ret, params in sigs] + fc.from_docstring = True + return True + + +def extract_tree( + path: Path, + setter_overrides: dict[str, str] | None = None, + method_overrides: dict | None = None, +) -> tuple[str, list[ClassI]]: + """Retourne (header, tree). Le header contient tout ce qui precede la 1ere classe + ou la 1ere fonction de niveau module (imports, __future__, __all__, etc.). + + setter_overrides: mapping "ClassName.propName" -> type string, consulte quand un + setter de property n'a pas de docstring parsable. + method_overrides: mapping "ClassName.method" -> [(ret, [(name, type), ...])] pour + les methodes generees sans docstring (ex: map_indexing_suite).__""" + if setter_overrides is None: + setter_overrides = SETTER_TYPE_OVERRIDES + if method_overrides is None: + method_overrides = METHOD_OVERRIDES + + lines = path.read_text().splitlines() + tree: list[ClassI] = [] + current: ClassI | None = None + class_name: str | None = None + module_root = ClassI(is_module=True) + module_root.name = "" + + n = len(lines) + header_end = n + for idx, ln in enumerate(lines): + if ( + CLASS_RE.match(ln) + or DEF_RE.match(ln) + or PROPERTY_RE.match(ln) + or STATICMETHOD_RE.match(ln) + or SETTER_RE.match(ln) + ): + header_end = idx + break + header = "\n".join(lines[:header_end]).rstrip("\n") + + i = header_end + while i < n: + line = lines[i] + + cm = CLASS_RE.match(line) + if cm: + class_name = cm.group(1) + current = ClassI() + current.name = class_name + bases_raw = cm.group("bases") or "" + raw_bases = [b.strip() for b in bases_raw.split(",") if b.strip()] + current.bases = [normalize_base_class(b) for b in raw_bases] + tree.append(current) + i += 1 + + j = i + while j < n and lines[j].strip() == "": + j += 1 + if j < n and DOCSTRING_OPEN_RE.match(lines[j]): + doc, after = read_docstring(lines, j) + current.docstring = clean_description(doc.splitlines()) + i = after + continue + + if line.strip() and not line[0].isspace() and not cm: + current = None + class_name = None + + is_prop = PROPERTY_RE.match(line) + setter_m = SETTER_RE.match(line) + static_m = STATICMETHOD_RE.match(line) + + if ( + (is_prop or setter_m or static_m) + and i + 1 < n + and DEF_RE.match(lines[i + 1]) + ): + def_line_idx = i + def_idx = i + 1 + dm = DEF_RE.match(lines[def_idx]) + fname = dm.group(1) + + parsed, after, body_end, _raw_doc = _parse_def_body( + lines, def_idx, n, fname + ) + raw_lines = lines[def_line_idx:body_end] + i = after + + container = current if current is not None else module_root + is_method_ctx = class_name if current is not None else None + + if setter_m: + existing = next( + ( + f + for f in container.functions + if f.name == fname and f.is_property + ), + None, + ) + if existing is None: + existing = Function() + existing.name = fname + existing.is_property = True + existing.in_class = current is not None + container.functions.append(existing) + if parsed: + existing.setter_overloads = _make_overloads(parsed, is_method_ctx) + existing.from_docstring = True + else: + override_key = f"{class_name}.{fname}" if class_name else fname + override_type = setter_overrides.get(override_key) + if override_type is not None and existing.from_docstring: + existing.setter_overloads = [ + _make_setter_override_overload(override_type) + ] + elif override_type is not None and not existing.from_docstring: + print( + f"warning: {override_key}: setter override ignored because " + f"the getter itself has no parsable docstring signature -- " + f"fix the getter's C++ docstring first (see mask example)", + file=sys.stderr, + ) + existing.setter_raw_blocks.append(raw_lines) + else: + existing.setter_raw_blocks.append(raw_lines) + continue + + fc = Function() + fc.name = fname + fc.raw_lines = raw_lines + fc.in_class = current is not None + override_key = f"{class_name}.{fname}" if class_name else fname + if _apply_method_override(fc, override_key, method_overrides): + pass # override applied, parsed is ignored + elif parsed: + fc.from_docstring = True + if fname == "__init__": + fc.overloads = [ + Overload(ret, params[1:], description, True) + for ret, params, description in parsed + ] + else: + fc.overloads = _make_overloads(parsed, is_method_ctx) + if is_prop: + fc.is_property = True + container.functions.append(fc) + continue + + dm = DEF_RE.match(line) + if dm: + fc_name = dm.group(1) + def_line_idx = i + parsed, after, body_end, _raw_doc = _parse_def_body(lines, i, n, fc_name) + raw_lines = lines[def_line_idx:body_end] + i = after + + container = current if current is not None else module_root + is_method_ctx = class_name if current is not None else None + + fc = Function() + fc.name = fc_name + fc.raw_lines = raw_lines + fc.in_class = current is not None + + override_key = f"{class_name}.{fc_name}" if class_name else fc_name + if _apply_method_override(fc, override_key, method_overrides): + pass # override applied, parsed is ignored + elif parsed: + fc.from_docstring = True + if fc_name == "__init__": + fc.overloads = [ + Overload(ret, params[1:], description, True) + for ret, params, description in parsed + ] + else: + fc.overloads = _make_overloads(parsed, is_method_ctx) + + # Fusionner les occurrences repetees du meme nom dans la meme classe. + # On ne devine jamais "property" ici (seulement si @property/@X.setter + # etaient presents litteralement), pour preserver l'idempotence. + existing = next((f for f in container.functions if f.name == fc_name), None) + if existing is not None and existing.from_docstring and fc.from_docstring: + existing.overloads.extend(fc.overloads) + continue + if ( + existing is not None + and existing.from_docstring + and not fc.from_docstring + ): + continue + if ( + existing is not None + and not existing.from_docstring + and not fc.from_docstring + ): + existing.raw_blocks.extend(fc.raw_blocks) + continue + + container.functions.append(fc) + continue + + if current is not None and CLASS_ATTR_RE.match(line) and line.strip(): + stripped = line.strip() + if not stripped.startswith("__slots__"): + current.extra_lines.append(stripped) + i += 1 + continue + + i += 1 + + if module_root.functions: + tree.insert(0, module_root) + + return header, tree diff --git a/doc/fix_stubs/_patterns.py b/doc/fix_stubs/_patterns.py new file mode 100644 index 00000000..5df3c7a4 --- /dev/null +++ b/doc/fix_stubs/_patterns.py @@ -0,0 +1,48 @@ +"""Compiled regex patterns and primitive constants shared across the package.""" + +from __future__ import annotations + +import re + +CLASS_RE = re.compile( + r"^class\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\((?P[^)]*)\))?\s*:" +) +DEF_RE = re.compile( + r"^\s*def\s+([A-Za-z_][A-Za-z0-9_]*)\s*\((?P.*?)\)\s*(?:->\s*(?P.+?))?\s*:" +) +STATICMETHOD_RE = re.compile(r"^\s*@staticmethod\s*$") +PROPERTY_RE = re.compile(r"^\s*@property\s*$") +SETTER_RE = re.compile(r"^\s*@([A-Za-z_][A-Za-z0-9_]*)\.setter\s*$") +DOCSTRING_OPEN_RE = re.compile(r'^\s*"""') +CLASS_ATTR_RE = re.compile(r"^\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*.+$") + +# Boost.Python signature line inside a docstring: +# addGripper( (Device)arg1, (str)arg2) -> None : +SIG_RE = re.compile( + r"""^\s* + (?P[A-Za-z_][A-Za-z0-9_]*) + \(\s*(?P.*?)\s*\)\s*->\s* + (?P[A-Za-z_][A-Za-z0-9_.\[\], ]*?) + \s*:?\s*$""", + re.VERBOSE, +) + +# A single "(Type)name" argument token +ARG_RE = re.compile( + r"^\(\s*(?P[A-Za-z_][A-Za-z0-9_.]*)\s*\)\s*(?P[A-Za-z_][A-Za-z0-9_]*)$" +) + +BUILTIN_TYPES = { + "int", + "float", + "str", + "bool", + "object", + "list", + "dict", + "tuple", + "set", + "None", + "NoneType", + "bytes", +} diff --git a/doc/fix_stubs/_renderer.py b/doc/fix_stubs/_renderer.py new file mode 100644 index 00000000..18c42cac --- /dev/null +++ b/doc/fix_stubs/_renderer.py @@ -0,0 +1,22 @@ +"""Render a parsed (header, tree) pair to a .pyi file string.""" + +from __future__ import annotations + +from ._models import ClassI + + +def render_file(header: str, tree: list[ClassI]) -> str: + """Construit le contenu complet du fichier .pyi corrige a partir du header et de l'arbre.""" + parts = [] + if header: + parts.append(header) + parts.append("") + + for cls in tree: + parts.append(str(cls)) + parts.append("") + + content = "\n".join(parts) + if not content.endswith("\n"): + content += "\n" + return content diff --git a/doc/fix_stubs/_text_utils.py b/doc/fix_stubs/_text_utils.py new file mode 100644 index 00000000..77fa8fcd --- /dev/null +++ b/doc/fix_stubs/_text_utils.py @@ -0,0 +1,46 @@ +"""Low-level text manipulation helpers.""" + +from __future__ import annotations + + +def indent_block(text: str, spaces: int) -> str: + """Indente chaque ligne de `text` de `spaces` espaces.""" + pad = " " * spaces + return "\n".join(pad + line if line else line for line in text.splitlines()) + + +def read_docstring(lines: list[str], start: int) -> tuple[str, int]: + """Lit une docstring triple-quote a partir de lines[start]. Retourne (texte, index_apres). + + Gere aussi le cas ou du texte suit directement les \"\"\" d'ouverture sur la meme ligne + (ex: '\"\"\"Device with handles.'), qui peut arriver quand on re-parse un fichier deja + corrige par ce script (pour que l'operation reste stable si on la relance).""" + stripped = lines[start].strip() + if stripped.count('"""') >= 2 and len(stripped) > 3: + return stripped.split('"""', 2)[1], start + 1 + + opening_text = stripped[3:] + body = [opening_text] if opening_text else [] + i = start + 1 + n = len(lines) + while i < n and lines[i].strip() != '"""': + body.append(lines[i]) + i += 1 + i += 1 + return "\n".join(body), i + + +def clean_description(desc_lines: list[str]) -> str: + """Nettoie les lignes de description qui suivent la signature dans la docstring: + retire les lignes vides en debut/fin, et dedente au minimum d'indentation commun.""" + while desc_lines and desc_lines[0].strip() == "": + desc_lines.pop(0) + while desc_lines and desc_lines[-1].strip() == "": + desc_lines.pop() + if not desc_lines: + return "" + + indents = [len(ln) - len(ln.lstrip()) for ln in desc_lines if ln.strip()] + min_indent = min(indents) if indents else 0 + dedented = [ln[min_indent:] if len(ln) >= min_indent else ln for ln in desc_lines] + return "\n".join(dedented) diff --git a/doc/fix_stubs/_type_utils.py b/doc/fix_stubs/_type_utils.py new file mode 100644 index 00000000..6ef700b0 --- /dev/null +++ b/doc/fix_stubs/_type_utils.py @@ -0,0 +1,39 @@ +"""Utilities for mapping Boost.Python type strings to valid Python type annotations.""" + +from __future__ import annotations + +from ._patterns import BUILTIN_TYPES + + +def quote_type(t: str) -> str: + """Met entre guillemets (forward reference) tout type qui n'est pas un builtin simple, + pour eviter que Pyright/mypy tente de le resoudre immediatement (ex: noms qui collisionnent + avec une fonction de meme nom au niveau module, types dans un autre fichier .pyi, etc.).""" + if t in BUILTIN_TYPES: + return t + if t == "NoneType": + return "None" + # certains types Boost.Python (souvent des templates C++ mal resolus) se terminent + # par un point parasite, ex: "pyhpp.core.path.bindings.SplineB3." -> retirer ce point, + # sinon la forward-reference n'est pas une expression de type valide pour Pyright + t = t.rstrip(".") + # Les expressions de type generiques (ex: typing.Iterator[str], list[bool]) sont deja + # syntaxiquement valides et ne doivent pas etre encadrees de guillemets: Pyright les + # interprete correctement telles quelles, et les guillemets les rendraient invalides. + if "[" in t: + return t + return f'"{t}"' + + +def type_module(t: str) -> str | None: + """Extrait le module a importer pour un type dotte, ex: + 'pyhpp.core.bindings.Distance' -> 'pyhpp.core.bindings' + 'numpy.ndarray' -> 'numpy' + 'Transition' (pas de point) -> None (type local, pas d'import necessaire) + 'pyhpp.core.path.bindings.SplineB3.' -> 'pyhpp.core.path.bindings' (le point final artefact est ignore).""" + if t in BUILTIN_TYPES or t == "NoneType": + return None + t = t.rstrip(".") + if "." not in t: + return None + return t.rsplit(".", 1)[0] diff --git a/src/pyhpp/manipulation/device.cc b/src/pyhpp/manipulation/device.cc index e66d37b6..cb74fde1 100644 --- a/src/pyhpp/manipulation/device.cc +++ b/src/pyhpp/manipulation/device.cc @@ -43,6 +43,7 @@ const char* DOC_HANDLE_LOCALPOSITION = "Local position of the handle in the joint frame."; const char* DOC_HANDLE_MASK = + "mask( (Handle)arg1) -> list:\n" "Constraint mask: vector of size 6 defining the symmetries of the " "handle. See Handle class documentation for details."; From 08d9c356c18f401137cff2d6df7589767f47901d Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Thu, 9 Jul 2026 16:05:41 +0200 Subject: [PATCH 09/18] rm cache --- .../__pycache__/__init__.cpython-313.pyc | Bin 667 -> 0 bytes .../__pycache__/__main__.cpython-313.pyc | Bin 286 -> 0 bytes doc/fix_stubs/__pycache__/_cli.cpython-313.pyc | Bin 2971 -> 0 bytes .../__pycache__/_fixer.cpython-313.pyc | Bin 1480 -> 0 bytes .../__pycache__/_imports.cpython-313.pyc | Bin 3492 -> 0 bytes .../__pycache__/_models.cpython-313.pyc | Bin 10707 -> 0 bytes .../__pycache__/_overrides.cpython-313.pyc | Bin 4720 -> 0 bytes .../__pycache__/_parser.cpython-313.pyc | Bin 14614 -> 0 bytes .../__pycache__/_patterns.cpython-313.pyc | Bin 1607 -> 0 bytes .../__pycache__/_renderer.cpython-313.pyc | Bin 1048 -> 0 bytes .../__pycache__/_text_utils.cpython-313.pyc | Bin 3430 -> 0 bytes .../__pycache__/_type_utils.cpython-313.pyc | Bin 1656 -> 0 bytes 12 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 doc/fix_stubs/__pycache__/__init__.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/__main__.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_cli.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_imports.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_models.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_parser.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_patterns.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_text_utils.cpython-313.pyc delete mode 100644 doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc diff --git a/doc/fix_stubs/__pycache__/__init__.cpython-313.pyc b/doc/fix_stubs/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 1f50fc92f9aa8317634640bb44b4ac067f20ccff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 667 zcmah`&59F25bpWOBnkGXo|AA_$@j>W+{^vN00`PtDAvleElC58XYIx#>IT z6L|6!d;?!$U@z;*n-aWRJ?`Q~u@2Q=cXfSL_0{3%$R}u@Kc2n*ViEGyT&~Q%1c!SF zK9HD55?ir7v!=GR!C+_ROkL@A*q!yJp7el(B<{ViWPj@g{rWbNZup)mU8f2z-oA%t zd=F2AP)~w@^t@F$s!q%S6}s4jU;7Sc^<)^FfV$e^IA7 z&*1T5^1uf)Q)}9lEPEMovA7sO{=D8m6^0G6~O~pVbY0d)w8%jk2 zt94CJcn+?HL45tMRRTx4m?ItIdg$iCpzTwN5>eW^G|PDxYdsWYj~bZYt^Y@lF3Dsh zc~0pka=MOW1_b_4^Kr_7dW{JcU83%>h7HHLnSkF9aY^LLo6Daj=N2U zl=6~mN^!aE;9()wb#~=<2FhLogm4J+?48aUV48e@SOx}!MOhrrz3^7bf45m;e z3JmGYnk-dnjyXB`*jNyP~YsTHZor6rm9d3rAyf!Z_~ zZ?WViX6E^6GT&m4k59=@j*q{^5}%xtS;P#KUdiwoWcDpT{fzwFRQ;5K%)FAslJv|R zeV6>?(%jU%l4AXgf`Xv@Vk5uI3jN}uWDr*uVzWNbG<~?$`tk7~i{j(;3My}L*yQG? nl;)(`6>$QM2RWoz07!gbW@Kc%$)IzWLF^#|ODkIuJ5Ue+R##2L diff --git a/doc/fix_stubs/__pycache__/_cli.cpython-313.pyc b/doc/fix_stubs/__pycache__/_cli.cpython-313.pyc deleted file mode 100644 index f78a806e43dcb28d8b61e4c268faedecaed93285..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2971 zcmb6bO>Y~=b(Y*+Qv8r8iLz8zk;k-)ijGJkk=;~m8o;)wC=?_)T+v8PyX=a)q}EyP zvNOwwVh}*+NiGUlEgDomAZRW05FH&LKw2Pad!+0DQZ{lMAnhSH8A^eoz4XmeYcfLG zx(n>goA=(#`Pab6nO#Jea}W zIWO@#Gf#M!`8gpg5)o*EB+mJ=e&Tmv-&{{NKmyqy3BtNZ@}EYxc@nC4@ZB%gaD0l3T zCkOyyj!ULE1LaXp=%%z`$t&>%(Zw^jO!4?X@%p+rk>r3NP~$m(1m=DYn43K zZB^dii==^k4~)@C{%LG`?G@)le*Ptdf(UH=ZF}zW)KH#>^G@Xj7>6f?r#ZEP&Z5G3 z*8(4s&*URO#^TTPo?J>C=o{Ba$x=!=ol(5NpRI3=WRR=6C6SImHvqUYT85PQ^0Hkt>Xf;*_ zY@r4vfZE)%&6l0?SOK6Is)wQxwFGBsu(V88Oz?O&P6laIl}TwuRX}{HPN?>Vs;|Z4 zURy9^QlU0)t<_YUUs3g1b0Cpu*Nw!Ja?X0FX{_#Gb||jOwHh-AkC%1X!shaAP&SRNOk_spdF0{-VHp|#rQSowJ)2)PNU@5b-l*tvBesC>QT)dviLA!Ca z0V;rAST(?(;F?vA{ z=$kR3TFl_3w!yKQOynvBOra1nm@_7hX}272+4X6gT}&1j4TPpzVhM2H#QD?!8PZ z^(tKVHMS&n2!gW+R0l_RcI^R&lOZXAX8yM* zBDEB-#}+kOgxrF)Ni0>d1ya$?Wx_h2-IHmQR3}g3L0fbr5u0~pIbxUuTdue zCJrfGvjr!cZLVx{hRvyzDS|tnolUKnRW+qRD#5FuqUot=v*Z{?Q!BMv&ZN&TXpIyl zCALh|oVt^OVv=HS1*i0+iX~lx`?L9~tQq72NMNmzZbLI%M+f1dz3}-5;q&{^n~%Mo zUU7ZqP((s-k00LUhxh$`oAjONe)#&KfP|69$Sa74{x0(9I~RANhrB2JBks`WaQJ{D z@^u`AuX2P2_JX6k!O@4ovGwV{4o}=$+Dly7Ot^Ry(qDNmp-`h-i-&5*+-F)e;o?I^iRs(2p{$aGD-fUWMJm3@1yUI z&5Zi~Fe(5Hi77%=sT0HI7feHqhizZ62tiOR+JR!RTxWfzSS0Y?LnO)u_Rg?PzGzEZO6VVi*p+9LIfv go_&O3pQFA{QQ#38`5PMj3!47BnB;!Qkx5|sH#AW4t^fc4 diff --git a/doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc b/doc/fix_stubs/__pycache__/_fixer.cpython-313.pyc deleted file mode 100644 index 2f023e70d487a4fd81fae44972133823673a9312..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1480 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 diff --git a/doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc b/doc/fix_stubs/__pycache__/_overrides.cpython-313.pyc deleted file mode 100644 index 750c8556eee26c286ba426cbcea7a0f93bd74520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4720 zcmdT{OKcm*8J=A}MT(S6iI%MAM6xW=rkAZGH@5P&q|}O{N4aE>mR1%kawV<3++}8$ zj>W`)qC={STbLb(59s;C?0_kB@88xw0fdJ_x=*5nlLxUdr&n{mQ zoIcP~m*DKb|IGX|^FO}-pMy{+fZ&<`<^xvRJ3-95*31*T_FuafV^8N%{ zqpRGO1dNB?z@c%$ZyeXk`AL8eT@Lc$QCN#_gI_y;jF0dgd?)86t$Y;vj@x}*d^dlB zV@b&7_1L@tzPATmag#7V$oJV)8;|*Zn`&Qo#hwsDE)XjjhD5NjS&*=HO(I0jNqQXT zC1XR&VO?HVMWaX#^@(Z@uZp^avx=zeSTC;XhHMlKSyOc`5a4H1soA8E`u_5)u=L7o zGC4Oh%Lgz#7d1^cxaCa{t&Zwg%;kgv(FzhVHpgOEm0)?iE~yd`4GFqcEEWyzY-0~= z8f2Chg!5uS$3xTf z42xo3;%Wlu#}GdIENnp44A@G}mTzlFH;AmR$8i>pT2!D5zp){~J{w!ov8v&MNOXIR zoR)Q_fL<16QcC~{Ud zup;XQHN_Z_j5yAWfda(5pn@jjR2e{1^~K_tLDu{93*yVd?dvd`(m&)d1i90^$tC7%NycJ@x2C_;xRg#)jp+1PW86 z7;Cw%N}HAoENi(JsbjI02V@i_LP4R_31AvQqP!20JDaQiv@h9JlhAncu0;K$9q5%7olPK}h1snLY47uH8t`ez z{Z2Ko%r1lx3L=cufZCGwCQmdZ)u>nBo~-KvqlH;#{HtQxMYHxo!TF~4(^(9EwPg)F zU;GUictIlT5RP$b5XYQ;LN*SPxBRKN!QH|>_@^ri$_%@0%f%^^qwLsNJtDH$oi z{h>G>va^5=%uHs8h8E@7eJf5hk)y_>H3->tCHB>Zu+#}99CwI9b}_`Okh=gzq?`n>VJprqI#!FaVw{y#r^?_;15;p+0>BLv3!-BY2QA`w zT{E!d0$o{NM?Hkb)E6|AVASC&kKHlK-z&fVe(J}{ZhU(CMKd~6X>GqXe{;UvQ4am7_0&Tj zI^O@lkGgud^>=~~+fdigg9vJkY%To#%NBHemeHwsK0zJR=lqAV1Wl1HnkC%eK>t2V ztmXl)|6el%^SBI=Zo*ZHpOA@v>p81_MIi!a+y|n40O+Rf0 zps~EgLJsaUF&7!8ypxpY29BtM?;r?ba+#O)-~%z8}C}zXA=!oF9cF_adWv zp)ot=c70v@Z5_8tZ;-fufmKKTpx{kD?(**3W%JUC+5g&JL@-;0ia&fSbTf22chBEj2_D;8IDpmAA!nzKYdd=! z2yk{fznR+Zf?0$LJOU%g6VTA&p5A433cA&^cd9f?Dso8@XqRALbIAk@ zSckt(Ym+Us9~{0P9Ni6$?zs1Y6Q+BDd<*z7x2@K%gY?3H-@^2EV0i)Ii$o<9DXRm> zB+!JGS_BFP${nXP4Pl2%sn{{gCkTLyfZzc^SS!+txF8UELfh1qk7BnXuM(;bf}ycI z_T9>s1e5BuxsylQ1fl6>1EOrNv6Vs5lu+~92mJuq$8mhY|8!o<6&2}O@(lD-Md=h` z9=I5W`H~GY?ym+AGyFLU{snbE2!Kc29x1!Zsk5?*&J9f`xhO?=#%Ead9(jQrEmPhi)QbWX3zQ0 zaBOGMJoA(}__R6jOa-6X889y_m}86P;F3A8>@dd8n}bwxrGMhXq}g}g?7i?X8a%_A zsNIZ=9-tt@KIlW8xY9LT>AP4N97$-v9sr diff --git a/doc/fix_stubs/__pycache__/_parser.cpython-313.pyc b/doc/fix_stubs/__pycache__/_parser.cpython-313.pyc deleted file mode 100644 index aa312c589dde9420bed9bb0239a820d3deb49e53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14614 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 diff --git a/doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc b/doc/fix_stubs/__pycache__/_renderer.cpython-313.pyc deleted file mode 100644 index 0d05d517124e944e6910d4e3ee8f2b7a5cc71915..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048 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 diff --git a/doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc b/doc/fix_stubs/__pycache__/_type_utils.cpython-313.pyc deleted file mode 100644 index 9619047fe448dc77db5be1cd2f053923d8e6640e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1656 zcmb7^&1>976u?LOy;`pwlccl_q|;znt2kO{8Zd4N#ElPe8Yhmtv>^vEmS%S+NHg-x z$n}QM8hqK%LVO7rdg-Ny`~&?vT5{P6dMZ8SPzboa_l;yXags|1qHvzK`x# zWYueWwOpzV>W=+jy{B2kG@}alg~TyQ62=F3SqP=w+ao;`9P3d+vC$>w96X-2^c8%A@1WE}cZ}a_%PU_#I+-J{(mUbrReNX}dDTnhIpowx0}O9c zjVafX;z7!yh{jYaJP+OYn#d4K+NYB8fI3ims*7f9r3~{9RT?Km;gsV|@YIZB#o{EQ z3w9z>iRm`eRN}3aLbD_r40SgiB_pgM8pR>S5ryNGNJEIt;+TevXb8_Wo~JuYn2T6} zH^YI5BBr3b9QJIoO#_HY;lg-edTx%2!Ou8gl48!bDg4ADOd*#%V}(P)^Gr!KQ~)>spqZop+{6(ePqn_2fV>w;5~RcLWmlLmt1sg+q;nGWM@y*m^! z?S=pi-2Z?@-D@I9O$n+yOp-f7eZ0nYx=IG-^J0<%)eS|^?Xw+U>2yG9S$*q0`v!zJ!3$yP{x@`GfDRh;AEc&z zuPrm3myagcs~UqG$R%)F+0l|PZSW}W5EH;75z^3B;?cj@7@Y^gpc88~NGIDuHq$H!lFjv9Wh6HHf&q44Go;RceXo-NU zcFcK(5j>yg5s83A3;8gFog!7tX+V`COj74WatYDHml=9V#5i5P-B_lScC;Zj-vb|KZ+4qi#(-UVOB;|M|h}v(~wT ztG~9+|575+xV0=Hb zeZQZA8})r@{`Rt_4^09nxGE|%^Ur+C`XKlG7o@wtQvd(} From 274664a9a7528addac762c1a032701017f22ee5c Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Thu, 9 Jul 2026 16:36:55 +0200 Subject: [PATCH 10/18] doc: add gen_api_md.py to generate Markdown API reference from .pyi stubs --- doc/gen_api_md.py | 492 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 doc/gen_api_md.py diff --git a/doc/gen_api_md.py b/doc/gen_api_md.py new file mode 100644 index 00000000..f89da0c2 --- /dev/null +++ b/doc/gen_api_md.py @@ -0,0 +1,492 @@ +#!/usr/bin/env python3 +"""Generate Markdown API reference from pyhpp .pyi stubs for mdbook. + +Usage: + gen_api_md.py --stubs \ + --output +Exemple: + python gen_api_md.py \ + --stubs $DEVEL_HPP_DIR/install/lib/python3.13/site-packages \ + --output $DEVEL_HPP_DIR/src/hpp-doc/mdbook/src/reference/hpp-python/api/ +""" + +import ast +import keyword +import re +import sys +import argparse +from pathlib import Path + +BOOST_SIG_RE = re.compile(r"^[\w][\w:]*\s*\(.*\)\s*->\s*\S") +BOOST_RET_RE = re.compile(r"->\s*(\S+?)(?:\s*:)?\s*$") + +SKIP_NAMES = {"__reduce__", "__instance_size__", "__hash__"} +SKIP_BASES = {"object", "instance"} + +NOT_INSTANTIABLE = re.compile( + r"raises an exception|cannot be instantiated from python", re.IGNORECASE +) +PARAM_RE = re.compile(r"^:param\s+(\w+):\s*(.*)") +RETURNS_RE = re.compile(r"^:returns?:\s*(.*)") +TYPE_RE = re.compile(r"^:(?:type|rtype)\b") +SECTION_RE = re.compile( + r"^(inputs?|outputs?|parameters?|args?|arguments?)s?:?\s*$", re.IGNORECASE +) +KV_RE = re.compile(r"^([A-Za-z]\w*): +(.+)$") + +MODULES = [ + ( + "pyhpp/core/bindings.pyi", + "pyhpp.core", + "Path planning problem, planners, optimizers, roadmap, steering methods.", + ), + ( + "pyhpp/core/path/bindings.pyi", + "pyhpp.core.path", + "Concrete path types: StraightPath, PathVector, splines.", + ), + ( + "pyhpp/core/path_optimization/bindings.pyi", + "pyhpp.core.path_optimization", + "Concrete path optimizer implementations.", + ), + ( + "pyhpp/core/problem_target/bindings.pyi", + "pyhpp.core.problem_target", + "Problem target types (goal configurations, etc.).", + ), + ( + "pyhpp/constraints/bindings.pyi", + "pyhpp.constraints", + "Differentiable functions, implicit/explicit constraints, hierarchical solver.", + ), + ( + "pyhpp/manipulation/bindings.pyi", + "pyhpp.manipulation", + "Manipulation-specific Device, constraint graph, path planners.", + ), + ( + "pyhpp/manipulation/urdf/bindings.pyi", + "pyhpp.manipulation.urdf", + "URDF/SRDF loading for manipulation devices.", + ), + ( + "pyhpp/manipulation/steering_method/bindings.pyi", + "pyhpp.manipulation.steering_method", + "Manipulation-specific steering methods.", + ), + ( + "pyhpp/pinocchio/bindings.pyi", + "pyhpp.pinocchio", + "Robot model (Device), Lie-group utilities.", + ), + ( + "pyhpp/pinocchio/urdf/bindings.pyi", + "pyhpp.pinocchio.urdf", + "URDF/SRDF loading for pinocchio devices.", + ), +] + + +def slug(module_name: str) -> str: + return module_name.replace(".", "-").removeprefix("pyhpp-") + + +def simplify_type(name: str) -> str: + return name.split(".")[-1] + + +# --------------------------------------------------------------------------- +# Docstring line formatting +# --------------------------------------------------------------------------- + + +def doxygen_to_katex(text: str) -> str: + """Convert Doxygen LaTeX notation to mdbook-katex syntax.""" + # \begin{env}...\end{env} → $$\begin{env}...\end{env}$$ + text = re.sub( + r"\\begin\{(\w+\*?)\}(.*?)\\end\{\1\}", + lambda m: f"\n$$\\begin{{{m.group(1)}}}{m.group(2)}\\end{{{m.group(1)}}}$$\n", + text, + flags=re.DOTALL, + ) + return text + + +def _format_line(s: str) -> str | None: + if TYPE_RE.match(s): + return None + m = PARAM_RE.match(s) + if m: + return f"**{m.group(1)}** — {m.group(2)}" + m = RETURNS_RE.match(s) + if m: + return f"**Returns** — {m.group(1)}" + if SECTION_RE.match(s): + return f"*{s.rstrip(':').capitalize()}:*" + m = KV_RE.match(s) + if m and " " not in m.group(1): + return f"**{m.group(1)}** — {m.group(2)}" + return s.replace("<", "<").replace(">", ">").replace("|", "\\|") + + +def text_to_cell(text: str) -> str: + """Flatten multiline text into a single table cell using
.""" + lines = [] + for line in text.split("\n"): + s = line.strip() + if not s: + continue + # List markers don't render inside table cells + s = re.sub(r"^[-*]\s+", "", s) + lines.append(s) + return "
".join(lines) + + +# --------------------------------------------------------------------------- +# Overload-aware docstring parsing +# --------------------------------------------------------------------------- + + +def parse_overloads(raw: str | None) -> list[tuple[str | None, str]]: + """Split a Boost.Python docstring into (return_type, formatted_text) pairs.""" + if not raw: + return [] + + result: list[tuple[str | None, str]] = [] + current_ret: str | None = None + current: list[str] = [] + + for line in raw.splitlines(): + s = line.strip() + if BOOST_SIG_RE.match(s): + if current: + text = "\n".join(current).rstrip() + if text: + result.append((current_ret, text)) + current = [] + m = BOOST_RET_RE.search(s) + current_ret = m.group(1) if m else None + elif not s: + if current and current[-1] != "": + current.append("") + else: + formatted = _format_line(s) + if formatted is not None: + current.append(formatted) + + if current: + text = "\n".join(current).rstrip() + if text: + result.append((current_ret, text)) + + return result + + +def overloads_to_cell(overloads: list[tuple[str | None, str]]) -> str: + """Convert overloads to a single table cell string.""" + if not overloads: + return "" + + if len(overloads) == 1: + return text_to_cell(overloads[0][1]) + + # 2 overloads — try getter/setter + if len(overloads) == 2: + rets = [r for r, _ in overloads] + if sum(1 for r in rets if r == "None") == 1: + parts = [] + for ret, text in overloads: + label = "**setter**" if ret == "None" else "**getter**" + parts.append(f"{label} — {text_to_cell(text)}") + return "
".join(parts) + + # N overloads — numbered + parts = [] + for i, (_, text) in enumerate(overloads, 1): + parts.append(f"**{i}.** {text_to_cell(text)}") + return "
".join(parts) + + +# --------------------------------------------------------------------------- +# AST helpers +# --------------------------------------------------------------------------- + + +def get_docstring(node: ast.AST) -> str | None: + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and isinstance(node.body[0].value.value, str) + ): + return node.body[0].value.value + return None + + +def is_ellipsis_body(node: ast.FunctionDef) -> bool: + return ( + len(node.body) == 1 + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and node.body[0].value.value is ... + ) + + +def has_decorator(node: ast.FunctionDef, name: str) -> bool: + for d in node.decorator_list: + if isinstance(d, ast.Name) and d.id == name: + return True + if isinstance(d, ast.Attribute) and d.attr == name: + return True + return False + + +def is_setter(node: ast.FunctionDef) -> bool: + return any( + isinstance(d, ast.Attribute) and d.attr == "setter" for d in node.decorator_list + ) + + +def base_links( + class_node: ast.ClassDef, stub_module_index: dict[str, str] +) -> list[str]: + """Return base class names as markdown links with full dotted path displayed. + + stub_module_index maps dotted stub module path to page slug, + e.g. "pyhpp.core.bindings" -> "core". + """ + links = [] + for b in class_node.bases: + raw = ( + ast.unparse(b) + if isinstance(b, ast.Attribute) + else (b.id if isinstance(b, ast.Name) else "") + ) + if not raw: + continue + class_name = simplify_type(raw) # last dotted component + if class_name in SKIP_BASES: + continue + # Try to resolve the page from the module path (all components but last) + module_path = raw.rsplit(".", 1)[0] if "." in raw else "" + page = stub_module_index.get(module_path) or stub_module_index.get(raw) + if page: + anchor = class_name.lower() + links.append(f"[`{raw}`]({page}.md#{anchor})") + else: + links.append(f"`{raw}`") + return links + + +# --------------------------------------------------------------------------- +# Markdown rendering +# --------------------------------------------------------------------------- + + +def collect_member_row(node: ast.FunctionDef) -> tuple[str, str, str] | None: + """Return (name, tag, cell_text) or None if nothing to show.""" + if node.name in SKIP_NAMES or is_ellipsis_body(node) or is_setter(node): + return None + + raw = get_docstring(node) + + if node.name == "__init__": + return None # handled separately as class-level note + + overloads = parse_overloads(raw) + if not overloads: + return None + + is_prop = has_decorator(node, "property") + tag = " *(property)*" if is_prop else "" + cell = overloads_to_cell(overloads) + return (node.name, tag, cell) + + +def render_class( + class_node: ast.ClassDef, lines: list[str], stub_module_index: dict[str, str] +) -> bool: + raw_doc = get_docstring(class_node) + bases = base_links(class_node, stub_module_index) + + # Collect method rows + rows: list[tuple[str, str, str]] = [] + init_not_instantiable = False + for item in class_node.body: + if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + if item.name == "__init__": + init_raw = get_docstring(item) + if NOT_INSTANTIABLE.search(init_raw or ""): + init_not_instantiable = True + else: + row = collect_member_row(item) + if row: + rows.append(row) + + if not raw_doc and not rows and not init_not_instantiable: + return False + + lines.append(f"## `{class_node.name}`\n") + + meta: list[str] = [] + if bases: + meta.append(f"*Inherits: {', '.join(bases)}*") + if init_not_instantiable: + meta.append("*Not instantiable from Python.*") + if meta: + lines.append(" ".join(meta) + "\n") + + if raw_doc: + overloads = parse_overloads(raw_doc) + doc = overloads_to_cell(overloads) + if doc: + doc = doxygen_to_katex(doc.replace("
", "\n\n")) + quoted = "\n".join( + f"> {ln}" if ln.strip() else ">" for ln in doc.splitlines() + ) + lines.append(quoted + "\n") + + if rows: + lines.append("| Method | Description |") + lines.append("|:---|:---|") + for name, tag, cell in rows: + lines.append(f"| `{name}`{tag} | {cell} |") + lines.append("") + + lines.append("---\n") + return True + + +def render_function_row(func_node: ast.FunctionDef) -> tuple[str, str] | None: + """Return (name, cell_text) for a module-level function.""" + if func_node.name in SKIP_NAMES or is_ellipsis_body(func_node): + return None + overloads = parse_overloads(get_docstring(func_node)) + cell = overloads_to_cell(overloads) + if not cell: + return None + return (func_node.name, cell) + + +# --------------------------------------------------------------------------- +# Per-module generation +# --------------------------------------------------------------------------- + +_KW_PARAM_RE = re.compile( + r"([,(]\s*)(" + "|".join(re.escape(kw) for kw in keyword.kwlist) + r")(\s*:)" +) + + +def _parse_stub(stub_path: Path) -> ast.Module | None: + try: + source = stub_path.read_text() + source = re.sub(r"ClassVar\[[^\]]*\.\]", "ClassVar[Any]", source) + source = _KW_PARAM_RE.sub(r"\1\2_\3", source) + return ast.parse(source) + except Exception: + return None + + +def build_stub_module_index() -> dict[str, str]: + """Map dotted stub module path -> page_slug. + + e.g. "pyhpp.core.bindings" -> "core", used to resolve base class links + like pyhpp.core.bindings.Problem -> core.md#problem without ambiguity.""" + index: dict[str, str] = {} + for rel_stub, module_name, _ in MODULES: + # "pyhpp/core/bindings.pyi" -> "pyhpp.core.bindings" + dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".") + index[dotted] = slug(module_name) + return index + + +def generate_module_md( + stub_path: Path, module_name: str, stub_module_index: dict[str, str] +) -> str: + tree = _parse_stub(stub_path) + if tree is None: + return f"# `{module_name}`\n\n*Failed to parse stub.*\n" + + lines: list[str] = [f"# `{module_name}`\n"] + has_content = False + + func_rows: list[tuple[str, str]] = [] + + for item in tree.body: + if isinstance(item, ast.ClassDef): + if render_class(item, lines, stub_module_index): + has_content = True + elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + row = render_function_row(item) + if row: + func_rows.append(row) + + if func_rows: + lines.append("## Functions\n") + lines.append("| Function | Description |") + lines.append("|:---|:---|") + for name, cell in func_rows: + lines.append(f"| `{name}()` | {cell} |") + lines.append("") + has_content = True + + if not has_content: + lines.append("*No documented symbols in this module.*\n") + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main(): + parser = argparse.ArgumentParser( + description="Generate mdbook Markdown pages from pyhpp .pyi stubs" + ) + parser.add_argument( + "--stubs", + required=True, + type=Path, + help="Python site-packages directory (e.g. install/lib/python3.x/site-packages)", + ) + parser.add_argument( + "--output", + required=True, + type=Path, + help="Output directory for generated .md files", + ) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + + stub_module_index = build_stub_module_index() + + generated: list[tuple[str, str, str]] = [] + for rel_stub, module_name, description in MODULES: + stub_path = args.stubs / rel_stub + if not stub_path.exists(): + print(f"WARNING: {stub_path} not found, skipping", file=sys.stderr) + continue + out_file = args.output / f"{slug(module_name)}.md" + out_file.write_text( + generate_module_md(stub_path, module_name, stub_module_index) + ) + generated.append((module_name, slug(module_name), description)) + print(f" {out_file.name}") + + index = [ + "# pyhpp API Reference\n\n", + "Python bindings for HPP — auto-generated from `.pyi` stubs.\n\n", + "| Module | Description |\n", + "|:---|:---|\n", + ] + for mod, s, desc in generated: + index.append(f"| [`{mod}`]({s}.md) | {desc} |\n") + (args.output / "index.md").write_text("".join(index)) + print(" index.md") + + +if __name__ == "__main__": + main() From efd0ccf7c3b90b5796cea46015d8bc56ffedbe42 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 15 Jul 2026 14:35:36 +0200 Subject: [PATCH 11/18] add fix stub in cmakelist --- doc/constraints_doc_todo.md | 158 -------- doc/core_doc_todo.md | 417 ------------------- doc/fix_stubs/_models.py | 5 + doc/fix_stubs/_parser.py | 174 +++++++- doc/fix_stubs/_patterns.py | 6 + doc/gen_api_md.py | 753 ++++++++++++++++++++++++++--------- doc/manipulation_doc_todo.md | 239 ----------- src/CMakeLists.txt | 28 ++ 8 files changed, 775 insertions(+), 1005 deletions(-) delete mode 100644 doc/constraints_doc_todo.md delete mode 100644 doc/core_doc_todo.md delete mode 100644 doc/manipulation_doc_todo.md diff --git a/doc/constraints_doc_todo.md b/doc/constraints_doc_todo.md deleted file mode 100644 index c538b8c7..00000000 --- a/doc/constraints_doc_todo.md +++ /dev/null @@ -1,158 +0,0 @@ -# pyhpp.constraints — documentation status - -## Legend - -- ✅ **Documented** — docstring present in the binding -- ❌ **No C++ doc** — C++ method has no `///` → nothing to add -- ⚠️ **Python-only** — no direct C++ equivalent → doc written in binding or to be written -- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert - ---- - -## differentiable-function.cc - -### DifferentiableFunction - -| Python method / property | Status | Note | -|---|---|---| -| `__call__` | ✅ | `DOC_DF_CALL` from `operator()` in `differentiable-function.hh` | -| `J` | ✅ | Inline string (Python-only: returns Jacobian as numpy array) | -| `name` | ✅ | `DocClassMethod(name)` | -| `ni` | ✅ | `DOC_DF_NI` from `inputSize()` in `differentiable-function.hh` | -| `no` | ✅ | `DOC_DF_NO` from `outputSize()` | -| `ndi` | ✅ | `DOC_DF_NDI` from `inputDerivativeSize()` | -| `ndo` | ✅ | `DOC_DF_NDO` from `outputDerivativeSize()` | -| `value` | ✅ | `DocClassMethod(value)` | -| `jacobian` | ✅ | `DocClassMethod(jacobian)` | -| `outputSpace` | ✅ | `DocClassMethod(outputSpace)` | -| `inputSize` | ✅ | `DocClassMethod(inputSize)` | -| `outputSize` | ✅ | `DocClassMethod(outputSize)` | -| `inputDerivativeSize` | ✅ | `DocClassMethod(inputDerivativeSize)` | -| `outputDerivativeSize` | ✅ | `DocClassMethod(outputDerivativeSize)` | -| `impl_compute` | ❌ No C++ doc | Protected pure virtual, no `///` in the public interface | -| `impl_jacobian` | ❌ No C++ doc | Same | - -### Manipulability / MinManipulability - -| Python method | Status | Note | -|---|---|---| -| `Manipulability.__init__` | ✅ | Inline string — factory `Manipulability::create` | -| `Manipulability.lockJoint` | ✅ | Inline string — no `///` in `manipulability.hh` | -| `MinManipulability.__init__` | ✅ | Inline string — factory `MinManipulability::create` | -| `MinManipulability.lockJoint` | ✅ | Inline string — no `///` in `manipulability.hh` | - ---- - -## implicit.cc - -### Implicit - -| Python method | Status | Note | -|---|---|---| -| `__init__` | ✅ | Inline string — wrapper for `Implicit::create` | -| `comparisonType` (getter) | ✅ | `DOC_COMPARISONTYPE_GET` (`DocClassMethod` unsafe: setter has `comp` param → kwargs mismatch) | -| `comparisonType` (setter) | ✅ | `DOC_COMPARISONTYPE_SET` | -| `function` | ✅ | `DocClassMethod(function)` | -| `parameterSize` | ✅ | `DocClassMethod(parameterSize)` | -| `rightHandSideSize` | ✅ | `DocClassMethod(rightHandSideSize)` | -| `getFunctionOutputSize` | ✅ | Inline string — static method, no direct C++ equivalent | - ---- - -## explicit.cc - -| Python method | Status | Note | -|---|---|---| -| `createExplicit` | ✅ | Inline string — wrapper around `Explicit::create` | - ---- - -## explicit-constraint-set.cc - -### ExplicitConstraintSet - -| Python method | Status | Note | -|---|---|---| -| `add` | ✅ | `DocClassMethod(add)` | -| `errorSize` | ✅ | `DocClassMethod(errorSize)` | - ---- - -## iterative-solver.cc - -### HierarchicalIterative - -| Method / property | Status | Note | -|---|---|---| -| `__str__` | ⚠️ Python-only | Uses `to_str` | -| `add` | ✅ | `DocClassMethod(add)` | -| `errorThreshold` (1st add_property) | ✅ | `DOC_HI_ERRORTHRESHOLD` from `hierarchical-iterative.hh` | -| `errorThreshold` (2nd add_property, duplicate) | ❌ | Pre-existing duplicate in the code, no docstring | -| `rightHandSideFromConfig` (config only) | ✅ | `DOC_HI_RHSFC1` from `hierarchical-iterative.hh` | -| `rightHandSideFromConfig` (constraint + config) | ✅ | `DOC_HI_RHSFC2` | -| `rightHandSide` (setter constraint + rhs) | ✅ | `DOC_HI_RHS_SET1` | -| `rightHandSide` (setter rhs only) | ✅ | `DOC_HI_RHS_SET2` | -| `rightHandSide` (getter) | ✅ | `DOC_HI_RHS_GET` | -| `maxIterations` (add_property) | ✅ | `DOC_HI_MAXITERATIONS` from `hierarchical-iterative.hh` | -| `lastIsOptional` (add_property) | ✅ | Inline string — no `///` in `hierarchical-iterative.hh` (uses `//`) | -| `solveLevelByLevel` (add_property) | ✅ | Inline string — same (non-doxygen `//`) | -| `numberStacks` | ✅ | Inline string — no `///` | -| `constraintsForPriority` | ✅ | Inline string — wrapper returning a Python list | -| `dimension` | ✅ | Inline string — no `///` on the exposed method | - ---- - -## by-substitution.cc - -### BySubstitution - -| Method / property | Status | Note | -|---|---|---| -| `SolverStatus` (enum) | ❌ No C++ doc | Enum without class-level `///` | -| `explicitConstraintSetHasChanged` | ✅ | `DocClassMethod(explicitConstraintSetHasChanged)` | -| `solve` | ✅ | Inline string — returns `(qout, status)` tuple | -| `explicitConstraintSet` | ✅ | `DocClassMethod(explicitConstraintSet)` | -| `rightHandSideFromConfig` (config only) | ✅ | `DOC_BS_RHSFC1` from `by-substitution.hh` | -| `rightHandSideFromConfig` (constraint + config) | ✅ | `DOC_BS_RHSFC2` | -| `rightHandSide` (setter constraint + rhs) | ✅ | `DOC_BS_RHS_SET1` | -| `rightHandSide` (setter rhs only) | ✅ | `DOC_BS_RHS_SET2` | -| `rightHandSide` (getter) | ✅ | `DOC_BS_RHS_GET` | -| `errorThreshold` (add_property) | ✅ | `DOC_BS_ERRORTHRESHOLD` from `by-substitution.hh` | -| `describeError` | ✅ | Inline string — returns list of `(constraint_name, error_norm)` pairs | - ---- - -## generic-transformation.cc - -| Python class | Status | Note | -|---|---|---| -| `Position.__init__` | ✅ | Inline string (absolute generic transformation) | -| `Orientation.__init__` | ✅ | Inline string | -| `Transformation.__init__` | ✅ | Inline string | -| `RelativePosition.__init__` | ✅ | Inline string (relative generic transformation) | -| `RelativeOrientation.__init__` | ✅ | Inline string | -| `RelativeTransformation.__init__` | ✅ | Inline string | -| `RelativeTransformationR3xSO3.__init__` | ✅ | Inline string | - ---- - -## locked-joint.cc - -### LockedJoint - -| Python method | Status | Note | -|---|---|---| -| `__init__` (joint + config) | ✅ | Inline string — `createLockedJoint` wrapper | -| `__init__` (joint + config + comp) | ✅ | Inline string — `createLockedJointWithComp` wrapper | - ---- - -## relative-com.cc - -### RelativeCom - -| Python method | Status | Note | -|---|---|---| -| `__init__` (create1: name+robot+joint+ref+mask) | ✅ | `DocClassMethod(create)` | -| `__init__` (create2: robot+comc+joint+ref+mask) | ⚠️ Python-only | No dedicated `///` in `relative-com.hh` | -| `__init__` (create3: name+robot+comc+joint+ref+mask) | ⚠️ Python-only | Same | diff --git a/doc/core_doc_todo.md b/doc/core_doc_todo.md deleted file mode 100644 index f0c5f7e7..00000000 --- a/doc/core_doc_todo.md +++ /dev/null @@ -1,417 +0,0 @@ -# pyhpp.core — documentation status - -## Legend - -- ✅ **Documented** — docstring present in the binding -- ❌ **No C++ doc** — C++ method has no `///` and no generated doc -- ⚠️ **Python-only** — no direct C++ equivalent → wrapper or modified signature -- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert - ---- - -## path.cc - -### Path - -| Python method | Status | Note | -|---|---|---| -| `__call__` (×2) | ✅ | Inline strings — `(q, success)` and in-place form | -| `eval` (×2) | ✅ | Same inline strings as `__call__` | -| `derivative` | ✅ | Inline string — returns numpy vector | -| `constraints` | ✅ | Inline string — no `///` in `path.hh` | -| `copy` | ✅ | `DocClassMethod(copy)` | -| `extract` | ✅ | `DocClassMethod(extract)` | -| `timeRange` | ✅ | `DocClassMethod(timeRange)` | -| `reverse` | ✅ | `DocClassMethod(reverse)` | -| `paramRange` | ✅ | `DocClassMethod(paramRange)` | -| `length` | ✅ | `DocClassMethod(length)` | -| `initial` | ✅ | `DocClassMethod(initial)` | -| `end` | ✅ | `DocClassMethod(end)` | -| `outputSize` | ✅ | `DocClassMethod(outputSize)` | -| `outputDerivativeSize` | ✅ | `DocClassMethod(outputDerivativeSize)` | -| `StraightPath.__init__` | ❌ No C++ doc | Factory wrapper — no `///` on `StraightPath::create` | - ---- - -## problem.cc - -### Problem - -| Method / property | Status | Note | -|---|---|---| -| `robot` | ✅ | `DocClassMethod(robot)` | -| `setParameter` | ✅ | `DocClassMethod(setParameter)` | -| `getParameter` | ✅ | `DocClassMethod(getParameter)` | -| `addConfigValidation` | ✅ | `DocClassMethod(addConfigValidation)` | -| `clearConfigValidations` | ✅ | `DocClassMethod(clearConfigValidations)` | -| `initConfig` | ✅ | `DocClassMethod(initConfig)` | -| `addGoalConfig` | ✅ | `DocClassMethod(addGoalConfig)` | -| `resetGoalConfigs` | ✅ | `DocClassMethod(resetGoalConfigs)` | -| `steeringMethod` (getter/setter) | ✅ | Inline strings — no `///` in `problem.hh` | -| `configValidation` (getter/setter) | ✅ | Inline strings | -| `pathValidation` (getter/setter) | ✅ | Inline strings | -| `pathProjector` (getter/setter) | ✅ | Inline strings | -| `distance` (getter/setter) | ✅ | Inline strings | -| `target` (getter/setter) | ✅ | Inline strings | -| `configurationShooter` (getter/setter) | ✅ | Inline strings | -| `errorThreshold` (def_readwrite) | ✅ | Inline string — Python-only field | -| `maxIterProjection` (def_readwrite) | ✅ | Inline string — Python-only field | -| `addPartialCom` | ✅ | Inline string | -| `getPartialCom` | ✅ | Inline string | -| `createRelativeComConstraint` | ✅ | Inline string | -| `createTransformationConstraint` (×2) | ✅ | Inline string | -| `setConstantRightHandSide` | ✅ | Inline string | -| `applyConstraints` | ✅ | Inline string — returns `(success, projected_config, residual_error)` | -| `isConfigValid` | ✅ | Inline string — returns `(valid, report)` | -| `setConstraints` | ✅ | Inline string | -| `getConstraints` | ✅ | Inline string | -| `setRightHandSideFromConfig` | ✅ | Inline string | -| `addNumericalConstraintsToConfigProjector` (×2) | ✅ | Inline strings | -| `createComBetweenFeet` | ✅ | Inline string | -| `directPath` | ✅ | Inline string — returns `(valid, path, report)` | - ---- - -## roadmap.cc - -### Roadmap - -| Python method | Status | Note | -|---|---|---| -| `clear` | ✅ | `DocClassMethod(clear)` | -| `nodesWithinBall` | ✅ | `DocClassMethod(nodesWithinBall)` | -| `addEdges` | ✅ | `DocClassMethod(addEdges)` | -| `merge` | ✅ | `DocClassMethod(merge)` | -| `insertPathVector` | ✅ | `DocClassMethod(insertPathVector)` | -| `addGoalNode` | ✅ | `DocClassMethod(addGoalNode)` | -| `resetGoalNodes` | ✅ | `DocClassMethod(resetGoalNodes)` | -| `pathExists` | ✅ | `DocClassMethod(pathExists)` | -| `goalNodes` | ✅ | `DocClassMethod(goalNodes)` | -| `distance` | ✅ | `DocClassMethod(distance)` | -| `addNode` | ✅ | Inline string | -| `nearestNode` (×4) | ✅ | Inline strings — returns `(config, minDistance)` | -| `nearestNodes` (×2) | ✅ | Inline strings | -| `addNodeAndEdges` | ✅ | Inline string | -| `addNodeAndEdge` | ✅ | Inline string | -| `addEdge` (×2) | ✅ | Inline strings | -| `nodes` | ✅ | Inline string | -| `nodesConnectedComponent` | ✅ | Inline string | -| `initNode` (×2) | ✅ | Inline strings | -| `connectedComponents` | ✅ | Inline string | -| `numberConnectedComponents` | ✅ | Inline string | -| `getConnectedComponent` | ✅ | Inline string | -| `connectedComponentOfNode` | ✅ | Inline string | - ---- - -## path-planner.cc - -### PathPlanner - -| Python method | Status | Note | -|---|---|---| -| `roadmap` | ✅ | `DocClassMethod(roadmap)` | -| `problem` | ✅ | `DocClassMethod(problem)` | -| `startSolve` | ✅ | `DocClassMethod(startSolve)` | -| `solve` | ✅ | `DocClassMethod(solve)` | -| `tryConnectInitAndGoals` | ✅ | `DocClassMethod(tryConnectInitAndGoals)` | -| `oneStep` | ✅ | `DocClassMethod(oneStep)` | -| `finishSolve` | ✅ | `DocClassMethod(finishSolve)` | -| `interrupt` | ✅ | `DocClassMethod(interrupt)` | -| `stopWhenProblemIsSolved` | ✅ | `DocClassMethod(stopWhenProblemIsSolved)` | -| `computePath` | ✅ | `DocClassMethod(computePath)` | -| `maxIterations` (getter) | ✅ | `DOC_PP_MAXITER_GET` from `path-planner.hh` | -| `maxIterations` (setter) | ✅ | `DOC_PP_MAXITER_SET` | -| `timeOut` (getter) | ✅ | `DOC_PP_TIMEOUT_GET` from `path-planner.hh` | -| `timeOut` (setter) | ✅ | `DOC_PP_TIMEOUT_SET` | - ---- - -## path-optimizer.cc - -### PathOptimizer - -| Python method | Status | Note | -|---|---|---| -| `problem` | ✅ | `DocClassMethod(problem)` | -| `optimize` | ✅ | `DocClassMethod(optimize)` | -| `interrupt` | ✅ | `DocClassMethod(interrupt)` | -| `maxIterations` | ✅ | `DocClassMethod(maxIterations)` (setter-only, safe) | -| `timeOut` | ✅ | `DocClassMethod(timeOut)` (setter-only, safe) | - -### TrapezoidalTimeParameterization - -| Python property | Status | Note | -|---|---|---| -| `maxVelocity` | ✅ | `DOC_TTP_MAXVEL` from `trapezoidal-time-parameterization.hh` | -| `maxAcceleration` | ✅ | `DOC_TTP_MAXACC` | -| `minimumDuration` | ✅ | `DOC_TTP_MINDUR` | - -### SimpleTimeParameterization - -| Python property | Status | Note | -|---|---|---| -| `safety` | ✅ | `DOC_STP_SAFETY` — from `///` in `simple-time-parameterization.hh` | -| `order` | ✅ | `DOC_STP_ORDER` | -| `maxAcceleration` | ✅ | `DOC_STP_MAXACC` | - -### SplineGradientBased (×3 templates) - -| Python property | Status | Note | -|---|---|---| -| `alphaInit` | ✅ | `DOC_SGB_ALPHAINIT` — from `///` in `spline-gradient-based.hh` | -| `alwaysStopAtFirst` | ✅ | `DOC_SGB_ALWAYSSTOPFIRST` | -| `costOrder` | ✅ | `DOC_SGB_COSTORDER` | -| `usePathLengthAsWeights` | ✅ | `DOC_SGB_USEPATHLENGTH` | -| `reorderIntervals` | ✅ | `DOC_SGB_REORDERINTERVALS` | -| `linearizeAtEachStep` | ✅ | `DOC_SGB_LINEARIZE` | -| `checkJointBound` | ✅ | `DOC_SGB_CHECKJOINTBOUND` | -| `returnOptimum` | ✅ | `DOC_SGB_RETURNOPTIMUM` | -| `costThreshold` | ✅ | `DOC_SGB_COSTTHRESHOLD` | -| `guessThreshold` | ✅ | `DOC_SGB_GUESSTHRESHOLD` | -| `QPAccuracy` | ✅ | `DOC_SGB_QPACCURACY` | - ---- - -## steering-method.cc - -### SteeringMethod - -| Python method | Status | Note | -|---|---|---| -| `__call__` | ✅ | Inline string | -| `steer` | ✅ | `DocClassMethod(steer)` | -| `problem` | ✅ | `DocClassMethod(problem)` | -| `constraints` (setter) | ✅ | `DocClassMethod(constraints)` | -| `constraints` (getter) | ✅ | Inline string (getter/setter pair → `DocClassMethod` unsafe on getter) | - ---- - -## node.cc - -### Node - -| Python method | Status | Note | -|---|---|---| -| `addOutEdge` | ✅ | `DocClassMethod(addOutEdge)` | -| `addInEdge` | ✅ | `DocClassMethod(addInEdge)` | -| `connectedComponent` (getter) | ✅ | Inline string — no `///` in `node.hh` | -| `connectedComponent` (setter) | ✅ | Inline string | -| `outEdges` | ✅ | `DocClassMethod(outEdges)` | -| `inEdges` | ✅ | `DocClassMethod(inEdges)` | -| `isOutNeighbor` | ✅ | `DocClassMethod(isOutNeighbor)` | -| `isInNeighbor` | ✅ | `DocClassMethod(isInNeighbor)` | -| `configuration` | ✅ | `DocClassMethod(configuration)` | - ---- - -## constraint.cc - -### Constraint - -| Python method | Status | Note | -|---|---|---| -| `name` | ✅ | `DocClassMethod(name)` | -| `apply` | ✅ | Inline string — modifies `q` in-place | -| `isSatisfied` (×2) | ✅ | Inline strings | -| `copy` | ✅ | Inline string | - -### ConstraintSet - -| Python method | Status | Note | -|---|---|---| -| `addConstraint` | ✅ | `DocClassMethod(addConstraint)` | -| `configProjector` | ✅ | `DocClassMethod(configProjector)` | - -### ConfigProjector - -| Method / property | Status | Note | -|---|---|---| -| `solver` | ✅ | `DocClassMethod(solver)` | -| `lineSearchType` (add_property) | ✅ | `DOC_CP_LINESEARCHTYPE` | -| `add` | ✅ | `DocClassMethod(add)` | -| `lastIsOptional` (getter/setter) | ✅ | Inline strings — no `///` in `config-projector.hh` | -| `maxIterations` (getter) | ✅ | `DOC_CP_MAXITER_GET` | -| `maxIterations` (setter) | ✅ | `DOC_CP_MAXITER_SET` | -| `errorThreshold` (getter) | ✅ | `DOC_CP_ERRTHRESH_GET` | -| `errorThreshold` (setter) | ✅ | `DOC_CP_ERRTHRESH_SET` | -| `residualError` | ✅ | `DocClassMethod(residualError)` | -| `sigma` | ✅ | `DocClassMethod(sigma)` | -| `setRightHandSideFromConfig` | ✅ | Inline string | -| `setRightHandSideOfConstraint` | ✅ | Inline string | -| `numericalConstraints` | ✅ | Inline string | - ---- - -## distance.cc - -### Distance - -| Python method | Status | Note | -|---|---|---| -| `compute` | ✅ | `DocClassMethod(compute)` | - -### WeighedDistance - -| Python method | Status | Note | -|---|---|---| -| `asDistancePtr_t` | ✅ | Inline string | -| `getWeights` | ✅ | Inline string | -| `setWeights` | ✅ | Inline string | - ---- - -## connected-component.cc - -### ConnectedComponent - -| Python method | Status | Note | -|---|---|---| -| `nodes` | ✅ | `DocClassMethod(nodes)` | -| `reachableFrom` | ✅ | `DocClassMethod(reachableFrom)` | -| `reachableTo` | ✅ | `DocClassMethod(reachableTo)` | -| `__eq__` | ✅ | Inline string | - ---- - -## configuration-shooter.cc - -### ConfigurationShooter - -| Python method | Status | Note | -|---|---|---| -| `shoot` | ✅ | `DocClassMethod(shoot)` | - ---- - -## config-validation.cc - -### ConfigValidation - -| Python method | Status | Note | -|---|---|---| -| `validate` | ✅ | `DocClassMethod(validate)` via `PYHPP_DEFINE_METHOD2` | -| `validate` (py tuple) | ✅ | Inline string — returns `(bool, report)` | - -### ConfigValidations - -| Python method | Status | Note | -|---|---|---| -| `add` | ✅ | `DocClassMethod(add)` via `PYHPP_DEFINE_METHOD2` | -| `numberConfigValidations` | ✅ | `DocClassMethod(numberConfigValidations)` | -| `clear` | ✅ | Inline string — no `///` in `config-validations.hh` | - ---- - -## parameter.cc - -### Parameter - -| Python method | Status | Note | -|---|---|---| -| `boolValue` | ✅ | Inline string | -| `intValue` | ✅ | Inline string | -| `floatValue` | ✅ | Inline string | -| `stringValue` | ✅ | Inline string | -| `vectorValue` | ✅ | Inline string | -| `matrixValue` | ✅ | Inline string | -| `value` | ✅ | Inline string — converts to Python object by type | -| `create_bool` | ✅ | Inline string — factory for `bool` | - ---- - -## path-projector.cc - -### PathProjector - -| Python method | Status | Note | -|---|---|---| -| `apply` | ✅ | `DocClassMethod(apply)` | -| `apply` (py tuple) | ✅ | Inline string — returns `(bool, projPath)` | -| `NoneProjector` | ✅ | Inline string | -| `ProgressiveProjector` | ✅ | Inline string | -| `DichotomyProjector` | ✅ | Inline string | -| `GlobalProjector` | ✅ | Inline string | -| `RecursiveHermiteProjector` | ✅ | Inline string | - ---- - -## path-validation.cc - -### PathValidation - -| Python method | Status | Note | -|---|---|---| -| `validate` | ✅ | `DocClassMethod(validate)` | -| `validate` (py tuple) | ✅ | Inline string — returns `(bool, validPart, report)` | -| `validateConfiguration` | ✅ | Inline string — returns `(bool, report)` | -| `Discretized` / `DiscretizedCollision` | ✅ | Inline string | -| `DiscretizedJointBound` | ✅ | Inline string | -| `DiscretizedCollisionAndJointBound` | ✅ | Inline string | -| `Progressive` | ✅ | Inline string | -| `Dichotomy` | ✅ | Inline string | - ---- - -## problem-target.cc - -### ProblemTarget - -No methods exposed. - ---- - -## reports.cc - -### ValidationReport / CollisionValidationReport / JointBoundValidationReport / PathValidationReport - -| Exposed | Status | Note | -|---|---|---| -| All `def_readonly`/`def_readwrite` fields | ❌ No C++ doc | No `///` in the corresponding headers | - ---- - -## path/vector.cc - -### PathVector (exposed as `Vector`) - -| Python method | Status | Note | -|---|---|---| -| `__init__` | ✅ | Inline string | -| `numberPaths` | ✅ | `DocClassMethod(numberPaths)` | -| `pathAtRank` | ✅ | `DocClassMethod(pathAtRank)` | -| `rankAtParam` | ✅ | `DocClassMethod(rankAtParam)` | -| `appendPath` | ✅ | `DocClassMethod(appendPath)` | -| `concatenate` | ✅ | `DocClassMethod(concatenate)` | -| `flatten` | ✅ | `DocClassMethod(flatten)` | - ---- - -## path/spline.cc - -### SplineB1, SplineB3 - -| Python method | Status | Note | -|---|---|---| -| All methods | ❌ No C++ doc | `PYHPP_DEFINE_METHOD` — no `///` in spline headers | - ---- - -## problem_target/goal-configurations.cc - -### GoalConfigurations - -| Python method | Status | Note | -|---|---|---| -| `computePath` | ❌ No C++ doc | No `///` in `goal-configurations.hh` | -| `reached` | ❌ No C++ doc | Same | - ---- - -## path_optimization/spline-gradient-based-abstract.cc - -### SplineGradientBasedAbstractB1/B3, LinearConstraint, QuadraticProgram - -| Python method / property | Status | Note | -|---|---|---| -| All methods and properties | ❌ No C++ doc | No `///` in the corresponding headers | diff --git a/doc/fix_stubs/_models.py b/doc/fix_stubs/_models.py index 884b21dd..9fd29878 100644 --- a/doc/fix_stubs/_models.py +++ b/doc/fix_stubs/_models.py @@ -163,6 +163,7 @@ def __init__(self, is_module: bool = False): self.bases: list[str] = [] self.docstring = "" self.functions: list[Function] = [] + self.nested_classes: list["ClassI"] = [] self.is_module = is_module self.extra_lines: list[str] = [] @@ -188,6 +189,10 @@ def __str__(self): for extra in self.extra_lines: lines.append(indent_block(extra, 4)) + for nested in self.nested_classes: + lines.append(indent_block(str(nested), 4)) + lines.append("") + for fn in self.functions: lines.append(indent_block(str(fn), 4)) lines.append("") diff --git a/doc/fix_stubs/_parser.py b/doc/fix_stubs/_parser.py index c54cd63a..7f9c5324 100644 --- a/doc/fix_stubs/_parser.py +++ b/doc/fix_stubs/_parser.py @@ -5,7 +5,6 @@ """ from __future__ import annotations - import sys from pathlib import Path @@ -16,7 +15,9 @@ CLASS_ATTR_RE, CLASS_RE, DEF_RE, + DEF_SELF_FIRST_RE, DOCSTRING_OPEN_RE, + NESTED_ARG1_RE, PROPERTY_RE, SETTER_RE, SIG_RE, @@ -133,6 +134,173 @@ def _apply_method_override( return True +def _nested_class_from_fn(fn: Function, outer_name: str) -> str | None: + """Return nested class name from parsed overloads (primary signal). + + A function belongs to a nested class when its first overload has is_method=False + and the first parameter is named 'arg1' with type 'OuterClass.NestedClass'. + + Exception: if any OTHER parameter also has that same NestedClass type, the function + is a static method of the outer class that takes NestedClass data as argument (e.g. + copy(Splines, Splines)), not an instance method of the nested class. + """ + marker = outer_name + "." + all_overloads = list(fn.overloads) + list(fn.setter_overloads) + for ov in all_overloads: + if ov.is_method or not ov.params or ov.params[0][0] != "arg1": + continue + type_path = ov.params[0][1] + idx = type_path.rfind(marker) + if idx == -1: + continue + rest = type_path[idx + len(marker) :] + if not rest or "." in rest: + continue + nested_name = rest + # Guard: if another parameter also carries the same nested class type, + # this is likely a real static method of the outer class, not an instance method. + nested_marker = outer_name + "." + nested_name + if any( + nested_marker in t or t.endswith("." + nested_name) + for _, t in ov.params[1:] + ): + return None + return nested_name + return None + + +def _nested_class_from_blocks(blocks: list[list[str]], outer_name: str) -> str | None: + """Fallback: find nested class name from typed `arg1: "outer.Nested"` in raw def lines. + + Used only for non-docstring functions where overloads are unavailable. + """ + marker = outer_name + "." + for block in blocks: + for line in block: + m = NESTED_ARG1_RE.search(line) + if m: + type_path = m.group(1) + idx = type_path.rfind(marker) + if idx != -1: + rest = type_path[idx + len(marker) :] + if rest and "." not in rest: + return rest + return None + + +def _is_definite_outer_method(fn: Function) -> bool: + """True only for non-dunder methods that definitively belong to the outer class.""" + if fn.name.startswith("__") and fn.name.endswith("__"): + return False + if fn.from_docstring: + return bool(fn.overloads and fn.overloads[0].is_method) + # Raw (non-docstring) functions: check for non-static def with self as first arg + for block in fn.raw_blocks + fn.setter_raw_blocks: + if any("@staticmethod" in ln for ln in block): + continue + for ln in block: + if DEF_SELF_FIRST_RE.match(ln): + return True + return False + + +def _make_instance_method(fn: Function) -> None: + """Convert is_method=False overloads with first param 'arg1' into instance method form.""" + for ov in list(fn.overloads) + list(fn.setter_overloads): + if not ov.is_method and ov.params and ov.params[0][0] == "arg1": + ov.params = ov.params[1:] + ov.is_method = True + + +def _post_process_nested_classes(cls: ClassI) -> None: + """Detect and move nested-class methods out of a flat ClassI into nested ClassI objects.""" + outer_name = cls.name + + # Step 1: compute hint for each function (nested class name, "" = outer, None = ambiguous) + hints: list[str | None] = [] + for fn in cls.functions: + nested = _nested_class_from_fn(fn, outer_name) + if nested is None and not fn.from_docstring: + nested = _nested_class_from_blocks( + fn.raw_blocks + fn.setter_raw_blocks, outer_name + ) + if nested is not None: + hints.append(nested) + elif _is_definite_outer_method(fn): + hints.append("") + else: + hints.append(None) + + if not any(h is not None and h != "" for h in hints): + return + + # Step 2: fill ambiguous hints from nearest definite neighbour + n = len(hints) + filled: list[str] = [""] * n + for i, h in enumerate(hints): + if h is not None: + filled[i] = h + for i in range(n): + if hints[i] is not None: + continue + prev_h = next( + (filled[j] for j in range(i - 1, -1, -1) if hints[j] is not None), None + ) + next_h = next( + (filled[j] for j in range(i + 1, n) if hints[j] is not None), None + ) + if prev_h == next_h: + filled[i] = prev_h if prev_h is not None else "" + elif prev_h is None: + filled[i] = next_h if next_h is not None else "" + elif next_h is None: + filled[i] = prev_h + else: + # Boundary between two blocks: assign to the next block + filled[i] = next_h + + # Step 3: collect ordered nested class names (first-occurrence order) + seen: set[str] = set() + ordered_names: list[str] = [] + for h in filled: + if h and h not in seen: + seen.add(h) + ordered_names.append(h) + + if not ordered_names: + return + + # Step 4: group functions and convert nested-class methods to instance methods + outer_fns: list[Function] = [] + nested_fn_map: dict[str, list[Function]] = {name: [] for name in ordered_names} + + for fn, h in zip(cls.functions, filled): + if h == "": + outer_fns.append(fn) + else: + _make_instance_method(fn) + nested_fn_map[h].append(fn) + + # Step 5: distribute __instance_size__ extra_lines to nested classes (in order) + size_extras = [e for e in cls.extra_lines if "__instance_size__" in e] + other_extras = [e for e in cls.extra_lines if "__instance_size__" not in e] + cls.extra_lines = other_extras + size_extras[:1] + nested_sizes = size_extras[1:] + + # Step 6: build nested ClassI objects + nested_classes: list[ClassI] = [] + for i, name in enumerate(ordered_names): + nested_cls = ClassI() + nested_cls.name = name + nested_cls.functions = nested_fn_map[name] + if i < len(nested_sizes): + nested_cls.extra_lines = [nested_sizes[i]] + nested_classes.append(nested_cls) + + cls.nested_classes = nested_classes + cls.functions = outer_fns + + def extract_tree( path: Path, setter_overrides: dict[str, str] | None = None, @@ -345,4 +513,8 @@ def extract_tree( if module_root.functions: tree.insert(0, module_root) + for cls in tree: + if not cls.is_module: + _post_process_nested_classes(cls) + return header, tree diff --git a/doc/fix_stubs/_patterns.py b/doc/fix_stubs/_patterns.py index 5df3c7a4..bcd40624 100644 --- a/doc/fix_stubs/_patterns.py +++ b/doc/fix_stubs/_patterns.py @@ -32,6 +32,12 @@ r"^\(\s*(?P[A-Za-z_][A-Za-z0-9_.]*)\s*\)\s*(?P[A-Za-z_][A-Za-z0-9_]*)$" ) +# Detects `arg1: "Some.Qualified.Type"` in a def line (nested class membership signal) +NESTED_ARG1_RE = re.compile(r'\barg1:\s*"([^"]+)"') + +# Matches a def line whose first parameter is the bare `self` keyword (outer class method) +DEF_SELF_FIRST_RE = re.compile(r"^\s*def\s+\w+\s*\(\s*self\s*[,)]") + BUILTIN_TYPES = { "int", "float", diff --git a/doc/gen_api_md.py b/doc/gen_api_md.py index f89da0c2..6cdbe533 100644 --- a/doc/gen_api_md.py +++ b/doc/gen_api_md.py @@ -1,28 +1,31 @@ #!/usr/bin/env python3 -"""Generate Markdown API reference from pyhpp .pyi stubs for mdbook. - -Usage: - gen_api_md.py --stubs \ - --output -Exemple: - python gen_api_md.py \ - --stubs $DEVEL_HPP_DIR/install/lib/python3.13/site-packages \ - --output $DEVEL_HPP_DIR/src/hpp-doc/mdbook/src/reference/hpp-python/api/ +"""Generate code-oriented Markdown API reference from pyhpp .pyi stubs (v2). + +Vs gen_api_md.py v1: +- Full Python type annotations with clickable type links, embedded directly in + the code display (HTML
 block) — no separate parameter table needed
+- Per-method sub-headings (### / ####) for sidebar navigation
+- Docstrings shown as blockquotes under each signature
 """
 
+from __future__ import annotations
+
 import ast
+import html as _html
 import keyword
 import re
 import sys
 import argparse
 from pathlib import Path
 
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
 BOOST_SIG_RE = re.compile(r"^[\w][\w:]*\s*\(.*\)\s*->\s*\S")
 BOOST_RET_RE = re.compile(r"->\s*(\S+?)(?:\s*:)?\s*$")
-
 SKIP_NAMES = {"__reduce__", "__instance_size__", "__hash__"}
 SKIP_BASES = {"object", "instance"}
-
 NOT_INSTANTIABLE = re.compile(
     r"raises an exception|cannot be instantiated from python", re.IGNORECASE
 )
@@ -92,27 +95,79 @@ def slug(module_name: str) -> str:
     return module_name.replace(".", "-").removeprefix("pyhpp-")
 
 
-def simplify_type(name: str) -> str:
-    return name.split(".")[-1]
-
-
 # ---------------------------------------------------------------------------
-# Docstring line formatting
+# Math / Doxygen → KaTeX (identical to v1)
 # ---------------------------------------------------------------------------
 
+_MATH_ENV_RE = re.compile(r"\\begin\{(\w+\*?)\}.*?\\end\{\1\}", re.DOTALL)
+
+
+def _protect_math_envs(raw: str) -> tuple[str, list[str]]:
+    stashed: list[str] = []
+
+    def _stash(m: re.Match[str]) -> str:
+        stashed.append(m.group(0))
+        return f"\x00MATHENV{len(stashed) - 1}\x00"
+
+    return _MATH_ENV_RE.sub(_stash, raw), stashed
+
+
+def _restore_math_envs(text: str, stashed: list[str]) -> str:
+    for i, body in enumerate(stashed):
+        text = text.replace(f"\x00MATHENV{i}\x00", body)
+    return text
+
 
 def doxygen_to_katex(text: str) -> str:
-    """Convert Doxygen LaTeX notation to mdbook-katex syntax."""
-    # \begin{env}...\end{env} → $$\begin{env}...\end{env}$$
+    text = re.sub(r"\\begin\{eqnarray(\*?)\}", r"\\begin{align\1}", text)
+    text = re.sub(r"\\end\{eqnarray(\*?)\}", r"\\end{align\1}", text)
+    text = re.sub(r"\\mbox\s*\{", r"\\text{", text)
+
+    def _collapse(body: str) -> str:
+        body = re.sub(r"\s*\n\s*", " ", body)
+        return re.sub(r"\s+", " ", body).strip()
+
+    bracket_blocks: list[str] = []
+
+    def _stash_brackets(m: re.Match[str]) -> str:
+        bracket_blocks.append(_collapse(m.group(1)))
+        return f"\x00BRACKETBLOCK{len(bracket_blocks) - 1}\x00"
+
+    text = re.sub(r"\\\[(.*?)\\\]", _stash_brackets, text, flags=re.DOTALL)
+
+    dollar_blocks: list[str] = []
+
+    def _stash_dollars(m: re.Match[str]) -> str:
+        body = m.group(1)
+        if "\n" not in body or "\\begin{" not in body:
+            return m.group(0)
+        dollar_blocks.append(_collapse(body))
+        return f"\x00DOLLARBLOCK{len(dollar_blocks) - 1}\x00"
+
     text = re.sub(
-        r"\\begin\{(\w+\*?)\}(.*?)\\end\{\1\}",
-        lambda m: f"\n$$\\begin{{{m.group(1)}}}{m.group(2)}\\end{{{m.group(1)}}}$$\n",
-        text,
-        flags=re.DOTALL,
+        r"(? str:
+        env, body = m.group(1), m.group(2)
+        body = _collapse(body)
+        return f"\n\n$$\\begin{{{env}}}{body}\\end{{{env}}}$$\n\n"
+
+    text = re.sub(r"\\begin\{(\w+\*?)\}(.*?)\\end\{\1\}", _wrap, text, flags=re.DOTALL)
+
+    for i, body in enumerate(bracket_blocks):
+        text = text.replace(f"\x00BRACKETBLOCK{i}\x00", f"\n\n$${body}$$\n\n")
+    for i, body in enumerate(dollar_blocks):
+        text = text.replace(f"\x00DOLLARBLOCK{i}\x00", f"\n\n$${body}$$\n\n")
+
     return text
 
 
+# ---------------------------------------------------------------------------
+# Docstring parsing (identical to v1)
+# ---------------------------------------------------------------------------
+
+
 def _format_line(s: str) -> str | None:
     if TYPE_RE.match(s):
         return None
@@ -130,33 +185,12 @@ def _format_line(s: str) -> str | None:
     return s.replace("<", "<").replace(">", ">").replace("|", "\\|")
 
 
-def text_to_cell(text: str) -> str:
-    """Flatten multiline text into a single table cell using 
.""" - lines = [] - for line in text.split("\n"): - s = line.strip() - if not s: - continue - # List markers don't render inside table cells - s = re.sub(r"^[-*]\s+", "", s) - lines.append(s) - return "
".join(lines) - - -# --------------------------------------------------------------------------- -# Overload-aware docstring parsing -# --------------------------------------------------------------------------- - - def parse_overloads(raw: str | None) -> list[tuple[str | None, str]]: - """Split a Boost.Python docstring into (return_type, formatted_text) pairs.""" if not raw: return [] - result: list[tuple[str | None, str]] = [] current_ret: str | None = None current: list[str] = [] - for line in raw.splitlines(): s = line.strip() if BOOST_SIG_RE.match(s): @@ -174,37 +208,37 @@ def parse_overloads(raw: str | None) -> list[tuple[str | None, str]]: formatted = _format_line(s) if formatted is not None: current.append(formatted) - if current: text = "\n".join(current).rstrip() if text: result.append((current_ret, text)) - return result def overloads_to_cell(overloads: list[tuple[str | None, str]]) -> str: - """Convert overloads to a single table cell string.""" if not overloads: return "" - if len(overloads) == 1: - return text_to_cell(overloads[0][1]) - - # 2 overloads — try getter/setter - if len(overloads) == 2: - rets = [r for r, _ in overloads] - if sum(1 for r in rets if r == "None") == 1: - parts = [] - for ret, text in overloads: - label = "**setter**" if ret == "None" else "**getter**" - parts.append(f"{label} — {text_to_cell(text)}") - return "
".join(parts) - - # N overloads — numbered + lines = [] + for line in overloads[0][1].split("\n"): + s = line.strip() + if not s: + continue + if not re.match(r"^[-*]\s+\\", s): + s = re.sub(r"^[-*]\s+", "", s) + lines.append(s) + return "
".join(lines) parts = [] for i, (_, text) in enumerate(overloads, 1): - parts.append(f"**{i}.** {text_to_cell(text)}") + lines = [] + for line in text.split("\n"): + s = line.strip() + if not s: + continue + if not re.match(r"^[-*]\s+\\", s): + s = re.sub(r"^[-*]\s+", "", s) + lines.append(s) + parts.append(f"**{i}.** {'
'.join(lines)}") return "
".join(parts) @@ -212,27 +246,33 @@ def overloads_to_cell(overloads: list[tuple[str | None, str]]) -> str: # AST helpers # --------------------------------------------------------------------------- +_KW_PARAM_RE = re.compile( + r"([,(]\s*)(" + "|".join(re.escape(kw) for kw in keyword.kwlist) + r")(\s*:)" +) + + +def _parse_stub(stub_path: Path) -> ast.Module | None: + try: + source = stub_path.read_text() + source = re.sub(r"ClassVar\[[^\]]*\.\]", "ClassVar[Any]", source) + source = _KW_PARAM_RE.sub(r"\1\2_\3", source) + return ast.parse(source) + except Exception: + return None + def get_docstring(node: ast.AST) -> str | None: + body = getattr(node, "body", None) if ( - node.body - and isinstance(node.body[0], ast.Expr) - and isinstance(node.body[0].value, ast.Constant) - and isinstance(node.body[0].value.value, str) + body + and isinstance(body[0], ast.Expr) + and isinstance(body[0].value, ast.Constant) + and isinstance(body[0].value.value, str) ): - return node.body[0].value.value + return body[0].value.value return None -def is_ellipsis_body(node: ast.FunctionDef) -> bool: - return ( - len(node.body) == 1 - and isinstance(node.body[0], ast.Expr) - and isinstance(node.body[0].value, ast.Constant) - and node.body[0].value.value is ... - ) - - def has_decorator(node: ast.FunctionDef, name: str) -> bool: for d in node.decorator_list: if isinstance(d, ast.Name) and d.id == name: @@ -248,14 +288,145 @@ def is_setter(node: ast.FunctionDef) -> bool: ) -def base_links( - class_node: ast.ClassDef, stub_module_index: dict[str, str] -) -> list[str]: - """Return base class names as markdown links with full dotted path displayed. +def _unparse_ann(ann: ast.expr | None) -> str: + """Unparse a type annotation node, stripping surrounding forward-ref quotes.""" + if ann is None: + return "" + return ast.unparse(ann).strip("'\"") + + +# --------------------------------------------------------------------------- +# Class index for type → link resolution +# --------------------------------------------------------------------------- + +ClassIndex = dict[str, str] # type name variants → "page.md#anchor" - stub_module_index maps dotted stub module path to page slug, - e.g. "pyhpp.core.bindings" -> "core". + +def build_class_index(stubs_root: Path) -> ClassIndex: + """Scan all stubs and map every known class name to its page#anchor.""" + index: ClassIndex = {} + for rel_stub, module_name, _ in MODULES: + stub_path = stubs_root / rel_stub + if not stub_path.exists(): + continue + tree = _parse_stub(stub_path) + if tree is None: + continue + page = slug(module_name) + stub_dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".") + for item in tree.body: + if not isinstance(item, ast.ClassDef): + continue + anchor = item.name.lower() + target = f"{page}.md#{anchor}" + for key in ( + item.name, + f"{module_name}.{item.name}", + f"{stub_dotted}.{item.name}", + ): + index[key] = target + for sub in item.body: + if not isinstance(sub, ast.ClassDef): + continue + sub_anchor = sub.name.lower() + sub_target = f"{page}.md#{sub_anchor}" + for key in ( + f"{item.name}.{sub.name}", + f"{module_name}.{item.name}.{sub.name}", + ): + index.setdefault(key, sub_target) + # simple name: setdefault so first occurrence wins + index.setdefault(sub.name, sub_target) + return index + + +def _type_link(type_str: str, class_index: ClassIndex, current_page: str) -> str: + """Markdown link for a type (used outside code blocks, e.g. base-class line).""" + if not type_str: + return "" + t = type_str.strip("'\"") + simple = t.split(".")[-1] + for key in (t, simple): + target = class_index.get(key) + if target: + page_part, anchor = target.split("#", 1) + href = f"#{anchor}" if page_part == f"{current_page}.md" else target + return f"[`{simple}`]({href})" + return f"`{simple}`" + + +# Built-in Python types that get hljs coloring inside the
 signature block
+_HLJS_BUILTINS: frozenset[str] = frozenset(
+    {
+        "int",
+        "float",
+        "str",
+        "bool",
+        "bytes",
+        "bytearray",
+        "list",
+        "dict",
+        "tuple",
+        "set",
+        "frozenset",
+        "object",
+        "type",
+        "complex",
+    }
+)
+_HLJS_LITERALS: frozenset[str] = frozenset({"None", "True", "False"})
+
+
+def _fmt_ann(ann: str, class_index: ClassIndex, current_page: str) -> str:
+    """Format a type annotation for inside a bare 
 block.
+
+    Returns one of:
+    - ``Full.Type``  for known pyhpp types (clickable link)
+    - ``int``  for Python builtins
+    - ``None``  for None / True / False
+    - HTML-escaped plain text for everything else (numpy, pinocchio, …)
+
+    The hljs CSS classes apply for coloring even inside a bare 
 because
+    mdbook loads highlight.css (which targets class names globally).
+    highlight.js itself never touches this element since it queries 'pre code'
+    and we deliberately omit the inner .
     """
+    if not ann:
+        return ""
+    t = ann.strip("'\"")
+    simple = t.split(".")[-1]
+    for key in (t, simple):
+        target = class_index.get(key)
+        if target:
+            page_part, anchor = target.split("#", 1)
+            href = f"#{anchor}" if page_part == f"{current_page}.md" else target
+            return f'{_html.escape(t)}'
+    if simple in _HLJS_BUILTINS:
+        return f'{_html.escape(t)}'
+    if simple in _HLJS_LITERALS:
+        return f'{_html.escape(t)}'
+    return _html.escape(t)
+
+
+# ---------------------------------------------------------------------------
+# Base class link helpers
+# ---------------------------------------------------------------------------
+
+
+def _build_stub_module_index() -> dict[str, str]:
+    index: dict[str, str] = {}
+    for rel_stub, module_name, _ in MODULES:
+        dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".")
+        index[dotted] = slug(module_name)
+    return index
+
+
+def _base_links(
+    class_node: ast.ClassDef,
+    stub_module_index: dict[str, str],
+    class_index: ClassIndex,
+    current_page: str,
+) -> list[str]:
     links = []
     for b in class_node.bases:
         raw = (
@@ -265,171 +436,372 @@ def base_links(
         )
         if not raw:
             continue
-        class_name = simplify_type(raw)  # last dotted component
+        class_name = raw.split(".")[-1]
         if class_name in SKIP_BASES:
             continue
-        # Try to resolve the page from the module path (all components but last)
+        # Try module-path index first, then class_index (handles same-module bases)
         module_path = raw.rsplit(".", 1)[0] if "." in raw else ""
         page = stub_module_index.get(module_path) or stub_module_index.get(raw)
         if page:
-            anchor = class_name.lower()
-            links.append(f"[`{raw}`]({page}.md#{anchor})")
+            links.append(f"[`{raw}`]({page}.md#{class_name.lower()})")
         else:
-            links.append(f"`{raw}`")
+            link = _type_link(raw, class_index, current_page)
+            links.append(link)
     return links
 
 
 # ---------------------------------------------------------------------------
-# Markdown rendering
+# Body grouping — consecutive same-name FunctionDefs become one overload group
 # ---------------------------------------------------------------------------
 
+type BodyGroup = list[ast.FunctionDef] | ast.ClassDef
 
-def collect_member_row(node: ast.FunctionDef) -> tuple[str, str, str] | None:
-    """Return (name, tag, cell_text) or None if nothing to show."""
-    if node.name in SKIP_NAMES or is_ellipsis_body(node) or is_setter(node):
-        return None
 
-    raw = get_docstring(node)
+def _group_body(body: list[ast.stmt]) -> list[BodyGroup]:
+    result: list[BodyGroup] = []
+    pending: list[ast.FunctionDef] = []
+    for item in body:
+        if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
+            if pending and item.name == pending[-1].name:
+                pending.append(item)
+            else:
+                if pending:
+                    result.append(pending)
+                pending = [item]
+        elif isinstance(item, ast.ClassDef):
+            if pending:
+                result.append(pending)
+                pending = []
+            result.append(item)
+        # Assign (class attrs like __slots__, __instance_size__) — skipped
+    if pending:
+        result.append(pending)
+    return result
 
-    if node.name == "__init__":
-        return None  # handled separately as class-level note
 
-    overloads = parse_overloads(raw)
-    if not overloads:
-        return None
+# ---------------------------------------------------------------------------
+# Signature rendering — HTML 
 with embedded  links
+# ---------------------------------------------------------------------------
+
+_SIG_WIDTH = 72  # chars before switching to multi-line params
+
+
+def _render_signature_pre(
+    group: list[ast.FunctionDef],
+    class_index: ClassIndex,
+    current_page: str,
+) -> str:
+    """Render a signature group as a syntax-coloured 
 with linked types.
+
+    Strategy:
+    - Bare 
 (no inner ) → highlight.js selector 'pre code' never
+      matches →  links survive untouched in the DOM.
+    - Manual  spans → mdbook loads highlight.css globally
+      so those CSS rules apply everywhere, giving us keyword / builtin colours
+      without triggering the highlight.js JavaScript engine.
+    """
+    is_multi_overload = len(group) > 1 and not is_setter(group[-1])
+    parts: list[str] = []
+
+    for node in group:
+        lines_out: list[str] = []
+
+        # --- decorators ---
+        if is_multi_overload:
+            lines_out.append('@typing.overload')
+        for d in node.decorator_list:
+            name = ast.unparse(d)
+            if name == "typing.overload":
+                continue
+            lines_out.append(f'@{_html.escape(name)}')
+
+        # --- parameters (HTML + plain copy for width estimate) ---
+        params_html: list[str] = []
+        params_plain: list[str] = []
+
+        for arg in node.args.args:
+            ann = _unparse_ann(arg.annotation)
+            params_plain.append(f"{arg.arg}: {ann}" if ann else arg.arg)
+            if arg.arg == "self":
+                part = 'self'
+            else:
+                part = _html.escape(arg.arg)
+            if ann:
+                part += f": {_fmt_ann(ann, class_index, current_page)}"
+            params_html.append(part)
+
+        if node.args.vararg:
+            v = node.args.vararg
+            ann = _unparse_ann(v.annotation)
+            params_plain.append(f"*{v.arg}: {ann}" if ann else f"*{v.arg}")
+            pfx = f"*{_html.escape(v.arg)}"
+            params_html.append(
+                f"{pfx}: {_fmt_ann(ann, class_index, current_page)}" if ann else pfx
+            )
+
+        if node.args.kwarg:
+            k = node.args.kwarg
+            ann = _unparse_ann(k.annotation)
+            params_plain.append(f"**{k.arg}: {ann}" if ann else f"**{k.arg}")
+            pfx = f"**{_html.escape(k.arg)}"
+            params_html.append(
+                f"{pfx}: {_fmt_ann(ann, class_index, current_page)}" if ann else pfx
+            )
+
+        # --- return type ---
+        ret = _unparse_ann(node.returns)
+        ret_html = f" -> {_fmt_ann(ret, class_index, current_page)}" if ret else ""
+
+        # --- assemble: single-line or multi-line ---
+        plain_sig = (
+            f"def {node.name}({', '.join(params_plain)}){' -> ' + ret if ret else ''}"
+        )
+        kw = 'def'
+        fn = f'{_html.escape(node.name)}'
+
+        if len(plain_sig) > _SIG_WIDTH and len(params_html) > 1:
+            inner = ",\n    ".join(params_html)
+            sig = f"{kw} {fn}(\n    {inner},\n){ret_html}"
+        else:
+            sig = f"{kw} {fn}({', '.join(params_html)}){ret_html}"
+
+        lines_out.append(sig)
+        parts.append("\n".join(lines_out))
+
+    return f"
{''.join(chr(10) + chr(10)).join(parts)}
" + + +# --------------------------------------------------------------------------- +# Table cell rendering — def | description +# --------------------------------------------------------------------------- + + +def _sig_cell( + group: list[ast.FunctionDef], + class_index: ClassIndex, + current_page: str, +) -> str: + """Inline signature(s) for the 'def' column of the method table. + + Multiple overloads are separated by

. Types are linked via
+ or coloured via hljs spans. Uses (inline) so it stays valid inside + a Markdown table cell. + """ + is_multi_ov = len(group) > 1 and not is_setter(group[-1]) + sig_parts: list[str] = [] + + for node in group: + dec_lines: list[str] = [] + if is_multi_ov: + dec_lines.append('@typing.overload') + for d in node.decorator_list: + name = ast.unparse(d) + if name == "typing.overload": + continue + dec_lines.append(f'@{_html.escape(name)}') + + params_html: list[str] = [] + for arg in node.args.args: + ann = _unparse_ann(arg.annotation) + if arg.arg == "self": + part = 'self' + else: + part = _html.escape(arg.arg) + if ann: + part += f": {_fmt_ann(ann, class_index, current_page)}" + params_html.append(part) + + if node.args.vararg: + v = node.args.vararg + ann = _unparse_ann(v.annotation) + pfx = f"*{_html.escape(v.arg)}" + params_html.append( + f"{pfx}: {_fmt_ann(ann, class_index, current_page)}" if ann else pfx + ) + + if node.args.kwarg: + k = node.args.kwarg + ann = _unparse_ann(k.annotation) + pfx = f"**{_html.escape(k.arg)}" + params_html.append( + f"{pfx}: {_fmt_ann(ann, class_index, current_page)}" if ann else pfx + ) + + ret = _unparse_ann(node.returns) + ret_html = f" -> {_fmt_ann(ret, class_index, current_page)}" if ret else "" + + kw = 'def' + fn = f'{_html.escape(node.name)}' + params_str = ", ".join(params_html) + + sig_line = f"{kw} {fn}({params_str}){ret_html}" + inner = "
".join(dec_lines + [sig_line]) + sig_parts.append(inner) - is_prop = has_decorator(node, "property") - tag = " *(property)*" if is_prop else "" - cell = overloads_to_cell(overloads) - return (node.name, tag, cell) + return "" + "

".join(sig_parts) + "
" + + +def _desc_cell(group: list[ast.FunctionDef]) -> str: + """First non-empty docstring paragraph as plain text for the description column.""" + for ov in group: + if is_setter(ov): + continue + raw = get_docstring(ov) + if not raw: + continue + protected, stashed = _protect_math_envs(raw) + overloads = parse_overloads(protected) + if overloads: + _, text = overloads[0] + text = _restore_math_envs(text, stashed) + first_para = text.split("\n\n")[0] + flat = " ".join(first_para.split()) + return flat.replace("|", "\\|") + return "" + + +# --------------------------------------------------------------------------- +# Class rendering +# --------------------------------------------------------------------------- def render_class( - class_node: ast.ClassDef, lines: list[str], stub_module_index: dict[str, str] -) -> bool: + class_node: ast.ClassDef, + class_index: ClassIndex, + current_page: str, + stub_module_index: dict[str, str], + heading_level: int = 2, +) -> tuple[list[str], bool]: + """Render a class and return (lines, has_content).""" raw_doc = get_docstring(class_node) - bases = base_links(class_node, stub_module_index) + bases = _base_links(class_node, stub_module_index, class_index, current_page) + heading = "#" * heading_level - # Collect method rows - rows: list[tuple[str, str, str]] = [] - init_not_instantiable = False - for item in class_node.body: - if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): - if item.name == "__init__": - init_raw = get_docstring(item) - if NOT_INSTANTIABLE.search(init_raw or ""): - init_not_instantiable = True - else: - row = collect_member_row(item) - if row: - rows.append(row) + groups = _group_body(class_node.body) - if not raw_doc and not rows and not init_not_instantiable: - return False + method_rows: list[tuple[str, str]] = [] + nested_class_lines: list[str] = [] - lines.append(f"## `{class_node.name}`\n") + for g in groups: + if isinstance(g, ast.ClassDef): + sub_lines, _ = render_class( + g, class_index, current_page, stub_module_index, heading_level + 1 + ) + nested_class_lines.extend(sub_lines) + else: + node = g[0] + if node.name in SKIP_NAMES or is_setter(node): + continue + if node.name == "__init__": + if any( + get_docstring(ov) and NOT_INSTANTIABLE.search(get_docstring(ov)) + for ov in g + ): + continue + sig = _sig_cell(g, class_index, current_page) + desc = _desc_cell(g) + method_rows.append((sig, desc)) + + has_content = bool(raw_doc or bases or method_rows or nested_class_lines) + if not has_content: + return [], False + + lines: list[str] = [f"{heading} `{class_node.name}`\n"] meta: list[str] = [] if bases: meta.append(f"*Inherits: {', '.join(bases)}*") - if init_not_instantiable: - meta.append("*Not instantiable from Python.*") if meta: lines.append(" ".join(meta) + "\n") if raw_doc: - overloads = parse_overloads(raw_doc) - doc = overloads_to_cell(overloads) + protected, stashed = _protect_math_envs(raw_doc) + bq_overloads = parse_overloads(protected) + doc = overloads_to_cell(bq_overloads) if doc: + doc = _restore_math_envs(doc, stashed) doc = doxygen_to_katex(doc.replace("
", "\n\n")) quoted = "\n".join( f"> {ln}" if ln.strip() else ">" for ln in doc.splitlines() ) lines.append(quoted + "\n") - if rows: - lines.append("| Method | Description |") + if method_rows: + lines.append("| def | Description |") lines.append("|:---|:---|") - for name, tag, cell in rows: - lines.append(f"| `{name}`{tag} | {cell} |") + for sig, desc in method_rows: + lines.append(f"| {sig} | {desc} |") lines.append("") - lines.append("---\n") - return True + if nested_class_lines: + lines.extend(nested_class_lines) + if heading_level == 2: + lines.append("---\n") -def render_function_row(func_node: ast.FunctionDef) -> tuple[str, str] | None: - """Return (name, cell_text) for a module-level function.""" - if func_node.name in SKIP_NAMES or is_ellipsis_body(func_node): - return None - overloads = parse_overloads(get_docstring(func_node)) - cell = overloads_to_cell(overloads) - if not cell: - return None - return (func_node.name, cell) + return lines, True # --------------------------------------------------------------------------- -# Per-module generation +# Module page generation # --------------------------------------------------------------------------- -_KW_PARAM_RE = re.compile( - r"([,(]\s*)(" + "|".join(re.escape(kw) for kw in keyword.kwlist) + r")(\s*:)" -) - - -def _parse_stub(stub_path: Path) -> ast.Module | None: - try: - source = stub_path.read_text() - source = re.sub(r"ClassVar\[[^\]]*\.\]", "ClassVar[Any]", source) - source = _KW_PARAM_RE.sub(r"\1\2_\3", source) - return ast.parse(source) - except Exception: - return None - - -def build_stub_module_index() -> dict[str, str]: - """Map dotted stub module path -> page_slug. - - e.g. "pyhpp.core.bindings" -> "core", used to resolve base class links - like pyhpp.core.bindings.Problem -> core.md#problem without ambiguity.""" - index: dict[str, str] = {} - for rel_stub, module_name, _ in MODULES: - # "pyhpp/core/bindings.pyi" -> "pyhpp.core.bindings" - dotted = str(Path(rel_stub).with_suffix("")).replace("/", ".") - index[dotted] = slug(module_name) - return index - def generate_module_md( - stub_path: Path, module_name: str, stub_module_index: dict[str, str] + stub_path: Path, + module_name: str, + class_index: ClassIndex, + stub_module_index: dict[str, str], ) -> str: tree = _parse_stub(stub_path) if tree is None: return f"# `{module_name}`\n\n*Failed to parse stub.*\n" + current_page = slug(module_name) lines: list[str] = [f"# `{module_name}`\n"] has_content = False - func_rows: list[tuple[str, str]] = [] + # Module-level functions + func_groups: list[list[ast.FunctionDef]] = [] + pending_funcs: list[ast.FunctionDef] = [] + for item in tree.body: + if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): + if pending_funcs and item.name == pending_funcs[-1].name: + pending_funcs.append(item) + else: + if pending_funcs: + func_groups.append(pending_funcs) + pending_funcs = [item] + elif isinstance(item, ast.ClassDef): + if pending_funcs: + func_groups.append(pending_funcs) + pending_funcs = [] + if pending_funcs: + func_groups.append(pending_funcs) + + if func_groups: + func_rows: list[tuple[str, str]] = [] + for fg in func_groups: + if fg[0].name in SKIP_NAMES: + continue + func_rows.append((_sig_cell(fg, class_index, current_page), _desc_cell(fg))) + if func_rows: + lines.append("## Functions\n") + lines.append("| def | Description |") + lines.append("|:---|:---|") + for sig, desc in func_rows: + lines.append(f"| {sig} | {desc} |") + lines.append("") + has_content = True for item in tree.body: if isinstance(item, ast.ClassDef): - if render_class(item, lines, stub_module_index): + class_lines, ok = render_class( + item, class_index, current_page, stub_module_index + ) + if ok: + lines.extend(class_lines) has_content = True - elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): - row = render_function_row(item) - if row: - func_rows.append(row) - - if func_rows: - lines.append("## Functions\n") - lines.append("| Function | Description |") - lines.append("|:---|:---|") - for name, cell in func_rows: - lines.append(f"| `{name}()` | {cell} |") - lines.append("") - has_content = True if not has_content: lines.append("*No documented symbols in this module.*\n") @@ -442,9 +814,9 @@ def generate_module_md( # --------------------------------------------------------------------------- -def main(): +def main() -> None: parser = argparse.ArgumentParser( - description="Generate mdbook Markdown pages from pyhpp .pyi stubs" + description="Generate code-oriented mdbook Markdown from pyhpp .pyi stubs (v2)" ) parser.add_argument( "--stubs", @@ -461,7 +833,8 @@ def main(): args = parser.parse_args() args.output.mkdir(parents=True, exist_ok=True) - stub_module_index = build_stub_module_index() + class_index = build_class_index(args.stubs) + stub_module_index = _build_stub_module_index() generated: list[tuple[str, str, str]] = [] for rel_stub, module_name, description in MODULES: @@ -471,20 +844,20 @@ def main(): continue out_file = args.output / f"{slug(module_name)}.md" out_file.write_text( - generate_module_md(stub_path, module_name, stub_module_index) + generate_module_md(stub_path, module_name, class_index, stub_module_index) ) generated.append((module_name, slug(module_name), description)) print(f" {out_file.name}") - index = [ + index_lines = [ "# pyhpp API Reference\n\n", "Python bindings for HPP — auto-generated from `.pyi` stubs.\n\n", "| Module | Description |\n", "|:---|:---|\n", ] for mod, s, desc in generated: - index.append(f"| [`{mod}`]({s}.md) | {desc} |\n") - (args.output / "index.md").write_text("".join(index)) + index_lines.append(f"| [`{mod}`]({s}.md) | {desc} |\n") + (args.output / "index.md").write_text("".join(index_lines)) print(" index.md") diff --git a/doc/manipulation_doc_todo.md b/doc/manipulation_doc_todo.md deleted file mode 100644 index 83b01a18..00000000 --- a/doc/manipulation_doc_todo.md +++ /dev/null @@ -1,239 +0,0 @@ -# pyhpp.manipulation — documentation status - -## Legend - -- ✅ **Documented** — docstring present in the binding -- ❌ **No C++ doc** — C++ method has no `///` → `DocClassMethod` would produce an empty string -- ⚠️ **Python-only** — no direct C++ equivalent → doc to be written in the binding -- ⚠️ **Signature diff.** — Python wrapper hides output params → `DocClassMethod` would trigger a Boost.Python static_assert - ---- - -## graph.cc - -### State - -| Python method | Status | Note | -|---|---|---| -| `State.name` | ✅ | `DocClassMethod(name)` | -| `State.id` | ✅ | `DocClassMethod(id)` | -| `State.configConstraint` | ❌ No C++ doc | `State::configConstraint()` declared without `///` in `state.hh` | -| `State.neighborEdges` | ✅ | `DOC_NEIGHBOREDGES` inline | - -### Transition (Edge) - -| Python method | Status | Note | -|---|---|---| -| `Transition.id` | ✅ | `DocClassMethod(id)` | -| `Transition.name` | ✅ | `DocClassMethod(name)` | -| `Transition.isWaypointTransition` | ✅ | `DocClassMethod(isWaypointEdge)` | -| `Transition.nbWaypoints` | ✅ | `DocClassMethod(nbWaypoints)` | -| `Transition.waypoint` | ✅ | `DocClassMethod(waypoint)` | -| `Transition.pathValidation` | ✅ | `DocClassMethod(pathValidation)` | - -### Graph - -| Method / property | Status | Note | -|---|---|---| -| `Graph._get_native_graph` | ✅ | Inline string — C++ capsule for external interop | -| `Graph.robot` | ✅ | Inline string — `def_readwrite`, no `///` | -| `Graph.maxIterations` | ✅ | Explicit `.def()` pair with inline strings | -| `Graph.errorThreshold` | ✅ | Explicit `.def()` pair with inline strings | -| `Graph.createState` | ✅ | `DOC_CREATESTATE` inline | -| `Graph.createTransition` | ✅ | `DOC_CREATETRANSITION` inline | -| `Graph.createWaypointTransition` | ✅ | `DOC_CREATEWAYPOINTTRANSITION` inline | -| `Graph.createLevelSetTransition` | ✅ | `DOC_CREATELEVELSETTRANSITION` inline | -| `Graph.setContainingNode` | ✅ | `DOC_SETCONTAININGNODE` inline | -| `Graph.getContainingNode` | ✅ | `DOC_GETCONTAININGNODE` inline | -| `Graph.setShort` | ✅ | `DOC_SETSHORT` inline | -| `Graph.isShort` | ✅ | `DOC_ISSHORT` inline | -| `Graph.getNodesConnectedByTransition` | ✅ | `DOC_GETNODESCONNECTEDBYTRANSITION` inline | -| `Graph.setWeight` | ✅ | `DOC_SETWEIGHT` inline | -| `Graph.getWeight` | ✅ | `DOC_GETWEIGHT` inline | -| `Graph.setWaypoint` | ✅ | `DOC_SETWAYPOINT` inline | -| `Graph.getState` *(by name)* | ✅ | Inline string — lookup by name in internal map | -| `Graph.getTransition` *(by name)* | ✅ | Inline string — lookup by name in internal map | -| `Graph.getStates` | ✅ | Inline string — returns list of `PyWState` | -| `Graph.getTransitions` | ✅ | Inline string — returns list of `PyWEdge` | -| `Graph.getStateNames` | ✅ | Inline string — returns list of names | -| `Graph.getTransitionNames` | ✅ | Inline string — returns list of names | -| `Graph.getStateFromConfiguration` | ✅ | `DOC_GETSTATE` inline | -| `Graph.addNumericalConstraint` | ✅ | `DOC_ADDNUMERICALCONSTRAINT` inline | -| `Graph.addNumericalConstraintsToState` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOSTATE` inline | -| `Graph.addNumericalConstraintsToTransition` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSTOTRANSITION` inline | -| `Graph.addNumericalConstraintsToGraph` | ✅ | Inline string — takes a list, no direct equivalent | -| `Graph.addNumericalConstraintsForPath` | ✅ | `DOC_ADDNUMERICALCONSTRAINTSFORPATH` inline | -| `Graph.getNumericalConstraintsForState` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFORSTATE` inline | -| `Graph.getNumericalConstraintsForEdge` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFOREDGE` inline | -| `Graph.getNumericalConstraintsForGraph` | ✅ | `DOC_GETNUMERICALCONSTRAINTSFORGRAPH` inline | -| `Graph.resetConstraints` | ✅ | `DOC_RESETCONSTRAINTS` inline | -| `Graph.registerConstraints` | ✅ | `DOC_REGISTERCONSTRAINTS` inline | -| `Graph.createPlacementConstraint` | ✅ | `DOC_CREATEPLACEMENTCONSTRAINT` inline | -| `Graph.createPrePlacementConstraint` | ✅ | `DOC_CREATEPREPLACEMENTCONSTRAINT` inline | -| `Graph.createGraspConstraint` | ✅ | Inline string — wrapper around `Handle::createGrasp` | -| `Graph.createPreGraspConstraint` | ✅ | Inline string — wrapper around `Handle::createPreGrasp` | -| `Graph.getConfigErrorForState` | ✅ | `DOC_GETCONFIGERRORFORSTATE` inline | -| `Graph.getConfigErrorForTransition` | ✅ | `DOC_GETCONFIGERRORFORTRANSITION` inline | -| `Graph.getConfigErrorForTransitionLeaf` | ✅ | `DOC_GETCONFIGERRORFORTRANSITIONLEAF` inline | -| `Graph.getConfigErrorForTransitionTarget` | ✅ | `DOC_GETCONFIGERRORFORTRANSITIONTARGET` inline | -| `Graph.applyStateConstraints` | ✅ | `DOC_APPLYSTATECONSTRAINTS` inline | -| `Graph.applyLeafConstraints` | ✅ | `DOC_APPLYLEAFCONSTRAINTS` inline | -| `Graph.generateTargetConfig` | ✅ | `DOC_GENERATETARGETCONFIG` inline | -| `Graph.addLevelSetFoliation` | ✅ | `DOC_ADDLEVELSETFOLIATION` inline | -| `Graph.getSecurityMarginMatrixForTransition` | ✅ | `DOC_GETSECURITYMARGINMATRIXFORTRANSITION` inline | -| `Graph.setSecurityMarginForTransition` | ✅ | `DOC_SETSECURITYMARGINFORTRANSITION` inline | -| `Graph.getRelativeMotionMatrix` | ✅ | `DOC_GETRELATIVEMOTIONMATRIX` inline | -| `Graph.removeCollisionPairFromTransition` | ✅ | `DOC_REMOVECOLLISIONPAIRFROMTRANSITION` inline | -| `Graph.createSubGraph` | ✅ | `DOC_CREATESUBGRAPH` inline | -| `Graph.setTargetNodeList` | ✅ | `DOC_SETTARGETNODELIST` inline | -| `Graph.displayStateConstraints` | ✅ | `DOC_DISPLAYSTATECONSTRAINTS` inline | -| `Graph.displayTransitionConstraints` | ✅ | `DOC_DISPLAYTRANSITIONCONSTRAINTS` inline | -| `Graph.displayTransitionTargetConstraints` | ✅ | `DOC_DISPLAYTRANSITIONTARGETCONSTRAINTS` inline | -| `Graph.display` | ✅ | `DOC_DISPLAY` inline | -| `Graph.initialize` | ✅ | `DOC_INITIALIZE` inline | -| `Graph.transitionAtParam` | ✅ | Inline string — static method, wraps `Graph::edgeAtParam` | - ---- - -## device.cc - -### Handle - -| Method / property | Status | Note | -|---|---|---| -| `Handle.name` | ✅ | Inline string from `handle.hh` | -| `Handle.localPosition` | ✅ | Inline string from `handle.hh` | -| `Handle.mask` | ✅ | Inline string from `handle.hh` | -| `Handle.maskComp` | ✅ | Inline string from `handle.hh` | -| `Handle.clearance` | ✅ | Inline string from `handle.hh` | -| `Handle.approachingDirection` | ✅ | Inline string from `handle.hh` | -| `Handle.createGrasp` | ✅ | `DocClassMethod(createGrasp)` | -| `Handle.createPreGrasp` | ✅ | `DocClassMethod(createPreGrasp)` | -| `Handle.createGraspComplement` | ✅ | `DocClassMethod(createGraspComplement)` | -| `Handle.createGraspAndComplement` | ✅ | `DocClassMethod(createGraspAndComplement)` | -| `Handle.getParentJointId` | ✅ | Inline string (Python-only) | - -### Device - -| Python method | Status | Note | -|---|---|---| -| `Device.setRobotRootPosition` | ✅ | Inline string — no `///` in `device.hh` | -| `Device.handles` | ✅ | Inline string — public member without `///` | -| `Device.grippers` | ✅ | Inline string — public member without `///` | -| `Device.getJointNames` | ✅ | Inline string — wrapper without direct C++ equivalent | -| `Device.getJointConfig` | ✅ | Inline string — wrapper without direct C++ equivalent | -| `Device.setJointBounds` | ✅ | Inline string | -| `Device.contactSurfaceNames` | ✅ | Inline string | -| `Device.contactSurfaces` | ✅ | Inline string | -| `Device.addHandle` | ✅ | Inline string | -| `Device.addGripper` | ✅ | Inline string | -| `Device.modelsInfo` | ✅ | Inline string | - ---- - -## problem.cc - -| Python method | Status | Note | -|---|---|---| -| `Problem.constraintGraph` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | -| `Problem.constraintGraph` (setter) | ✅ | Inline string | -| `Problem.checkProblem` | ✅ | `DocClassMethod(checkProblem)` | -| `Problem.steeringMethod` (getter) | ✅ | Inline string — unwraps graph SM if applicable | -| `Problem.steeringMethod` (setter, core SM) | ✅ | Inline string | -| `Problem.steeringMethod` (setter, graph SM) | ✅ | Inline string | - ---- - -## path-planner.cc — TransitionPlanner - -| Python method | Status | Note | -|---|---|---| -| `innerPlanner` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | -| `innerPlanner` (setter) | ✅ | Inline string | -| `innerProblem` | ✅ | `DocClassMethod(innerProblem)` | -| `computePath` | ✅ | `DocClassMethod(computePath)` | -| `planPath` *(deprecated)* | ✅ | `DocClassMethod(planPath)` | -| `directPath` | ✅ | Inline string — returns `(success, path, status)` | -| `validateConfiguration` | ✅ | Inline string — returns `(valid, report)` | -| `optimizePath` | ✅ | `DocClassMethod(optimizePath)` | -| `timeParameterization` | ✅ | `DocClassMethod(timeParameterization)` | -| `setEdge` *(deprecated)* | ✅ | `DocClassMethod(setEdge)` | -| `setTransition` | ✅ | `DocClassMethod(setEdge)` | -| `setReedsAndSheppSteeringMethod` | ✅ | `DocClassMethod(setReedsAndSheppSteeringMethod)` | -| `pathProjector` | ✅ | `DocClassMethod(pathProjector)` | -| `clearPathOptimizers` | ✅ | `DocClassMethod(clearPathOptimizers)` | -| `addPathOptimizer` | ✅ | `DocClassMethod(addPathOptimizer)` | - -## path-planner.cc — ManipulationPlanner / StatesPathFinder - -| Python class | Status | Note | -|---|---|---| -| `ManipulationPlanner` | ❌ No C++ doc | Constructor only, no class-level `///` | -| `StatesPathFinder` | ❌ No C++ doc | Same | - -## path-planner.cc — EndEffectorTrajectory *(deprecated)* - -| Python method | Status | Note | -|---|---|---| -| `nRandomConfig` (getter) | ✅ | Inline string (`DocClassMethod` produced empty string) | -| `nDiscreteSteps` (getter) | ✅ | Inline string | -| `checkFeasibilityOnly` (getter) | ✅ | Inline string (`DocClassMethod` unsafe: setter kwargs > getter args) | - ---- - -## path-optimizer.cc - -| Python class / method | Status | Note | -|---|---|---| -| `EnforceTransitionSemantic` | ✅ | `DOC_ENFORCE_TRANSITION_SEMANTIC` inline | -| `RandomShortcut` | ❌ No C++ doc | No class-level `///` in `random-shortcut.hh` | -| `GraphRandomShortcut` | ⚠️ Python-only | Factory function, no direct equivalent | -| `GraphPartialShortcut` | ⚠️ Python-only | Same | -| `SplineGradientBased_bezier1/3` attributes (`alphaInit`, `alwaysStopAtFirst`, `costOrder`, `usePathLengthAsWeights`, `reorderIntervals`, `linearizeAtEachStep`, `checkJointBound`, `returnOptimum`, `costThreshold`, `guessThreshold`, `QPAccuracy`) | ❌ No C++ doc | `def_readwrite` without `///` in manipulation binding | - ---- - -## path-projector.cc - -| Python function | Status | Note | -|---|---|---| -| `NoneProjector` | ⚠️ Python-only | | -| `ProgressiveProjector` | ⚠️ Python-only | Wraps `pathProjector::Progressive::create` | -| `DichotomyProjector` | ⚠️ Python-only | Wraps `pathProjector::Dichotomy::create` | -| `GlobalProjector` | ⚠️ Python-only | Wraps `pathProjector::Global::create` | -| `RecursiveHermiteProjector` | ⚠️ Python-only | Wraps `pathProjector::RecursiveHermite::create` | - ---- - -## steering-method.cc - -| Python class / method | Status | Note | -|---|---|---| -| `GraphSteeringMethod` (no methods) | ❌ No C++ doc | No class-level `///` | -| `EndEffectorTrajectorySteeringMethod.setTrajectoryConstraint` | ✅ | Inline string from `end-effector-trajectory.hh` | -| `EndEffectorTrajectorySteeringMethod.setTrajectory` | ✅ | Inline string from `end-effector-trajectory.hh` | - ---- - -## steering_method/cartesian.cc - -| Method / property | Status | Note | -|---|---|---| -| `Cartesian.maxIterations` | ✅ | Inline string | -| `Cartesian.errorThreshold` | ✅ | Inline string | -| `Cartesian.trajectoryConstraint` | ✅ | Inline string | -| `Cartesian.nDiscreteSteps` | ✅ | Inline string | -| `Cartesian.timeRange` | ✅ | Inline string | -| `Cartesian.setRightHandSide` (×2) | ✅ | Inline string | -| `Cartesian.getRightHandSide` | ✅ | Inline string | -| `Cartesian.planPath` | ✅ | Inline string | -| `makePiecewiseLinearTrajectory` | ✅ | Inline string | - ---- - -## urdf/util.cc - -| Python function | Status | Note | -|---|---|---| -| `loadModel` | ✅ | Inline string — also calls `srdf::loadModelFromFile` | -| `loadModelFromString` | ✅ | Inline string — overload with XML strings | diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8a59d01..6452bdc8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -113,6 +113,13 @@ macro(ADD_PYTHON_LIBRARY MODULE) FILES ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi DESTINATION ${PYTHON_SITELIB}/${MODULE} ) + + set_property( + GLOBAL + APPEND + PROPERTY + PYHPP_ALL_STUB_FILES ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi + ) endif() endmacro() @@ -240,3 +247,24 @@ add_custom_target(all_pyhpp_bindings DEPENDS ${PYHPP_ALL_BINDINGS}) python_install_on_site(pyhpp/tools __init__.py) python_install_on_site(pyhpp/tools xacro.py) python_install_on_site(pyhpp/tools constraint_error.py) + +if(GENERATE_PYTHON_STUBS AND PYBIND11_STUBGEN_EXECUTABLE) + get_property(PYHPP_ALL_STUB_FILES GLOBAL PROPERTY PYHPP_ALL_STUB_FILES) + set(STUB_OUTPUT_DIR ${CMAKE_BINARY_DIR}/stubs) + set(FIX_STUBS_STAMP ${CMAKE_BINARY_DIR}/fix_stubs.stamp) + + add_custom_command( + OUTPUT ${FIX_STUBS_STAMP} + COMMAND + ${CMAKE_COMMAND} -E env + "PYTHONPATH=${CMAKE_SOURCE_DIR}/doc:$ENV{PYTHONPATH}" + ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/doc/fix_stubs.py + ${STUB_OUTPUT_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${FIX_STUBS_STAMP} + DEPENDS ${PYHPP_ALL_STUB_FILES} + COMMENT "Fixing Python stub signatures with fix_stubs.py" + VERBATIM + ) + + add_custom_target(fix_stubs ALL DEPENDS ${FIX_STUBS_STAMP}) +endif() From 99061aac9ec52e89aa642e6da9001cc51687d265 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Mon, 20 Jul 2026 10:40:11 +0200 Subject: [PATCH 12/18] update readme about documention generation and stubs --- README.md | 205 ++++++++---------------------------------------------- 1 file changed, 30 insertions(+), 175 deletions(-) diff --git a/README.md b/README.md index ed74758d..36b643b9 100644 --- a/README.md +++ b/README.md @@ -16,69 +16,16 @@ The resulting `pyhpp` package mirrors the C++ namespace layout: ## Table of contents -- [Package layout](#package-layout) - [Module dependency graph](#module-dependency-graph) - [Dependencies](#dependencies) - [Installation](#installation) - [From source with CMake](#from-source-with-cmake) - [With Nix](#with-nix) -- [Usage](#usage) - - [Building a robot and a problem (`pyhpp.pinocchio` / `pyhpp.core`)](#building-a-robot-and-a-problem-pyhpppinocchio--pyhppcore) - - [Constraints (`pyhpp.constraints`)](#constraints-pyhppconstraints) - - [Manipulation problems (`pyhpp.manipulation`)](#manipulation-problems-pyhppmanipulation) - - [Diagnosing constraint errors (`pyhpp.tools`)](#diagnosing-constraint-errors-pyhpptools) -- [Tests](#tests) - [Documentation generation](#documentation-generation) + - [Injecting Doxygen docstrings into bindings](#injecting-doxygen-docstrings-into-bindings) + - [API reference from stubs](#api-reference-from-stubs) - [License](#license) -## Package layout - -``` -hpp-python/ -├── CMakeLists.txt # C++/CMake build (bindings + headers + Python stub generation) -├── package.xml # ROS package manifest -├── flake.nix # Nix flake (build via github:gepetto/nix) -├── include/pyhpp/ # C++ headers shared by the binding translation units -├── doc/ -│ ├── configure.py # Injects Doxygen-extracted docstrings into the .cc binding sources -│ ├── doxygen_xml_parser.py # Parses Doxygen XML to feed configure.py -│ ├── core_doc_todo.md # Docstring coverage status for pyhpp.core bindings -│ ├── constraints_doc_todo.md # Docstring coverage status for pyhpp.constraints bindings -│ └── manipulation_doc_todo.md # Docstring coverage status for pyhpp.manipulation bindings -├── src/pyhpp/ -│ ├── __init__.py # Top-level package init (imports eigenpy, silences converter warnings) -│ ├── pinocchio/ # hpp-pinocchio bindings: Device, Gripper, Lie-group utilities -│ │ ├── bindings.cc, device.cc/.hh, liegroup.cc, urdf/ -│ │ └── utils.py # shrinkJointRange() and other pure-Python helpers -│ ├── constraints/ # hpp-constraints bindings -│ │ ├── bindings.cc, differentiable-function.cc, generic-transformation.cc, -│ │ │ implicit.cc, explicit.cc, explicit-constraint-set.cc, locked-joint.cc, -│ │ │ iterative-solver.cc, by-substitution.cc, relative-com.cc -│ ├── core/ # hpp-core bindings -│ │ ├── bindings.cc, problem.cc, path.cc, path-planner.cc, path-optimizer.cc, -│ │ │ path-projector.cc, path-validation.cc, roadmap.cc, steering-method.cc, -│ │ │ config-validation.cc, configuration-shooter.cc, constraint.cc, -│ │ │ connected-component.cc, distance.cc, node.cc, parameter.cc, reports.cc, -│ │ │ problem-target.cc -│ │ ├── path/ # concrete Path subclasses submodule -│ │ ├── path_optimization/ # concrete PathOptimizer subclasses submodule -│ │ ├── problem_target/ # concrete ProblemTarget subclasses submodule -│ │ └── static_stability_constraint_factory.py -│ ├── manipulation/ # hpp-manipulation(-urdf) bindings -│ │ ├── bindings.cc, device.cc/.hh, graph.cc/.hh, problem.cc/.hh, -│ │ │ path-planner.cc/.hh, path-optimizer.cc, path-projector.cc, -│ │ │ steering-method.cc/.hh -│ │ ├── steering_method/ # e.g. cartesian.cc submodule -│ │ ├── urdf/ # URDF/SRDF loading submodule -│ │ ├── constraint_graph_factory.py # ConstraintGraphFactory: build constraint graphs declaratively -│ │ └── security_margins.py # SecurityMargins: per-pair collision security margins -│ └── tools/ # Pure-Python helpers, no C++ bindings -│ ├── xacro.py # process_xacro(): resolve xacro files (with ROS 2 AMENT_PREFIX_PATH support) -│ └── constraint_error.py # describe_error(): human-readable constraint violation report -└── tests/ - ├── unit/ # unittest-based tests, one file per bound module (see below) - └── integration/ # End-to-end scenarios (pr2-in-iai-kitchen, romeo-placard, ur3-spheres, ...) -``` ## Module dependency graph @@ -95,8 +42,6 @@ flowchart LR ext --> pin pin --> cons cons --> core - pin --> manip - cons --> manip core --> manip style pin fill:#dbe9f4,stroke:#4a90d9 @@ -106,8 +51,6 @@ flowchart LR style ext fill:#e8f4e8,stroke:#4a9d4a ``` -Importing `pyhpp.manipulation` transitively imports `pyhpp.core`, `pyhpp.constraints` and `pyhpp.pinocchio`; you rarely need to import the lower-level modules explicitly unless you only need robot/constraint primitives without a full manipulation problem. - ## Dependencies - Python ≥ 3.9, with `numpy` @@ -147,135 +90,47 @@ A flake is provided, pulling `hpp-constraints`, `hpp-core` and `hpp-manipulation nix build github:humanoid-path-planner/hpp-python ``` -## Usage - -### Building a robot and a problem (`pyhpp.pinocchio` / `pyhpp.core`) - -```python -from pinocchio import SE3 -from pyhpp.pinocchio import Device, urdf -from pyhpp.core import Problem - -UR5_URDF = "package://example-robot-data/robots/ur_description/urdf/ur5_joint_limited_robot.urdf" -UR5_SRDF = "package://example-robot-data/robots/ur_description/srdf/ur5_joint_limited_robot.srdf" - -robot = Device("ur5") -urdf.loadModel(robot, 0, "ur5", "anchor", UR5_URDF, UR5_SRDF, SE3.Identity()) - -problem = Problem(robot) -sm = problem.steeringMethod() -distance = problem.distance() -shooter = problem.configurationShooter() -``` +## Documentation generation -Path planners, path optimizers, path validation and the roadmap are all bound in `pyhpp.core`, with concrete implementations exposed through the `pyhpp.core.path`, `pyhpp.core.path_optimization` and `pyhpp.core.problem_target` submodules. - -### Constraints (`pyhpp.constraints`) - -```python -from pyhpp.constraints import ( - Transformation, - ComparisonTypes, - ComparisonType, - Implicit, - LockedJoint, -) - -# A 6D relative-transformation function between two frames, turned into an -# implicit equality constraint on the robot's configuration space. -function = Transformation.create("placement", robot, joint, frame_in_joint, target) -constraint = Implicit.create( - function, ComparisonTypes([ComparisonType.Equality] * 6) -) +### Injecting Doxygen docstrings into bindings + +Script `doc/configure.py` is used to generate documentation of Python objects from C++ objects. +It reads the XML documentation generated by doxygen so the dependencies must provide such +files. To make it work, do: +- Add `GENERATE_XML=YES` into `doc/Doxyfile.extra.in` of all the dependency of this project. +- Install the generated documentation by adding the following lines to `CMakeLists.txt`: +```cmake +IF(_INSTALL_DOC) + INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/doxygen-xml + DESTINATION ${CMAKE_INSTALL_DOCDIR}) +ENDIF() ``` -The hierarchical iterative solver (`ExplicitConstraintSet`, `HierarchicalIterativeSolver`, `BySubstitution`) lets you compose several `Implicit`/`Explicit` constraints and project configurations onto the resulting manifold. - -`pyhpp.core.static_stability_constraint_factory.StaticStabilityConstraintsFactory` builds static-stability constraints (e.g. for legged or manipulation problems) on top of these primitives. - -### Manipulation problems (`pyhpp.manipulation`) - -```python -from pyhpp.manipulation import Device, Graph, Problem, urdf, ManipulationPlanner -from pyhpp.manipulation.constraint_graph_factory import ConstraintGraphFactory -from pyhpp.manipulation.security_margins import SecurityMargins +File `doc/configure.py` contains a short documentation of how to document the bindings. -robot = Device("ur3-and-objects") -urdf.loadModel(robot, 0, "ur3", "anchor", UR3_URDF, UR3_SRDF, SE3.Identity()) -# ... load additional robots / objects into the same Device ... +### API reference from stubs -problem = Problem(robot) -graph = Graph("graph", problem) +After building and installing `pyhpp`, `pybind11-stubgen` produces `.pyi` stub files under the Python site-packages directory. -factory = ConstraintGraphFactory(graph) -factory.setGrippers(["ur3/gripper"]) -factory.setObjects(["sphere"], [["sphere/handle"]], [[]]) -factory.generate() +**`doc/fix_stubs.py`** improves those raw Boost.Python stubs in-place by recovering proper parameter names, types, and return-type annotations from the embedded docstrings. The fixed stubs are useful on their own — IDEs (autocompletion, type checking) and static analysis tools consume them directly, independently of any documentation generation step: -margins = SecurityMargins(problem, factory, robotsAndObjects, robot) -margins.setSecurityMarginBetween("ur3", "sphere", 0.01) -margins.apply() - -planner = ManipulationPlanner(problem, problem.roadmap()) -``` - -`ConstraintGraphFactory` builds a constraint graph (nodes, edges, grippers, handles) declaratively from lists of grippers and objects, instead of assembling `Graph`/`Implicit`/`LockedJoint` objects by hand. `SecurityMargins` then lets you set per-pair collision security margins across the whole graph in one call. - -### Diagnosing constraint errors (`pyhpp.tools`) - -```python -from pyhpp.tools.constraint_error import describe_error - -entries, satisfied = describe_error(config_projector, q) -for entry in entries: - status = "OK" if entry["satisfied"] else "FAILED" - print(f"{entry['name']} ({entry['kind']}): |error| = {entry['norm']:.3g} [{status}]") +```bash +python doc/fix_stubs.py path/to/site-packages/pyhpp/ +# or individual files: +python doc/fix_stubs.py path/to/site-packages/pyhpp/core/bindings.pyi ``` -`pyhpp.tools.xacro.process_xacro(*args)` resolves a xacro file into a URDF string, transparently picking up ROS 2 resource paths from `AMENT_PREFIX_PATH` when available. +Optional JSON override files can supply or correct signatures that cannot be inferred from docstrings alone (`--setter-overrides`, `--method-overrides`). -## Tests +**`doc/gen_api_md.py`** reads the (optionally fixed) stubs and generates one Markdown file per module — suitable for [mdbook](https://rust-lang.github.io/mdBook/) — plus an `index.md`, with syntax-coloured signatures and clickable cross-module type links: -Unit tests (`tests/unit/`) use Python's built-in `unittest` framework, with shared robot/problem fixtures in `tests/unit/conftest.py` (`create_ur5_problem`, `create_ur3_robot`, ...). One test file typically covers one bound module: `test_problem.py`, `test_path_planner.py`, `test_path_optimizer.py`, `test_path_projector.py`, `test_path_validation.py`, `test_steering_method.py`, `test_configuration_shooter.py`, `test_constraint_factory.py`, `test_constraint_graph_factory.py`, `test_security_margins.py`, `test_static_stability.py`, `test_device.py`, `test_handles_grippers.py`, `test_liegroup.py`, `test_differentiable_function.py`, `test_position_constraint.py`, `test_roadmap.py`. - -Integration tests (`tests/integration/`) run complete planning scenarios end to end, e.g. `pr2-in-iai-kitchen.py`, `romeo-placard.py`, `ur3-spheres.py` / `ur3-spheres-spf.py`, and `construction-set-m-rrt.py`, plus `test_benchmarks.py` / `benchmark_utils.py` for timing-oriented benchmarks. - -Run them via CMake (`make test`, requires `BUILD_TESTING=ON`) or directly with `pytest`/`unittest` once the package is on your `PYTHONPATH`. - -## Documentation generation - -Doxygen comments in the upstream C++ libraries can be injected into the Python docstrings exposed by these bindings: - -1. In every dependency's `doc/Doxyfile.extra.in`, set `GENERATE_XML=YES`. -2. Install the generated XML documentation from each dependency's `CMakeLists.txt`: - ```cmake - IF(_INSTALL_DOC) - INSTALL(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc/doxygen-xml - DESTINATION ${CMAKE_INSTALL_DOCDIR}) - ENDIF() - ``` -3. `doc/configure.py` then reads that XML (via `doc/doxygen_xml_parser.py`) and rewrites special in-place comments in the `.cc` binding sources: - - `DocNamespace(namespace)` sets the current C++ namespace (mapped to a Python package via `nsToPackage`). - - `DocClass(classname)` sets the current class being documented. - - `DocClassMethod(methodname, classname=None)` is replaced by the corresponding Doxygen-extracted docstring. - -See the header comment of `doc/configure.py` for the full syntax and a worked example. - -For binding methods that have no upstream `///` Doxygen comment (Python-only wrappers, modified signatures that hide output parameters, etc.), docstrings are written directly in the `.cc` binding source using `const char*` constants in an anonymous namespace placed before `namespace pyhpp {`: - -```cpp -namespace { -const char* DOC_MY_METHOD = "Describe what the method does from the Python caller's perspective."; -} // namespace - -namespace pyhpp { - // ... - .def("myMethod", &MyClass::myMethod, DOC_MY_METHOD) +```bash +python doc/gen_api_md.py \ + --stubs path/to/site-packages \ + --output path/to/docs/api/ ``` -The documentation coverage for each module is tracked in `doc/core_doc_todo.md`, `doc/constraints_doc_todo.md` and `doc/manipulation_doc_todo.md`, using a `✅ / ❌ / ⚠️` legend per method. - -> **TODO** (tracked in the source): use Doxygen to generate XML documentation of the headers included by a given binding file, then use that generated XML to auto-update the documentation comments in `src/pyhpp` (Doxygen's `INCLUDE_PATH` / `SEARCH_INCLUDES` options may help here). +Modules covered: `pyhpp.core`, `pyhpp.core.path`, `pyhpp.core.path_optimization`, `pyhpp.core.problem_target`, `pyhpp.constraints`, `pyhpp.manipulation`, `pyhpp.manipulation.urdf`, `pyhpp.manipulation.steering_method`, `pyhpp.pinocchio`, `pyhpp.pinocchio.urdf`. ## License From 903efae92c3c17d7a731844b1fac17b60856be66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=A9go=20Pintat--Gil?= Date: Tue, 21 Jul 2026 18:51:15 +0200 Subject: [PATCH 13/18] Update README.md Co-authored-by: Guilhem Saurel --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36b643b9..74b4240e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ flowchart LR ## Dependencies -- Python ≥ 3.9, with `numpy` +- Python ≥ 3.10, with `numpy` - [Boost.Python](https://www.boost.org/doc/libs/release/libs/python/) (found via `search_for_boost_python()`) - [`eigenpy`](https://github.com/stack-of-tasks/eigenpy) - [Pinocchio](https://github.com/stack-of-tasks/pinocchio) (C++ and Python) From aa195bdcfef9778bb300f27226c33d17c298742c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Di=C3=A9go=20Pintat--Gil?= Date: Tue, 21 Jul 2026 18:51:22 +0200 Subject: [PATCH 14/18] Update README.md Co-authored-by: Guilhem Saurel --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74b4240e..2b6c7912 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ flowchart LR ### From source with CMake ```bash -git clone --recursive https://github.com/humanoid-path-planner/hpp-python.git +git clone https://github.com/humanoid-path-planner/hpp-python.git mkdir hpp-python/build cd hpp-python/build cmake -DCMAKE_INSTALL_PREFIX= -DCMAKE_BUILD_TYPE=Release .. From 5179f4875b3bc0739959b6016965a44602e51ed8 Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 22 Jul 2026 10:51:05 +0200 Subject: [PATCH 15/18] remove fix_stubs.py file that wan only a redundant entry point, use python -m fix_stubs instead --- doc/fix_stubs.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 doc/fix_stubs.py diff --git a/doc/fix_stubs.py b/doc/fix_stubs.py deleted file mode 100644 index 4cb231a6..00000000 --- a/doc/fix_stubs.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -"""Backward-compatible entry point — logic lives in the fix_stubs/ package. - -Usage (unchanged): - python fix_stubs.py path/to/site-packages/pyhpp/ - python fix_stubs.py some/module/bindings.pyi another.pyi - python fix_stubs.py --setter-overrides overrides.json path/to/pyhpp/ - -Or via the package: - python -m fix_stubs ... -""" - -from fix_stubs._cli import main - -if __name__ == "__main__": - main() From 6279738b0b47adb074c5686d88340182ee90a7ed Mon Sep 17 00:00:00 2001 From: DiegoP-G Date: Wed, 22 Jul 2026 10:52:12 +0200 Subject: [PATCH 16/18] remove internal references from gen_api_md.py --- doc/gen_api_md.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/doc/gen_api_md.py b/doc/gen_api_md.py index 6cdbe533..cc1df883 100644 --- a/doc/gen_api_md.py +++ b/doc/gen_api_md.py @@ -1,12 +1,5 @@ #!/usr/bin/env python3 -"""Generate code-oriented Markdown API reference from pyhpp .pyi stubs (v2). - -Vs gen_api_md.py v1: -- Full Python type annotations with clickable type links, embedded directly in - the code display (HTML
 block) — no separate parameter table needed
-- Per-method sub-headings (### / ####) for sidebar navigation
-- Docstrings shown as blockquotes under each signature
-"""
+"""Generate code-oriented Markdown API reference from pyhpp .pyi stubs."""
 
 from __future__ import annotations
 
@@ -96,7 +89,7 @@ def slug(module_name: str) -> str:
 
 
 # ---------------------------------------------------------------------------
-# Math / Doxygen → KaTeX (identical to v1)
+# Math / Doxygen → KaTeX
 # ---------------------------------------------------------------------------
 
 _MATH_ENV_RE = re.compile(r"\\begin\{(\w+\*?)\}.*?\\end\{\1\}", re.DOTALL)
@@ -164,7 +157,7 @@ def _wrap(m: re.Match[str]) -> str:
 
 
 # ---------------------------------------------------------------------------
-# Docstring parsing (identical to v1)
+# Docstring parsing
 # ---------------------------------------------------------------------------
 
 
@@ -256,6 +249,7 @@ def _parse_stub(stub_path: Path) -> ast.Module | None:
         source = stub_path.read_text()
         source = re.sub(r"ClassVar\[[^\]]*\.\]", "ClassVar[Any]", source)
         source = _KW_PARAM_RE.sub(r"\1\2_\3", source)
+
         return ast.parse(source)
     except Exception:
         return None
@@ -816,7 +810,7 @@ def generate_module_md(
 
 def main() -> None:
     parser = argparse.ArgumentParser(
-        description="Generate code-oriented mdbook Markdown from pyhpp .pyi stubs (v2)"
+        description="Generate code-oriented mdbook Markdown from pyhpp .pyi stubs"
     )
     parser.add_argument(
         "--stubs",

From 38b19167811a77d094c7528df60ef5c89591e25c Mon Sep 17 00:00:00 2001
From: DiegoP-G 
Date: Wed, 22 Jul 2026 10:53:45 +0200
Subject: [PATCH 17/18] fix CmakeList.txt: add BYPRODUCTS and switch t python
 -m fix_stubs

---
 src/CMakeLists.txt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6452bdc8..859abaf3 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -255,14 +255,14 @@ if(GENERATE_PYTHON_STUBS AND PYBIND11_STUBGEN_EXECUTABLE)
 
     add_custom_command(
         OUTPUT ${FIX_STUBS_STAMP}
+        BYPRODUCTS ${PYHPP_ALL_STUB_FILES}
         COMMAND
             ${CMAKE_COMMAND} -E env
             "PYTHONPATH=${CMAKE_SOURCE_DIR}/doc:$ENV{PYTHONPATH}"
-            ${PYTHON_EXECUTABLE} ${CMAKE_SOURCE_DIR}/doc/fix_stubs.py
-            ${STUB_OUTPUT_DIR}
+            ${PYTHON_EXECUTABLE} -m fix_stubs ${STUB_OUTPUT_DIR}
         COMMAND ${CMAKE_COMMAND} -E touch ${FIX_STUBS_STAMP}
         DEPENDS ${PYHPP_ALL_STUB_FILES}
-        COMMENT "Fixing Python stub signatures with fix_stubs.py"
+        COMMENT "Fixing Python stub signatures"
         VERBATIM
     )
 

From c52ae93f97c07a53a3cedafd2c9559e9397519b9 Mon Sep 17 00:00:00 2001
From: DiegoP-G 
Date: Wed, 22 Jul 2026 11:05:12 +0200
Subject: [PATCH 18/18] fix stub pipeline: separate raw and fixed stubs into
 distinct directories

pybind11-stubgen now writes to stubs_raw/, fix_stubs reads from there
and writes fixed stubs to stubs/ via --output-dir. CMake can now track
each step independently with BYPRODUCTS and install().
---
 doc/fix_stubs/_cli.py   | 16 ++++++++++++++++
 doc/fix_stubs/_fixer.py | 10 ++++++++--
 src/CMakeLists.txt      | 42 ++++++++++++++++++++++++++++++-----------
 3 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/doc/fix_stubs/_cli.py b/doc/fix_stubs/_cli.py
index 658b0032..e2e22f07 100644
--- a/doc/fix_stubs/_cli.py
+++ b/doc/fix_stubs/_cli.py
@@ -40,6 +40,15 @@ def main() -> None:
             "Merged on top of the built-in METHOD_OVERRIDES table (external entries win)."
         ),
     )
+    parser.add_argument(
+        "--output-dir",
+        type=Path,
+        default=None,
+        help=(
+            "Write fixed stubs to this directory instead of modifying in-place. "
+            "The input directory structure is preserved relative to each input path."
+        ),
+    )
     args = parser.parse_args()
 
     setter_overrides = load_setter_overrides(args.setter_overrides)
@@ -51,11 +60,18 @@ def main() -> None:
         if not p.exists():
             print(f"warning: path not found, skipping: {p}", file=sys.stderr)
             continue
+        root = p if p.is_dir() else p.parent
         files = sorted(p.rglob("*.pyi")) if p.is_dir() else [p]
         for f in files:
+            output_path = (
+                args.output_dir / f.relative_to(root)
+                if args.output_dir is not None
+                else None
+            )
             try:
                 n = fix_file(
                     f,
+                    output_path=output_path,
                     setter_overrides=setter_overrides,
                     method_overrides=method_overrides,
                 )
diff --git a/doc/fix_stubs/_fixer.py b/doc/fix_stubs/_fixer.py
index fde98b5b..5b57a08b 100644
--- a/doc/fix_stubs/_fixer.py
+++ b/doc/fix_stubs/_fixer.py
@@ -11,10 +11,14 @@
 
 def fix_file(
     path: Path,
+    output_path: Path | None = None,
     setter_overrides: dict[str, str] | None = None,
     method_overrides: dict | None = None,
 ) -> int:
-    """Fix a single .pyi file in-place. Returns the number of signatures fixed."""
+    """Fix a single .pyi file. Returns the number of signatures fixed.
+
+    If output_path is given, writes there; otherwise modifies path in-place.
+    """
     header, tree = extract_tree(
         path,
         setter_overrides=setter_overrides,
@@ -33,6 +37,8 @@ def fix_file(
                 fixed += 1
 
     new_content = render_file(header, tree)
-    path.write_text(new_content)
+    target = output_path if output_path is not None else path
+    target.parent.mkdir(parents=True, exist_ok=True)
+    target.write_text(new_content)
 
     return fixed
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 859abaf3..5fcfa40b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -89,15 +89,16 @@ macro(ADD_PYTHON_LIBRARY MODULE)
 
     if(GENERATE_PYTHON_STUBS AND PYBIND11_STUBGEN_EXECUTABLE)
         string(REPLACE "/" "." MODULE_DOT_PATH "${MODULE}")
-        set(STUB_OUTPUT_DIR ${CMAKE_BINARY_DIR}/stubs)
+        set(RAW_STUB_DIR ${CMAKE_BINARY_DIR}/stubs_raw)
+        set(FIXED_STUB_DIR ${CMAKE_BINARY_DIR}/stubs)
 
         add_custom_command(
-            OUTPUT ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi
+            OUTPUT ${RAW_STUB_DIR}/${MODULE}/${LIBNAME}.pyi
             COMMAND
                 ${CMAKE_COMMAND} -E env
                 "PYTHONPATH=${CMAKE_BINARY_DIR}/src:$ENV{PYTHONPATH}"
                 ${PYBIND11_STUBGEN_EXECUTABLE} ${MODULE_DOT_PATH}.${LIBNAME}
-                --output-dir ${STUB_OUTPUT_DIR}
+                --output-dir ${RAW_STUB_DIR}
             DEPENDS all_pyhpp_bindings
             COMMENT "Generating Python stubs for ${MODULE_DOT_PATH}.${LIBNAME}"
             VERBATIM
@@ -106,11 +107,11 @@ macro(ADD_PYTHON_LIBRARY MODULE)
         add_custom_target(
             stubs_${TARGET_NAME}
             ALL
-            DEPENDS ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi
+            DEPENDS ${RAW_STUB_DIR}/${MODULE}/${LIBNAME}.pyi
         )
 
         install(
-            FILES ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi
+            FILES ${FIXED_STUB_DIR}/${MODULE}/${LIBNAME}.pyi
             DESTINATION ${PYTHON_SITELIB}/${MODULE}
         )
 
@@ -118,7 +119,15 @@ macro(ADD_PYTHON_LIBRARY MODULE)
             GLOBAL
             APPEND
             PROPERTY
-                PYHPP_ALL_STUB_FILES ${STUB_OUTPUT_DIR}/${MODULE}/${LIBNAME}.pyi
+                PYHPP_ALL_RAW_STUB_FILES
+                    ${RAW_STUB_DIR}/${MODULE}/${LIBNAME}.pyi
+        )
+        set_property(
+            GLOBAL
+            APPEND
+            PROPERTY
+                PYHPP_ALL_FIXED_STUB_FILES
+                    ${FIXED_STUB_DIR}/${MODULE}/${LIBNAME}.pyi
         )
     endif()
 endmacro()
@@ -249,19 +258,30 @@ python_install_on_site(pyhpp/tools xacro.py)
 python_install_on_site(pyhpp/tools constraint_error.py)
 
 if(GENERATE_PYTHON_STUBS AND PYBIND11_STUBGEN_EXECUTABLE)
-    get_property(PYHPP_ALL_STUB_FILES GLOBAL PROPERTY PYHPP_ALL_STUB_FILES)
-    set(STUB_OUTPUT_DIR ${CMAKE_BINARY_DIR}/stubs)
+    get_property(
+        PYHPP_ALL_RAW_STUB_FILES
+        GLOBAL
+        PROPERTY PYHPP_ALL_RAW_STUB_FILES
+    )
+    get_property(
+        PYHPP_ALL_FIXED_STUB_FILES
+        GLOBAL
+        PROPERTY PYHPP_ALL_FIXED_STUB_FILES
+    )
+    set(RAW_STUB_DIR ${CMAKE_BINARY_DIR}/stubs_raw)
+    set(FIXED_STUB_DIR ${CMAKE_BINARY_DIR}/stubs)
     set(FIX_STUBS_STAMP ${CMAKE_BINARY_DIR}/fix_stubs.stamp)
 
     add_custom_command(
         OUTPUT ${FIX_STUBS_STAMP}
-        BYPRODUCTS ${PYHPP_ALL_STUB_FILES}
+        BYPRODUCTS ${PYHPP_ALL_FIXED_STUB_FILES}
         COMMAND
             ${CMAKE_COMMAND} -E env
             "PYTHONPATH=${CMAKE_SOURCE_DIR}/doc:$ENV{PYTHONPATH}"
-            ${PYTHON_EXECUTABLE} -m fix_stubs ${STUB_OUTPUT_DIR}
+            ${PYTHON_EXECUTABLE} -m fix_stubs --output-dir ${FIXED_STUB_DIR}
+            ${RAW_STUB_DIR}
         COMMAND ${CMAKE_COMMAND} -E touch ${FIX_STUBS_STAMP}
-        DEPENDS ${PYHPP_ALL_STUB_FILES}
+        DEPENDS ${PYHPP_ALL_RAW_STUB_FILES}
         COMMENT "Fixing Python stub signatures"
         VERBATIM
     )