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>
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Keep the doctest list honest: every path exists, and the file stays sorted.
|
|
|
|
Adapted from `transformers/utils/check_doctest_list.py`. It is agnostic to whether the list is an allowlist
|
|
(what we have now) or a denylist (where transformers ended up), so it survives that inversion unchanged.
|
|
|
|
Check, as CI does:
|
|
|
|
```bash
|
|
python utils/check_doctest_list.py
|
|
```
|
|
|
|
Sort in place:
|
|
|
|
```bash
|
|
python utils/check_doctest_list.py --fix_and_overwrite
|
|
```
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_PATH = Path(__file__).resolve().parent.parent
|
|
DOCTEST_FILE_PATHS = ["documentation_tests.txt"]
|
|
|
|
|
|
def split_header(lines: list[str]) -> tuple[list[str], list[str]]:
|
|
"""Split a list file into its leading comment header and its path entries.
|
|
|
|
Args:
|
|
lines (`list[str]`):
|
|
The file's lines, without trailing newlines.
|
|
|
|
Returns:
|
|
`tuple[list[str], list[str]]`: The leading comment/blank lines, and the remaining lines.
|
|
"""
|
|
for i, line in enumerate(lines):
|
|
if line.strip() and not line.lstrip().startswith("#"):
|
|
return lines[:i], lines[i:]
|
|
return lines, []
|
|
|
|
|
|
def clean_doctest_list(doctest_file: Path, overwrite: bool = False) -> None:
|
|
"""Check, and optionally fix, one doctest list file.
|
|
|
|
Args:
|
|
doctest_file (`Path`):
|
|
The list file to check or clean.
|
|
overwrite (`bool`, *optional*, defaults to `False`):
|
|
Whether to fix problems in place. When `False`, raises instead.
|
|
|
|
Raises:
|
|
ValueError: If the file lists a path that does not exist, or is not alphabetically sorted and
|
|
`overwrite` is `False`.
|
|
"""
|
|
lines = doctest_file.read_text(encoding="utf-8").splitlines()
|
|
header, entries = split_header(lines)
|
|
paths = [line.strip().split(" ")[0] for line in entries if line.strip()]
|
|
|
|
non_existent = [p for p in paths if not (REPO_PATH / p).exists()]
|
|
if non_existent:
|
|
listed = "\n".join(f"- {p}" for p in non_existent)
|
|
raise ValueError(f"`{doctest_file.name}` contains non-existent paths:\n{listed}")
|
|
|
|
if paths != sorted(paths):
|
|
if not overwrite:
|
|
raise ValueError(
|
|
f"Files in `{doctest_file.name}` are not in alphabetical order, run "
|
|
"`make fix-docstrings` to fix this automatically."
|
|
)
|
|
doctest_file.write_text("\n".join(header + sorted(paths)) + "\n", encoding="utf-8")
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the check over every doctest list file.
|
|
|
|
Returns:
|
|
`int`: A process exit code — `0` when every file is clean, `1` otherwise.
|
|
"""
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--fix_and_overwrite", action="store_true", help="Whether to fix inconsistencies.")
|
|
args = parser.parse_args()
|
|
|
|
failed = False
|
|
for name in DOCTEST_FILE_PATHS:
|
|
try:
|
|
clean_doctest_list(REPO_PATH / "utils" / name, args.fix_and_overwrite)
|
|
except ValueError as error:
|
|
print(error, file=sys.stderr)
|
|
failed = True
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|