mirror of
https://github.com/huggingface/lerobot.git
synced 2026-08-08 17:39:44 +00:00
2e8345a5cc
LeRobot's documentation build passes `--not_python_module`, which tells doc-builder there is no importable Python package and disables `[[autodoc]]` entirely. The result is that all 90+ pages are hand-written guides and there is no generated API reference at all. This is the machinery to change that. It deliberately contains no docstring changes of its own — every docstring edit lives in the follow-up PR, so this one can be reviewed as tooling and configuration alone. **The standard.** `docs/source/writing_docstrings.mdx` is the contract: Google section headers with Hugging Face type formatting, the machine-checked argument line, `**Attributes**:`, doc-builder cross-references, fenced doctest examples. It also records three behaviours that are not discoverable from the source and were verified against a local build: `[[autodoc]]` silently skips members with no docstring; doc-builder does not inherit docstrings from base classes, so a registered config shim whose body is `pass` renders every field with no description; and module-level aliases resolve to the canonical class. **Autodoc turned on**, with two changes that are not obvious: - `--version main` on the main-docs job. Without `--not_python_module`, doc-builder resolves the version from `lerobot.__version__` and only maps it to the default branch when it contains "dev". transformers relies on that; our main carries 0.6.2. Verified by building both ways — dropping the flag alone would publish the main docs to /lerobot/v0.6.2/ instead of /lerobot/main/ and disable notebook building. - `pre_command` on both jobs. doc-builder ships a mock-deps registry entry for lerobot, so the reusable workflow takes its light-install path, which cannot import the package. The heavy dependencies cannot be mocked either: draccus runs `register_subclass` at import time and `processor/converters.py` calls `functools.singledispatch.register(torch.Tensor)`, which needs a real class. `[dataset]` is the only extra required. Workflow triggers gain `src/**`, since the reference is now generated from docstrings. `docs/source/api/` is excluded from the prettier hook, which reads `[[autodoc]]` member lists as lazy paragraph continuations and joins a ten-entry list onto one line. Nine API reference pages, scaffolded with each module's base class. **Doctests.** `LeRobotDocTestParser` is mandatory rather than optional here: ruff's `docstring-code-format = true` drops the blank line before a closing fence, after which stdlib's `_EXAMPLE_RE` reads the fence as expected output and every example with output fails. It is written against the installed pytest rather than copied from transformers, whose version predates pytest 9's `import_path` signature and its own fix for the `@property` line-number bug. `preprocess_string` also diverges: the upstream fenced-block split puts a single-line example's code in a chunk with no `>>>` in it, so neither the CUDA skip nor the `+IGNORE_RESULT` injection fires for it. **Checkers.** `utils/check_docstrings.py` is the ~300-line core of the 2203-line transformers original; the `@auto_docstring` system, modular propagation, GitPython and `checkers.py` are not ported. `utils/check_config_docstrings.py` checks that every registered robot config documents its port and calibration semantics. **Gates**, all set to values that pass today: ruff `D` with per-file-ignores per unconverted module, `interrogate` at `fail-under = 52` against a measured 52.1%, and Makefile targets wired into the quality workflow. The doctest allowlist ships empty and the `doctest` target handles that, because the files carrying runnable examples arrive with the docstring PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
288 lines
12 KiB
Plaintext
288 lines
12 KiB
Plaintext
# Writing docstrings
|
|
|
|
LeRobot's API reference is generated directly from the docstrings in `src/lerobot/`. A docstring is not a
|
|
comment — it is the published documentation for that object, and the format below is what the renderer and
|
|
the CI checks parse.
|
|
|
|
This page is the contract. If you are adding or editing anything public in `src/lerobot/`, follow it.
|
|
|
|
> [!IMPORTANT]
|
|
> **An undocumented public method is an invisible one.** `[[autodoc]]` silently skips members that have no
|
|
> docstring — no warning, no error, it simply does not appear on the rendered page. Coverage and
|
|
> API-reference completeness are the same problem.
|
|
|
|
## The format in one example
|
|
|
|
Google section headers, Hugging Face type formatting. Both, not one or the other.
|
|
|
|
````python
|
|
def send_action(self, action: RobotAction, rate_hz: float = 30.0) -> RobotAction:
|
|
"""Command the robot to move to a target joint configuration.
|
|
|
|
Values are clipped by the configured maximum relative target before reaching the motors, so the
|
|
returned action may differ from the requested one.
|
|
|
|
Args:
|
|
action (`dict[str, float]`):
|
|
Target values keyed by motor name, e.g. `{"shoulder_pan.pos": 0.0}`. Keys must match the
|
|
robot's action features.
|
|
rate_hz (`float`, *optional*, defaults to `30.0`):
|
|
Control loop frequency.
|
|
|
|
Returns:
|
|
`dict[str, float]`: The action actually written to the motors after safety clipping.
|
|
|
|
Raises:
|
|
DeviceNotConnectedError: If the robot has not been connected.
|
|
|
|
Example:
|
|
```python
|
|
>>> from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig
|
|
>>> robot = SO101Follower(SO101FollowerConfig(port="/dev/ttyACM0")) # doctest: +SKIP
|
|
>>> robot.connect() # doctest: +SKIP
|
|
>>> robot.send_action({"shoulder_pan.pos": 0.0}) # doctest: +SKIP
|
|
```
|
|
"""
|
|
````
|
|
|
|
Cross-references are omitted from the examples on this page — see [Cross-references](#cross-references) for
|
|
their syntax and why they cannot be shown inside a code block.
|
|
|
|
## Rules
|
|
|
|
### Sections
|
|
|
|
`Args:` · `Returns:` · `Raises:` · `Yields:` · `Example:` · `Note:`
|
|
|
|
In that order. No other section headers. A one-line summary comes first, then an optional free-form
|
|
description, then the sections.
|
|
|
|
### The `Args:` line is machine-parsed
|
|
|
|
```
|
|
name (`type`, *optional*, defaults to `X`):
|
|
Description, indented on its own line.
|
|
```
|
|
|
|
The `*optional*, defaults to` clause is **checked against the real signature default** by
|
|
`make check-docstrings`. It is not decorative — if you write a default that has drifted from the code, CI
|
|
fails. Omit the clause entirely for required parameters:
|
|
|
|
```python
|
|
Args:
|
|
port (`str`):
|
|
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
|
|
max_relative_target (`float | dict[str, float]`, *optional*):
|
|
Caps the magnitude of the relative positional target vector. `None` disables clipping.
|
|
use_degrees (`bool`, *optional*, defaults to `True`):
|
|
Keep `True` for backward compatibility with existing policies and datasets.
|
|
```
|
|
|
|
Types go in backticks. Use `*optional*` with no `defaults to` when the default is `None` or is otherwise not
|
|
worth restating.
|
|
|
|
### `Returns:` is type-first
|
|
|
|
One indented line, type first, then a colon, then the description:
|
|
|
|
```python
|
|
Returns:
|
|
`dict[str, float]`: The action actually written to the motors after safety clipping.
|
|
```
|
|
|
|
`Yields:` takes the same shape.
|
|
|
|
### `**Attributes**:`, never `Attributes:`
|
|
|
|
doc-builder parses a bare `Attributes:` as a **synonym for `Parameters:`**, so your attributes get rendered
|
|
as constructor arguments. This is silent and wrong. Whenever the attributes differ from the constructor
|
|
parameters, use the bold form with a `--` separator:
|
|
|
|
```python
|
|
class Robot(abc.ABC):
|
|
"""The base abstract class for all LeRobot-compatible robots.
|
|
|
|
**Attributes**:
|
|
- **config_class** (`type[RobotConfig]`) -- The expected configuration class for this robot.
|
|
- **name** (`str`) -- The unique robot name used to identify this robot type.
|
|
"""
|
|
```
|
|
|
|
Note `--`, not `:`.
|
|
|
|
### Cross-references
|
|
|
|
Use doc-builder's bracket syntax: a square-bracketed backtick-quoted path. **Sphinx roles (`:pymeth:`,
|
|
`:pyattr:`) are not supported** and render as literal text on the page.
|
|
|
|
| Want | Write |
|
|
| ---------------------------- | ----------------------------------- |
|
|
| Class in the main package | [`Robot`] |
|
|
| Method, show the full path | [`Robot.connect`] |
|
|
| Method, show the bare name | [`~Robot.connect`] |
|
|
| Nested path | [`~robots.Robot.connect`] |
|
|
| Object in another HF library | [`~accelerate.Accelerator`] |
|
|
|
|
The `~` strips the path from the **link text only**; the link still resolves to the full path.
|
|
|
|
> [!NOTE]
|
|
> doc-builder resolves this syntax everywhere in a page — including inside fenced code blocks. That is why
|
|
> the docstring examples on this page use plain prose instead of cross-references: a code block containing
|
|
> one would render the resolved link rather than the syntax you need to type. In your own docstrings, use
|
|
> cross-references freely; this restriction only affects documentation _about_ the syntax.
|
|
|
|
### Callouts
|
|
|
|
Use GitHub-style blockquotes:
|
|
|
|
```markdown
|
|
> [!TIP]
|
|
> Call this once at startup — it takes about two seconds.
|
|
|
|
> [!WARNING]
|
|
> Torque is disabled on disconnect. The arm will drop if it is holding a load.
|
|
```
|
|
|
|
The `<Tip>` component is legacy per doc-builder; don't add new ones.
|
|
|
|
### Examples must be fenced
|
|
|
|
An example lives inside a fenced ` ```python ` block containing `>>> `. The fence is what makes it render
|
|
as a code block, and it is what the doctest preprocessor's regex looks for:
|
|
|
|
````python
|
|
Example:
|
|
```python
|
|
>>> from lerobot.robots.so_follower import SO101FollowerConfig
|
|
>>> cfg = SO101FollowerConfig(port="/dev/ttyACM0")
|
|
>>> cfg.use_degrees
|
|
True
|
|
```
|
|
````
|
|
|
|
> [!WARNING]
|
|
> An unfenced `>>>` is still collected — doctest finds prompts anywhere in a docstring. What you lose is the
|
|
> rendering, so it shows up as a wall of prose on the page. Every example needs the fence.
|
|
|
|
Every example either executes in CI or carries `# doctest: +SKIP`. Anything that touches hardware, a GPU, or
|
|
downloads from the Hub gets `+SKIP`:
|
|
|
|
````python
|
|
Example:
|
|
```python
|
|
>>> robot.connect() # doctest: +SKIP
|
|
>>> policy = ACTPolicy.from_pretrained("lerobot/act_aloha_sim_transfer_cube_human") # doctest: +SKIP
|
|
```
|
|
````
|
|
|
|
Add files containing runnable examples to `utils/documentation_tests.txt`.
|
|
|
|
Put examples on the three to five genuine entry points of a module. Examples on trivial accessors are noise.
|
|
|
|
## Three patterns you will hit constantly
|
|
|
|
### Config dataclasses
|
|
|
|
Configuration fields are historically documented with `#` comments above each field. **doc-builder cannot
|
|
see inline comments** — such a class renders with every field listed and not a single description. Move them
|
|
into an `Args:` block on the class docstring:
|
|
|
|
```python
|
|
@dataclass
|
|
class SOFollowerConfig:
|
|
"""Configuration for SO-family follower arms.
|
|
|
|
Args:
|
|
port (`str`):
|
|
Serial port the arm is connected to, e.g. `/dev/ttyACM0`.
|
|
max_relative_target (`float | dict[str, float]`, *optional*):
|
|
Caps the magnitude of the relative positional target vector. A scalar applies to all motors;
|
|
a dict maps motor name to a per-motor cap. `None` disables clipping.
|
|
use_degrees (`bool`, *optional*, defaults to `True`):
|
|
Keep `True` for backward compatibility with existing policies and datasets.
|
|
"""
|
|
|
|
port: str
|
|
max_relative_target: float | dict[str, float] | None = None
|
|
use_degrees: bool = True
|
|
```
|
|
|
|
> [!IMPORTANT]
|
|
> **doc-builder does not inherit docstrings from base classes.** LeRobot's registered config classes are
|
|
> often thin multiple-inheritance shims:
|
|
>
|
|
> ```python
|
|
> @RobotConfig.register_subclass("so101_follower")
|
|
> @dataclass
|
|
> class SOFollowerRobotConfig(RobotConfig, SOFollowerConfig):
|
|
> pass
|
|
> ```
|
|
>
|
|
> That class renders **every** field — including the ones it inherits — with no descriptions at all, no
|
|
> matter how well the bases are documented. The `Args:` block must live on the concrete class that
|
|
> `[[autodoc]]` names, and it must cover inherited fields too.
|
|
|
|
### Base class, then concrete subclass
|
|
|
|
The abstract base carries the canonical contract. Subclasses document only what deviates — port semantics,
|
|
calibration quirks, motor layout, supported feature keys. Do not copy the base contract into every subclass.
|
|
|
|
`Robot`, `Teleoperator`, `Camera`, `MotorsBus`, `ProcessorStep`, and `PreTrainedPolicy` all follow this
|
|
shape.
|
|
|
|
### Module-level aliases
|
|
|
|
Several public names are aliases rather than distinct classes:
|
|
|
|
```python
|
|
SO100FollowerConfig = SOFollowerRobotConfig
|
|
SO101FollowerConfig = SOFollowerRobotConfig
|
|
```
|
|
|
|
`[[autodoc]]` resolves the alias and renders the **canonical** class name, so a `## SO101FollowerConfig`
|
|
heading will show `class lerobot.robots.so_follower.SOFollowerRobotConfig` in the body. Document the
|
|
canonical class once, and mention the aliases in the page's prose rather than giving each alias its own
|
|
autodoc block.
|
|
|
|
## What not to document
|
|
|
|
- **Private members.** Anything starting with `_` is not part of the public API.
|
|
- **The type annotation restated as prose.** `port (`str`): A string.` adds nothing. Say what it is for.
|
|
- **Vendored upstream code.** `src/lerobot/policies/molmoact2/molmoact2_hf_model/` is vendored from
|
|
`transformers` and already carries upstream-style docstrings. Leave it alone — restyling it only creates
|
|
conflicts on the next sync. It is excluded from the API reference and from the docstring checks.
|
|
|
|
## How this is enforced
|
|
|
|
| Check | What it catches |
|
|
| ------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
| `make check-docstrings` | An `Args:` entry that doesn't match the signature; a documented default that has drifted from the real one |
|
|
| `make doctest` | Examples that no longer run |
|
|
| `make check-doctest-list` | Stale or unsorted entries in `utils/documentation_tests.txt` |
|
|
| `ruff` (`D` rules) | Google-convention style violations |
|
|
| `interrogate` | Docstring coverage falling below the current threshold |
|
|
| doc-builder | A `[[autodoc]]` path that points at something that doesn't exist — this breaks the docs build |
|
|
|
|
Run them together before opening a PR:
|
|
|
|
```bash
|
|
make check-docstrings && make doctest && pre-commit run --all-files
|
|
```
|
|
|
|
Then render the page and actually look at it:
|
|
|
|
```bash
|
|
doc-builder build lerobot docs/source/ --build_dir /tmp/doc-build
|
|
```
|
|
|
|
## Checklist
|
|
|
|
- [ ] Every public member you touched has a docstring.
|
|
- [ ] Every `Args:` entry matches the signature, including the `*optional*, defaults to` clause.
|
|
- [ ] `Returns:` is type-first on one indented line.
|
|
- [ ] No bare `Attributes:` — use `**Attributes**:` with `--` separators.
|
|
- [ ] No Sphinx roles — cross-references use [`~module.Class.method`].
|
|
- [ ] Examples are inside a fenced ` ```python ` block, and either run in CI or carry `# doctest: +SKIP`.
|
|
- [ ] Config dataclass fields are in an `Args:` block on the concrete class, not `#` comments.
|
|
- [ ] The rendered page has been eyeballed.
|