From fefbc2c40786baa46f1c70239c7b78242dc1d740 Mon Sep 17 00:00:00 2001 From: Taylor Howell Date: Fri, 24 Apr 2026 16:08:09 +0100 Subject: [PATCH] mujoco introspect --- .readthedocs.yml | 40 ++------- doc/make_mujoco_stubs.py | 176 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 doc/make_mujoco_stubs.py diff --git a/.readthedocs.yml b/.readthedocs.yml index 5f9792b3..89ef662f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -20,46 +20,18 @@ build: - asdf install uv latest - asdf global uv latest - uv venv $READTHEDOCS_VIRTUALENV_PATH - # install doc requirements and build tools + # install doc requirements - | UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH \ uv pip install \ -r doc/requirements.txt \ - cmake pip build setuptools absl-py - # build and install MuJoCo C library + pip setuptools absl-py + # generate and install doc-only mujoco stubs (no C build required) + - python doc/make_mujoco_stubs.py python/mujoco_doc - | - VENV=$READTHEDOCS_VIRTUALENV_PATH && \ - $VENV/bin/cmake -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=$VENV \ - -DMUJOCO_BUILD_EXAMPLES=OFF \ - -DMUJOCO_BUILD_SIMULATE=OFF \ - -DMUJOCO_BUILD_TESTS=OFF \ - -DMUJOCO_TEST_PYTHON_UTIL=OFF && \ - $VENV/bin/cmake --build build --parallel && \ - $VENV/bin/cmake --install build - # copy plugins - - | - mkdir -p $READTHEDOCS_VIRTUALENV_PATH/mujoco_plugin && \ - cp build/lib/libactuator.* \ - build/lib/libelasticity.* \ - build/lib/libsensor.* \ - $READTHEDOCS_VIRTUALENV_PATH/mujoco_plugin/ && \ - cp build/lib/libsdf_plugin.* \ - $READTHEDOCS_VIRTUALENV_PATH/mujoco_plugin/ || true - # build and install Python bindings from source - - | - export VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH \ - PATH=$READTHEDOCS_VIRTUALENV_PATH/bin:$PATH && \ - cd python && bash make_sdist.sh && cd dist && \ - MUJOCO_PATH=$READTHEDOCS_VIRTUALENV_PATH \ - MUJOCO_PLUGIN_PATH=$READTHEDOCS_VIRTUALENV_PATH/mujoco_plugin \ - MUJOCO_CMAKE_ARGS="-DCMAKE_INTERPROCEDURAL_OPTIMIZATION:BOOL=OFF \ - -DGLFW_BUILD_WAYLAND=OFF -DGLFW_BUILD_X11=OFF" \ UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH \ - uv pip install mujoco-*.tar.gz && \ - cd ../.. - # install mjx and mujoco_warp + uv pip install --no-deps python/mujoco_doc + # install mjx and mujoco_warp (mujoco dep satisfied by stubs above) - UV_PROJECT_ENVIRONMENT=$READTHEDOCS_VIRTUALENV_PATH uv pip install -e mjx - | find mjx/mujoco/mjx/third_party/mujoco_warp -type f -exec \ diff --git a/doc/make_mujoco_stubs.py b/doc/make_mujoco_stubs.py new file mode 100644 index 00000000..e8f781e9 --- /dev/null +++ b/doc/make_mujoco_stubs.py @@ -0,0 +1,176 @@ +# Copyright 2026 DeepMind Technologies Limited +# +# 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. +# ============================================================================== +"""Generates a pip-installable doc-only mujoco stub package. + +This creates a lightweight pure-Python package that provides the mujoco +public API surface (enums, constants) without C extensions, for use by +Sphinx autodoc during documentation builds. + +Usage: + python doc/make_mujoco_stubs.py +""" + +import glob +import os +import re +import shutil +import sys +import textwrap + + +def _read_version(repo_root): + """Reads the mujoco version from python/pyproject.toml.""" + with open(os.path.join(repo_root, 'python', 'pyproject.toml')) as f: + for line in f: + m = re.match(r'version\s*=\s*"([^"]+)"', line.strip()) + if m: + return m.group(1) + raise RuntimeError('Could not find version in python/pyproject.toml') + + +def _parse_constants(repo_root): + """Parses numeric #define constants from MuJoCo C headers.""" + consts = {} + for path in glob.glob(os.path.join(repo_root, 'include', 'mujoco', '*.h')): + with open(path) as f: + for line in f: + m = re.match( + r'\s*#define\s+(mj[A-Z]\w*)\s+([\d.eE+\-]+)\s', line) + if m and not m.group(2).endswith('f'): + consts[m.group(1)] = m.group(2) + return consts + + +_PYPROJECT_TEMPLATE = textwrap.dedent("""\ + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + + [project] + name = "mujoco" + version = "{version}" + requires-python = ">=3.10" + dependencies = [] + + [tool.setuptools] + include-package-data = false + + [tool.setuptools.packages.find] + include = ["mujoco*"] +""") + +_INIT_PY_TEMPLATE = textwrap.dedent("""\ + \"\"\"Doc-only stub for MuJoCo. Provides enums and constants for autodoc.\"\"\" + import enum + import sys + + __path__ = __import__('pkgutil').extend_path(__path__, __name__) + + from mujoco.introspect.enums import ENUMS + + _mod = sys.modules[__name__] + for _n, _d in ENUMS.items(): + _cls = enum.IntEnum(_n, list(_d.values.items())) + setattr(_mod, _n, _cls) + for _vn, _vv in _d.values.items(): + setattr(_mod, _vn, _vv) + + {constants} + + try: + from importlib.metadata import version as _v + __version__ = _v('mujoco') + except Exception: + __version__ = '0.0.0' + + def mj_versionString(): + return __version__ + + # Stub types for C extensions (MjModel, MjData, mj_* functions, etc.) + # needed by MJX and mujoco_warp imports during Sphinx autodoc. + # Each accessed name gets a dynamically created class so that Sphinx + # renders the real type name instead of "Mock". + _mock_cache = {{}} + + def _make_mock_meta(mock_name): + class _MockMeta(type): + def __getattr__(cls, name): + return _make_mock(f'{{mock_name}}.{{name}}') + def __instancecheck__(cls, instance): + return True + def __repr__(cls): + return mock_name + return _MockMeta + + def _make_mock(qualname): + if qualname in _mock_cache: + return _mock_cache[qualname] + basename = qualname.rsplit('.', 1)[-1] + meta = _make_mock_meta(qualname) + cls = meta(basename, (), {{ + '__init__': lambda self, *a, **kw: None, + '__call__': lambda self, *a, **kw: _make_mock(qualname)(), + '__getattr__': lambda self, name: _make_mock(f'{{qualname}}.{{name}}'), + '__class_getitem__': classmethod(lambda cls, item: cls), + '__iter__': lambda self: iter([]), + '__bool__': lambda self: False, + '__repr__': lambda self: qualname, + '__module__': 'mujoco', + '__qualname__': basename, + }}) + _mock_cache[qualname] = cls + return cls + + def __getattr__(name): + return _make_mock(name) +""") + + + +def main(): + if len(sys.argv) != 2: + print(f'Usage: {sys.argv[0]} ', file=sys.stderr) + sys.exit(1) + + output_dir = sys.argv[1] + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + version = _read_version(repo_root) + consts = _parse_constants(repo_root) + + mujoco_dir = os.path.join(output_dir, 'mujoco') + os.makedirs(mujoco_dir, exist_ok=True) + + # Write pyproject.toml. + with open(os.path.join(output_dir, 'pyproject.toml'), 'w') as f: + f.write(_PYPROJECT_TEMPLATE.format(version=version)) + + # Write mujoco/__init__.py with constants inlined. + constants_str = '\n'.join( + f'{k} = {v}' for k, v in sorted(consts.items())) + with open(os.path.join(mujoco_dir, '__init__.py'), 'w') as f: + f.write(_INIT_PY_TEMPLATE.format(constants=constants_str)) + + # Copy introspect/ into the package. + introspect_src = os.path.join(repo_root, 'python', 'mujoco', 'introspect') + introspect_dst = os.path.join(mujoco_dir, 'introspect') + if os.path.exists(introspect_dst): + shutil.rmtree(introspect_dst) + shutil.copytree(introspect_src, introspect_dst) + + print(f'Generated doc-only mujoco {version} package at {output_dir}') + + +if __name__ == '__main__': + main()