Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/accelerate_doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ jobs:
run: uv venv --python 3.10 .venv

- name: Install Python dependencies
run: uv pip install --python .venv/bin/python ./doc-builder './accelerate[dev]'
# light install: only accelerate's real (light) doc-build deps; torch & co are
# mocked at build time from the mock-deps registry
run: |
uv pip install --python .venv/bin/python ./doc-builder
uv pip install --python .venv/bin/python --no-deps ./accelerate
./.venv/bin/doc-builder light-install accelerate

- name: Make documentation
run: |
Expand Down
73 changes: 21 additions & 52 deletions .github/workflows/build_main_documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ on:
convert_notebooks:
type: boolean
description: "Convert notebooks to markdown files before building docs."
# Docker image to use for the build. Set custom_container="huggingface/transformers-doc-builder" if you need
# a complete install. Default to `""` which means CI will run on the runner directly.
custom_container:
type: string
default: ""
description: "Docker image to use for the build."
secrets:
hf_token:
required: true
Expand All @@ -61,8 +55,6 @@ on:
jobs:
build_main_documentation:
runs-on: ubuntu-22.04
container:
${{ inputs.custom_container }}
defaults:
run:
shell: bash
Expand Down Expand Up @@ -92,28 +84,7 @@ jobs:
node-version: '20'
cache-dependency-path: "kit/package-lock.json"

- name: Export ROOT_APT_GET ('apt-get' or 'sudo apt-get')
# Use `sudo` only if running on the base runner.
# When using a container, `sudo` is not needed (and not installed)
run: |
if [ -z "${{ inputs.custom_container }}" ]
then
echo "ROOT_APT_GET=sudo apt-get" >> $GITHUB_ENV
else
echo "ROOT_APT_GET=apt-get" >> $GITHUB_ENV
fi

- name: Export PIP_OR_UV ('pip' or 'uv pip')
# Use `uv` only if running on the base runner.
# When using a container, `pip` has already been used to installed existing deps
# and is therefore quicker to resolve (already have some cached stuff).
run: |
if [ -z "${{ inputs.custom_container }}" ]
then
echo "PIP_OR_UV=uv pip" >> $GITHUB_ENV
else
echo "PIP_OR_UV=pip" >> $GITHUB_ENV
fi
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0

- name: Set env variables
run: |
Expand All @@ -134,46 +105,45 @@ jobs:

# Needed to upload zips to hf.co
- name: Install zip
# Use sudo only if running on the base runner.
# When using a container, `sudo` is not needed (and not installed).
run: $ROOT_APT_GET install -y zip
run: sudo apt-get install -y zip

- name: Install libgl1
if: inputs.install_libgl1
run: $ROOT_APT_GET install -y libgl1
run: sudo apt-get install -y libgl1

- name: Install Rust
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master 2026-03-27
if: inputs.install_rust
with:
toolchain: stable

# Use venv in both cases
- name: Install uv
run: |
pip install -U uv
uv venv

- name: Setup environment
shell: bash
run: |
uv venv
source .venv/bin/activate
$PIP_OR_UV uninstall doc-builder
cd doc-builder
git pull origin main
$PIP_OR_UV install .
uv pip install .
cd ..

if [[ -n "${{ inputs.package_path }}" ]]
then
cd ${{ inputs.package_path }}
$PIP_OR_UV install .[dev]
elif [[ "${{ inputs.additional_args }}" != *"--not_python_module"* ]];
if [[ "${{ inputs.additional_args }}" == *"--not_python_module"* ]]
then
cd ${{ inputs.package }}
$PIP_OR_UV install .[dev]
echo "Not a Python module; skipping package install."
else
package_dir="${{ inputs.package_path || inputs.package }}"
# Light install when the package has a mock-deps registry entry: only its
# real (light) doc-build deps are installed and the heavy ones are mocked
# at build time. Falls back to a full [dev] install otherwise.
if doc-builder light-install ${{ env.package_name }} --check
then
uv pip install "./$package_dir" --no-deps
doc-builder light-install ${{ env.package_name }}
else
echo "Falling back to a full install."
uv pip install "./$package_dir[dev]"
fi
fi
cd ..

- name: Setup git
run: |
Expand Down Expand Up @@ -244,9 +214,8 @@ jobs:
HF_TOKEN: ${{ secrets.hf_token }}
PACKAGE_NAME: ${{ env.package_name }}
run: |
pip install -U huggingface_hub
cd build_dir
hf sync "./$PACKAGE_NAME" "hf://buckets/hf-doc-build/doc/$PACKAGE_NAME"
uvx --from huggingface_hub hf sync "./$PACKAGE_NAME" "hf://buckets/hf-doc-build/doc/$PACKAGE_NAME"
cd ..

- name: Push to dataset (legacy, kept for rollback safety)
Expand Down
66 changes: 19 additions & 47 deletions .github/workflows/build_pr_documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ on:
convert_notebooks:
type: boolean
description: "Convert notebooks to markdown files before building docs."
# Docker image to use for the build. Set custom_container="huggingface/transformers-doc-builder" if you need
# a complete install. Default to `""` which means CI will run on the runner directly.
custom_container:
type: string
default: ""
description: "Docker image to use for the build."
# Debug purposes only!
doc_builder_revision:
type: string
Expand All @@ -66,8 +60,6 @@ on:
jobs:
build_pr_documentation:
runs-on: ubuntu-22.04
container:
${{ inputs.custom_container }}
defaults:
run:
shell: bash
Expand Down Expand Up @@ -102,28 +94,7 @@ jobs:
node-version: '20'
cache-dependency-path: "kit/package-lock.json"

- name: Export ROOT_APT_GET ('apt-get' or 'sudo apt-get')
# Use `sudo` only if running on the base runner.
# When using a container, `sudo` is not needed (and not installed)
run: |
if [ -z "${{ inputs.custom_container }}" ]
then
echo "ROOT_APT_GET=sudo apt-get" >> $GITHUB_ENV
else
echo "ROOT_APT_GET=apt-get" >> $GITHUB_ENV
fi

- name: Export PIP_OR_UV ('pip' or 'uv pip')
# Use `uv` only if running on the base runner.
# When using a container, `pip` has already been used to installed existing deps
# and is therefore quicker to resolve (already have some cached stuff).
run: |
if [ -z "${{ inputs.custom_container }}" ]
then
echo "PIP_OR_UV=uv pip" >> $GITHUB_ENV
else
echo "PIP_OR_UV=pip" >> $GITHUB_ENV
fi
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0

- name: Set env variables
run: |
Expand Down Expand Up @@ -151,40 +122,41 @@ jobs:

- name: Install libgl1
if: inputs.install_libgl1
run: $ROOT_APT_GET install -y libgl1
run: sudo apt-get install -y libgl1

- name: Install Rust
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master 2026-03-27
if: inputs.install_rust
with:
toolchain: stable

# Use venv in both cases
- name: Install uv
run: |
pip install -U uv
uv venv

- name: Setup environment
shell: bash
run: |
uv venv
source .venv/bin/activate
$PIP_OR_UV uninstall doc-builder
cd doc-builder
git pull origin ${{ inputs.doc_builder_revision }}
$PIP_OR_UV install .
uv pip install .
cd ..

if [[ -n "${{ inputs.package_path }}" ]]
then
cd ${{ inputs.package_path }}
$PIP_OR_UV install .[dev]
elif [[ "${{ inputs.additional_args }}" != *"--not_python_module"* ]];
if [[ "${{ inputs.additional_args }}" == *"--not_python_module"* ]]
then
cd ${{ inputs.package }}
$PIP_OR_UV install .[dev]
echo "Not a Python module; skipping package install."
else
package_dir="${{ inputs.package_path || inputs.package }}"
# Light install when the package has a mock-deps registry entry: only its
# real (light) doc-build deps are installed and the heavy ones are mocked
# at build time. Falls back to a full [dev] install otherwise.
if doc-builder light-install ${{ env.package_name }} --check
then
uv pip install "./$package_dir" --no-deps
doc-builder light-install ${{ env.package_name }}
else
echo "Falling back to a full install."
uv pip install "./$package_dir[dev]"
fi
fi
cd ..

- name: Run pre-command
shell: bash
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/delete_old_pr_documentations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ jobs:
- uses: denoland/setup-deno@11b63cf76cfcafb4e43f97b6cad24d8e8438f62d # v1.5.2
with:
deno-version: v1.x
- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
- name: Install hf CLI
run: pip install -U huggingface_hub
run: uv tool install huggingface_hub
- run: deno run --allow-env --allow-net --allow-run --allow-read ./delete-old-prs.ts
env:
HF_ACCESS_TOKEN: ${{ secrets.HF_ACCESS_TOKEN }}
10 changes: 4 additions & 6 deletions .github/workflows/upload_pr_documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ jobs:
# Uncomment the following line to use a specific revision of doc-builder
# ref: fix-corrupted-zip

- uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0

- name: Setup environment
shell: bash
id: setup-env
run: |
pip install black
cd doc-builder
pip install .
cd ..
uv pip install --system ./doc-builder
echo "current_work_dir=$(pwd)" >> $GITHUB_OUTPUT

- name: 'Download artifact'
Expand Down Expand Up @@ -137,8 +136,7 @@ jobs:
HF_TOKEN: ${{ secrets.hf_token }}
run: |
cd build_dir
pip install -U huggingface_hub
hf sync "./$PACKAGE_NAME" "hf://buckets/hf-doc-build/doc-dev/$PACKAGE_NAME"
uvx --from huggingface_hub hf sync "./$PACKAGE_NAME" "hf://buckets/hf-doc-build/doc-dev/$PACKAGE_NAME"

- name: Find doc comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ A page is reused when its generated MDX (post autodoc and internal-link resoluti

`--html_page_cache_write` also stores freshly built pages. Only enable it on trusted builds (the shared GitHub workflows enable it on main-branch builds and keep PR builds read-only), so that untrusted code cannot poison the cache. Cache failures are never fatal: any error degrades to building the affected pages.

### Building without installing heavy dependencies

doc-builder imports the documented library to read docstrings with `inspect` — necessary because libraries like `transformers` construct docstrings dynamically. Historically that meant installing the library's full dependency tree (torch, GPU wheels, custom containers) just to build documentation.

doc-builder mocks heavy dependencies instead, so only the documented library itself (and its light dependencies) needs to be installed. Each library's registry file `src/doc_builder/mock_deps/<library>.txt` records both sides of that split:

- bare lines name the heavy dependencies to **mock** at build time (applied automatically by `doc-builder build`/`preview`);
- `real:<pip spec>` lines name the light dependencies a doc build must **install for real** (things the library imports unguarded, version-pinned requirements, packages used through `isinstance` checks, ...);
- `nodeps:<pip spec>` lines are also installed for real but with `--no-deps`, because their own dependency trees pull in heavy packages (e.g. `accelerate`, whose install requires torch).

`doc-builder light-install <library>` installs the `real:`/`nodeps:` dependencies (via `uv`), so a full light setup is:

```bash
uv pip install ./accelerate --no-deps
doc-builder light-install accelerate
doc-builder build accelerate ~/git/accelerate/docs/source --build_dir ~/tmp/test-build
# torch and deepspeed are mocked automatically (src/doc_builder/mock_deps/accelerate.txt)
```

Install the library before running `light-install`: bare registry names are pinned to the library's own declared version ranges (e.g. transformers' `tokenizers>=0.22,<=0.23`), so registry files never have to chase upstream pins. `light-install` exits with code 3 when the library has no registry entry — `light-install <library> --check` tests this without installing anything, which is how the shared GitHub workflows decide between the light path and a full `[dev]` install. For a library without a registry entry (or to experiment), `--mock_deps torch,...` adds extra mocks on top of the registry.

The registry key is the doc-builder *library name* as passed to `doc-builder build` — for namespace subpackages that is the dotted module name (e.g. `mock_deps/optimum.intel.txt`), and libraries that build under a shared name share a file (optimum-onnx builds as `optimum`). A registered name may itself be a dotted submodule (e.g. `optimum.onnxruntime`), which mocks a missing sibling inside an installed namespace package.

The mocked packages are importable, pass the `importlib.util.find_spec` + `importlib.metadata` availability checks HF libraries use, are subclassable without affecting the real subclass's signature or docstring, behave as pass-through decorators, and render in signatures like the real objects (`repr(torch.float32) == "torch.float32"`). Packages that are actually installed are never mocked, and `doc-builder preview` supports the flag too.

Fidelity, verified on `accelerate` built without torch installed: 46/48 pages byte-identical to a real-torch build; the two remaining pages differ only in typing paths rendered from the public import path instead of the internal one (e.g. `torch.nn.Module` instead of `torch.nn.modules.module.Module`).

### Notebook conversion

`doc-builder` can also automatically convert some of the documentation guides or tutorials into notebooks. This requires two steps:
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
keywords="doc documentation doc-builder huggingface hugging face",
url="https://github.com/huggingface/doc-builder",
package_dir={"": "src"},
package_data={"doc_builder": ["mock_deps/*.txt"]},
include_package_data=True,
packages=find_packages("src"),
extras_require=extras,
install_requires=install_requires,
Expand Down
17 changes: 17 additions & 0 deletions src/doc_builder/autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import inspect
import json
import re
import sys
from functools import lru_cache

from .convert_md_to_mdx import convert_md_docstring_to_mdx
Expand Down Expand Up @@ -44,6 +45,16 @@ def find_object_in_package(object_name, package):
full_module_path = f"{module.__name__}.{split}"
importlib.import_module(full_module_path)
submodule = getattr(module, split, None)
if submodule is None:
# a submodule can be importable without being bound as an
# attribute of its parent (e.g. lazy-module parents whose
# attribute binding was skipped because the submodule was
# already in sys.modules when first accessed)
submodule = sys.modules.get(full_module_path)
if submodule is not None:
# heal the missing binding so later attribute walks
# (e.g. get_shortest_path) resolve too
setattr(module, split, submodule)
except ImportError:
return None # Return None if the module cannot be found or imported
module = submodule
Expand Down Expand Up @@ -93,6 +104,12 @@ def get_type_name(typ):
if isinstance(typ, type):
# If it's a class, use its name.
return getattr(typ, "__qualname__", None) or getattr(typ, "__name__", None) or str(typ)
# Non-classes carrying a name (e.g. mocked classes from `--mock_deps`) render like
# classes; typing constructs (whose reprs are the readable form) are excluded.
if getattr(typ, "__module__", "") != "typing":
qualname = getattr(typ, "__qualname__", None)
if isinstance(qualname, str):
return qualname
return str(typ) # otherwise, trust its string representation


Expand Down
Loading