diff --git a/README.md b/README.md index caae061f..2b6c7912 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,98 @@ -# 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 + +- [Module dependency graph](#module-dependency-graph) +- [Dependencies](#dependencies) +- [Installation](#installation) + - [From source with CMake](#from-source-with-cmake) + - [With Nix](#with-nix) +- [Documentation generation](#documentation-generation) + - [Injecting Doxygen docstrings into bindings](#injecting-doxygen-docstrings-into-bindings) + - [API reference from stubs](#api-reference-from-stubs) +- [License](#license) + + +## 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 + 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 +``` + +## Dependencies + +- 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) +- 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 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 +``` + +## Documentation generation + +### 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 @@ -33,8 +108,30 @@ ENDIF() File `doc/configure.py` contains a short documentation of how to document the bindings. -### TODO +### API reference from stubs + +After building and installing `pyhpp`, `pybind11-stubgen` produces `.pyi` stub files under the Python site-packages directory. + +**`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: + +```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 +``` + +Optional JSON override files can supply or correct signatures that cannot be inferred from docstrings alone (`--setter-overrides`, `--method-overrides`). + +**`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: + +```bash +python doc/gen_api_md.py \ + --stubs path/to/site-packages \ + --output path/to/docs/api/ +``` + +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 -- 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. 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/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/_cli.py b/doc/fix_stubs/_cli.py new file mode 100644 index 00000000..e2e22f07 --- /dev/null +++ b/doc/fix_stubs/_cli.py @@ -0,0 +1,88 @@ +"""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)." + ), + ) + 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) + 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 + 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, + ) + 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..5b57a08b --- /dev/null +++ b/doc/fix_stubs/_fixer.py @@ -0,0 +1,44 @@ +"""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, + output_path: Path | None = None, + setter_overrides: dict[str, str] | None = None, + method_overrides: dict | None = None, +) -> int: + """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, + 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) + 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/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..9fd29878 --- /dev/null +++ b/doc/fix_stubs/_models.py @@ -0,0 +1,202 @@ +"""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.nested_classes: list["ClassI"] = [] + 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 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("") + 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..7f9c5324 --- /dev/null +++ b/doc/fix_stubs/_parser.py @@ -0,0 +1,520 @@ +"""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, + DEF_SELF_FIRST_RE, + DOCSTRING_OPEN_RE, + NESTED_ARG1_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 _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, + 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) + + 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 new file mode 100644 index 00000000..bcd40624 --- /dev/null +++ b/doc/fix_stubs/_patterns.py @@ -0,0 +1,54 @@ +"""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_]*)$" +) + +# 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", + "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/doc/gen_api_md.py b/doc/gen_api_md.py new file mode 100644 index 00000000..cc1df883 --- /dev/null +++ b/doc/gen_api_md.py @@ -0,0 +1,859 @@ +#!/usr/bin/env python3 +"""Generate code-oriented Markdown API reference from pyhpp .pyi stubs.""" + +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 +) +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-") + + +# --------------------------------------------------------------------------- +# Math / Doxygen → KaTeX +# --------------------------------------------------------------------------- + +_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: + 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"(? 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 +# --------------------------------------------------------------------------- + + +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 parse_overloads(raw: str | None) -> list[tuple[str | None, str]]: + 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: + if not overloads: + return "" + if len(overloads) == 1: + 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): + 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) + + +# --------------------------------------------------------------------------- +# 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 ( + body + and isinstance(body[0], ast.Expr) + and isinstance(body[0].value, ast.Constant) + and isinstance(body[0].value.value, str) + ): + return body[0].value.value + return None + + +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 _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" + + +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 = (
+            ast.unparse(b)
+            if isinstance(b, ast.Attribute)
+            else (b.id if isinstance(b, ast.Name) else "")
+        )
+        if not raw:
+            continue
+        class_name = raw.split(".")[-1]
+        if class_name in SKIP_BASES:
+            continue
+        # 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:
+            links.append(f"[`{raw}`]({page}.md#{class_name.lower()})")
+        else:
+            link = _type_link(raw, class_index, current_page)
+            links.append(link)
+    return links
+
+
+# ---------------------------------------------------------------------------
+# Body grouping — consecutive same-name FunctionDefs become one overload group
+# ---------------------------------------------------------------------------
+
+type BodyGroup = list[ast.FunctionDef] | ast.ClassDef
+
+
+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
+
+
+# ---------------------------------------------------------------------------
+# 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) + + 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, + 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, class_index, current_page) + heading = "#" * heading_level + + groups = _group_body(class_node.body) + + method_rows: list[tuple[str, str]] = [] + nested_class_lines: list[str] = [] + + 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 meta: + lines.append(" ".join(meta) + "\n") + + if raw_doc: + 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 method_rows: + lines.append("| def | Description |") + lines.append("|:---|:---|") + for sig, desc in method_rows: + lines.append(f"| {sig} | {desc} |") + lines.append("") + + if nested_class_lines: + lines.extend(nested_class_lines) + + if heading_level == 2: + lines.append("---\n") + + return lines, True + + +# --------------------------------------------------------------------------- +# Module page generation +# --------------------------------------------------------------------------- + + +def generate_module_md( + 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 + + # 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): + class_lines, ok = render_class( + item, class_index, current_page, stub_module_index + ) + if ok: + lines.extend(class_lines) + has_content = True + + if not has_content: + lines.append("*No documented symbols in this module.*\n") + + return "\n".join(lines) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Generate code-oriented mdbook Markdown 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) + + 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: + 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, class_index, stub_module_index) + ) + generated.append((module_name, slug(module_name), description)) + print(f" {out_file.name}") + + 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_lines.append(f"| [`{mod}`]({s}.md) | {desc} |\n") + (args.output / "index.md").write_text("".join(index_lines)) + print(" index.md") + + +if __name__ == "__main__": + main() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8a59d01..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,13 +107,28 @@ 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} ) + + set_property( + GLOBAL + APPEND + PROPERTY + 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() @@ -240,3 +256,35 @@ 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_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_FIXED_STUB_FILES} + COMMAND + ${CMAKE_COMMAND} -E env + "PYTHONPATH=${CMAKE_SOURCE_DIR}/doc:$ENV{PYTHONPATH}" + ${PYTHON_EXECUTABLE} -m fix_stubs --output-dir ${FIXED_STUB_DIR} + ${RAW_STUB_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${FIX_STUBS_STAMP} + DEPENDS ${PYHPP_ALL_RAW_STUB_FILES} + COMMENT "Fixing Python stub signatures" + VERBATIM + ) + + add_custom_target(fix_stubs ALL DEPENDS ${FIX_STUBS_STAMP}) +endif() diff --git a/src/pyhpp/constraints/by-substitution.cc b/src/pyhpp/constraints/by-substitution.cc index 30113e49..10b0153e 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 { @@ -99,37 +123,49 @@ void exposeBySubstitution() { // DocClass(BySubstitution) class_ >( - "BySubstitution", init()) + "BySubstitution", DocClassDoc(), init()) .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), 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)) - .def("describeError", &BySubstitution_describeError); + &BySubstitution::errorThreshold), + DOC_BS_ERRORTHRESHOLD) + .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 27eecfd7..0c32e02a 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 { @@ -102,18 +114,21 @@ void exposeDifferentiableFunction() { // class_ class_( - "DifferentiableFunction", no_init) + "DifferentiableFunction", DocClassDoc(), 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)) @@ -155,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-constraint-set.cc b/src/pyhpp/constraints/explicit-constraint-set.cc index b1c858c3..053317b3 100644 --- a/src/pyhpp/constraints/explicit-constraint-set.cc +++ b/src/pyhpp/constraints/explicit-constraint-set.cc @@ -42,11 +42,12 @@ using namespace hpp::constraints; void exposeExplicitConstraintSet() { // DocClass(ExplicitConstraintSet) - class_("ExplicitConstraintSet", + class_("ExplicitConstraintSet", DocClassDoc(), 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/explicit.cc b/src/pyhpp/constraints/explicit.cc index 68bf9ae0..ba8a1c37 100644 --- a/src/pyhpp/constraints/explicit.cc +++ b/src/pyhpp/constraints/explicit.cc @@ -64,8 +64,10 @@ ExplicitPtr_t createExplicit(const LiegroupSpacePtr_t& configSpace, void exposeExplicit() { // DocClass(Explicit) - class_("Explicit", no_init) - .def("__init__", make_constructor(&createExplicit)); + class_("Explicit", DocClassDoc(), + no_init) + .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 a5cd89c0..91a39744 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 { @@ -56,22 +61,27 @@ void exposeImplicit() { .value("Superior", Superior) .value("Inferior", Inferior); // DocClass(Implicit) - class_("Implicit", no_init) - .def("__init__", make_constructor(&Implicit::create)) + class_("Implicit", DocClassDoc(), + no_init) + .def("__init__", make_constructor(&Implicit::create), + "Create an implicit constraint from a differentiable function and " + "comparison types.") .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, 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 e7fe30a1..ee7cca81 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 { @@ -63,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)) @@ -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( @@ -105,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..ed5f840f 100644 --- a/src/pyhpp/constraints/locked-joint.cc +++ b/src/pyhpp/constraints/locked-joint.cc @@ -64,10 +64,14 @@ LockedJointPtr_t createLockedJointWithComp(const DevicePtr_t& robot, } void exposeLockedJoint() { + // DocClass(LockedJoint) class_, LockedJointPtr_t, boost::noncopyable>( - "LockedJoint", no_init) - .def("__init__", make_constructor(&createLockedJoint)) - .def("__init__", make_constructor(&createLockedJointWithComp)); + "LockedJoint", DocClassDoc(), no_init) + .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/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 6f2c1a8c..3411a43e 100644 --- a/src/pyhpp/core/config-validation.cc +++ b/src/pyhpp/core/config-validation.cc @@ -58,18 +58,20 @@ 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); + .def("validate", &CVWrapper::py_validate, + "Validate configuration; returns (valid, report)."); // 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)) - .def("clear", &ConfigValidations::clear); + .def("clear", &ConfigValidations::clear, + "Remove all config validations."); } } // namespace core } // namespace pyhpp 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 399b5e13..2e1778c9 100644 --- a/src/pyhpp/core/connected-component.cc +++ b/src/pyhpp/core/connected-component.cc @@ -77,12 +77,14 @@ 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)) .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 b9c3b3a4..a68e0f69 100644 --- a/src/pyhpp/core/constraint.cc +++ b/src/pyhpp/core/constraint.cc @@ -38,6 +38,33 @@ // 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."; +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 { @@ -94,18 +121,19 @@ 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)) - .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_ >("ConstraintSet", no_init) + bases >("ConstraintSet", DocClassDoc(), no_init) .def("__init__", make_constructor(&createConstraintSet)) .def("addConstraint", &ConstraintSet::addConstraint, DocClassMethod(addConstraint)) @@ -119,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( @@ -131,29 +159,42 @@ 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("lastIsOptional", + static_cast( + &ConfigProjector::lastIsOptional), + DOC_CP_LASTISOPTIONAL) + .def("lastIsOptional", + static_cast( + &ConfigProjector::lastIsOptional), + DOC_CP_LASTISOPTIONAL_SET) + .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) - .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..b5c33c0b 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 { @@ -58,16 +67,18 @@ 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) - .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 1ffc5c56..7b067ce3 100644 --- a/src/pyhpp/core/node.cc +++ b/src/pyhpp/core/node.cc @@ -45,17 +45,20 @@ 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)) .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)) + &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/parameter.cc b/src/pyhpp/core/parameter.cc index f20525d8..b0120bde 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 { @@ -83,17 +97,17 @@ 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) + .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 ad1b65ca..47d9a455 100644 --- a/src/pyhpp/core/path-optimizer.cc +++ b/src/pyhpp/core/path-optimizer.cc @@ -43,6 +43,45 @@ // 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."; +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; namespace pyhpp { @@ -56,23 +95,31 @@ 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() { // 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)) @@ -106,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_, @@ -129,17 +179,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..32526847 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 { @@ -145,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)) @@ -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/path-projector.cc b/src/pyhpp/core/path-projector.cc index c6678db2..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,29 +57,74 @@ struct PPWrapper { return boost::python::make_tuple(res, projPath); } }; - void exposePathProjector() { // DocClass(PathProjector) - class_("PathProjector", - no_init) + class_( + "PathProjector", DocClassDoc(), 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>( - "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", @@ -86,47 +132,8 @@ void exposePathProjector() { const value_type&) -> PathProjectorPtr_t { return PathProjectorPtr_t(); }, - (arg("distance"), arg("steeringMethod"), arg("step"))); - - 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"))); - - 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"))); - - 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"))); - - 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"))); + (arg("distance"), arg("steeringMethod"), arg("step")), + "Return a null path projector (no projection)."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path-validation.cc b/src/pyhpp/core/path-validation.cc index b46db98e..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,41 +79,62 @@ 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) - .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>( - "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"))); 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"))); - def("Progressive", &continuousValidation::Progressive::create, - (arg("robot"), arg("tolerance"))); - def("Dichotomy", &continuousValidation::Dichotomy::create, - (arg("robot"), arg("tolerance"))); + (arg("robot"), arg("stepSize")), + "Create a discretized path validation checking both collision and joint " + "bounds."); } } // namespace core } // namespace pyhpp diff --git a/src/pyhpp/core/path.cc b/src/pyhpp/core/path.cc index 8110fc8b..b2e4969e 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 { @@ -144,14 +155,15 @@ 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) - .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 +182,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-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 b6b6ee07..474b4aaa 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 { @@ -604,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)) @@ -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/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 3e846343..25032c19 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 { @@ -221,26 +260,27 @@ 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)) .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 +293,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 e2e3532d..f8f33424 100644 --- a/src/pyhpp/core/steering-method.cc +++ b/src/pyhpp/core/steering-method.cc @@ -143,8 +143,10 @@ const ConstraintSetPtr_t& SteeringMethod::constraints() const { void exposeSteeringMethod() { register_ptr_to_python>(); // DocClass(SteeringMethod) - class_("SteeringMethod", no_init) - .def("__call__", &SteeringMethod::operator()) + class_("SteeringMethod", DocClassDoc(), no_init) + .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", @@ -154,7 +156,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..cb74fde1 100644 --- a/src/pyhpp/manipulation/device.cc +++ b/src/pyhpp/manipulation/device.cc @@ -35,6 +35,30 @@ // 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 = + "mask( (Handle)arg1) -> list:\n" + "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; @@ -272,18 +296,20 @@ static JointIndex getParentJointId(const HandlePtr_t& handle) { void exposeHandle() { // DocClass(Handle) - class_("Handle", no_init) - .add_property("name", &getHandleName, &setHandleName) + class_("Handle", DocClassDoc(), no_init) + .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" @@ -318,14 +344,22 @@ void exposeDevice() { // DocClass(Device) class_, boost::shared_ptr, - boost::noncopyable>("Device", init()) - .def("setRobotRootPosition", &Device::setRobotRootPosition) - .def("handles", &Device::handles) - .def("grippers", &Device::grippers) + 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.") + .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..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,16 +1310,33 @@ void exposeGraph() { // DocClass(Graph) class_( - "Graph", + "Graph", DocClassDoc(), 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 561ad9a5..d4c2d522 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 { @@ -302,20 +320,28 @@ void exposePathPlanners() { // DocClass(TransitionPlanner) boost::python::class_>( - "TransitionPlanner", boost::python::init()) + "TransitionPlanner", DocClassDoc(), + 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, 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, @@ -346,22 +372,26 @@ void exposePathPlanners() { // DocClass(EndEffectorTrajectory) boost::python::class_>( - "EndEffectorTrajectory", + "EndEffectorTrajectory", DocClassDoc(), 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 237066b8..57856cff 100644 --- a/src/pyhpp/manipulation/problem.cc +++ b/src/pyhpp/manipulation/problem.cc @@ -42,6 +42,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 { @@ -103,11 +110,6 @@ pyhpp::core::PyWSteeringMethodPtr_t Problem::steeringMethod() const { return std::shared_ptr(sm); } -void Problem::graphSteeringMethod( - const PyWGraphSteeringMethodPtr_t& steeringMethod) { - obj->steeringMethod(steeringMethod->obj); -} - // PathValidationPtr_t Problem::pathValidation() const { // return obj->pathValidation(); // } @@ -144,23 +146,33 @@ void Problem::graphSteeringMethod( void exposeProblem() { // DocClass(Problem) - class_>("Problem", + class_>("Problem", DocClassDoc(), 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", 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::graphSteeringMethod)) - .def("fullSteeringMethod", &Problem::fullSteeringMethod) + const pyhpp::core::PyWSteeringMethodPtr_t&)>( + &Problem::steeringMethod), + "Set the steering method.") + + .def("fullSteeringMethod", &Problem::fullSteeringMethod, + "Set the problem steering method directly. Unlike steeringMethod, " + "this does not wrap the given steering method in a manipulation " + "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/problem.hh b/src/pyhpp/manipulation/problem.hh index 8dfb5905..d2fbf0bc 100644 --- a/src/pyhpp/manipulation/problem.hh +++ b/src/pyhpp/manipulation/problem.hh @@ -60,7 +60,6 @@ struct Problem : public pyhpp::core::Problem { void steeringMethod( const pyhpp::core::PyWSteeringMethodPtr_t& steeringMethod); pyhpp::core::PyWSteeringMethodPtr_t steeringMethod() const; - void graphSteeringMethod(const PyWGraphSteeringMethodPtr_t& steeringMethod); void fullSteeringMethod( const pyhpp::core::PyWSteeringMethodPtr_t& steeringMethod); // PathValidationPtr_t pathValidation() const; 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): 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 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/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 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)