blender/doc/python_api/sphinx_doc_gen.py
Richard Antalik 437f2815e1 Fix #163692: Incorrect type in bpy docs
Some people rely on linters that use our docs to construct shell of bpy
class. Our docs specified `Sequence` type for some context members,
when actual type used is `list`.

Pull Request: https://projects.blender.org/blender/blender/pulls/163734
2026-09-14 21:19:49 +02:00

3713 lines
135 KiB
Python

# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
API dump in RST files
---------------------
Run this script from Blender's root path once you have compiled Blender
blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py
This will generate Python files in doc/python_api/sphinx-in/
providing ./blender is or links to the Blender executable
To choose sphinx-in directory:
blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --output=../python_api
For quick builds:
blender --background --factory-startup --python doc/python_api/sphinx_doc_gen.py -- --partial=bmesh.*
Sphinx: HTML generation
-----------------------
After you have built doc/python_api/sphinx-in (see above),
generate html docs by running:
sphinx-build doc/python_api/sphinx-in doc/python_api/sphinx-out
Sphinx: PDF generation
----------------------
After you have built doc/python_api/sphinx-in (see above),
generate the pdf doc by running:
sphinx-build -b latex doc/python_api/sphinx-in doc/python_api/sphinx-out
cd doc/python_api/sphinx-out
make
"""
__all__ = (
"main",
)
import argparse
import sys
import inspect
import shutil
import logging
import warnings
from collections.abc import (
Callable,
Iterator,
Sequence,
)
from typing import (
Final,
NamedTuple,
Protocol,
)
from pathlib import Path
from textwrap import indent
try:
import bpy # type: ignore[import-not-found] # Blender module.
except ImportError:
print("\nERROR: this script must run from inside Blender")
print(__doc__)
sys.exit()
import _rna_info as rna_info # type: ignore[import-not-found] # Blender module.
# ----------------------------------------------------------------------------
# Type Info
# Alias for a file's `.write` method, used as `fw(...)`.
WriteFn = Callable[[str], int]
# ----------------------------------------------------------------------------
# Inline stubs for `bpy` and `_rna_info`
#
# These types have no runtime stubs available,
# they could be important but the code depends heavily on `bpy`, so define stubs here.
class stub:
"""Namespace holding inline stubs for `bpy` and `_rna_info` types."""
class RnaEnumItem(Protocol):
identifier: str
name: str
description: str
def as_pointer(self) -> int: ...
# Tuple form: (note, current_version, removal_version)
RnaDeprecated = tuple[str, tuple[int, int, int], tuple[int, int, int]]
class InfoPropertyRNA(Protocol):
identifier: str
name: str
description: str
type: str
fixed_type: "stub.InfoStructRNA | None"
srna: "stub.InfoStructRNA | None"
# Tuples of `(identifier, name, description)`.
enum_items: Sequence[tuple[str, str, str]]
enum_pointer: int | None
deprecated: "stub.RnaDeprecated | None"
is_required: bool
def get_arg_default(self, force: bool = ...) -> str: ...
def get_type_description(
self,
*,
as_arg: bool = ...,
as_ret: bool = ...,
class_fmt: str = ...,
mathutils_fmt: str = ...,
literal_fmt: str = ...,
collection_id: str = ...,
enum_descr_override: str | None = ...,
) -> tuple[str, list[str]]: ...
class InfoFunctionRNA(Protocol):
identifier: str
description: str
args: "Sequence[stub.InfoPropertyRNA]"
return_values: "Sequence[stub.InfoPropertyRNA]"
is_classmethod: bool
class InfoOperatorRNA(Protocol):
identifier: str
module_name: str
func_name: str
description: str
args: "Sequence[stub.InfoPropertyRNA]"
def get_location(self) -> tuple[str | None, int | None]: ...
class InfoStructRNA(Protocol):
identifier: str
module_name: str
base: "stub.InfoStructRNA | None"
description: str
properties: "Sequence[stub.InfoPropertyRNA]"
functions: "Sequence[stub.InfoFunctionRNA]"
references: Sequence[str]
py_class: type
def get_bases(self) -> "list[stub.InfoStructRNA]": ...
def get_py_properties(self) -> "list[tuple[str, stub.PyProperty]]": ...
def get_py_c_properties_getset(self) -> list[tuple[str, object]]: ...
def get_py_functions(self) -> list[tuple[str, Callable[..., object]]]: ...
def get_py_c_functions(self) -> list[tuple[str, Callable[..., object]]]: ...
def is_operator_properties(self) -> bool: ...
class PyProperty(Protocol):
fset: Callable[..., object] | None
# Result of `_rna_info.BuildRNAInfo()`: (structs, funcs, ops, props).
# Keys are `(parent_id, identifier)` tuples, see `_GetInfoRNA` in `_rna_info.py`.
RnaInfo = tuple[
"dict[tuple[str, str], stub.InfoStructRNA]",
"dict[tuple[str, str], stub.InfoFunctionRNA]",
"dict[tuple[str, str], stub.InfoOperatorRNA]",
"dict[tuple[str, str], stub.InfoPropertyRNA]",
]
def rna_info_BuildRNAInfo_cache() -> stub.RnaInfo:
ret: stub.RnaInfo | None = rna_info_BuildRNAInfo_cache.ret # type: ignore[attr-defined]
if ret is None:
ret = rna_info.BuildRNAInfo()
rna_info_BuildRNAInfo_cache.ret = ret # type: ignore[attr-defined]
return ret
rna_info_BuildRNAInfo_cache.ret = None # type: ignore[attr-defined]
# --- end rna_info cache
SCRIPT_DIR = Path(__file__).resolve().parent
# ----------------------------------------------------------------------------
# Global State
class Global(NamedTuple):
"""Static configuration assembled from command-line arguments and Blender state."""
# Command line arguments.
output_dir: Path
partial: str
full_rebuild: bool
bpy: bool
changelog: bool
api_dump_index_path: Path | None
sphinx_build: bool
sphinx_build_pdf: bool
pack_reference: bool
log: bool
# Derived from `partial` + Blender's available modules.
exclude_modules: frozenset[str]
filter_bpy_ops: tuple[str, ...] | None
filter_bpy_types: tuple[str, ...] | None
exclude_info_docs: bool
# Derived from `bpy.app`.
blender_revision: str
blender_revision_timestamp: int
blender_version_string: str
blender_version_dots: str
blender_version_path: str
# Deployable artifact names and paths (used when `pack_reference` is set).
output_base_name: str
output_base_path: Path
output_filename_pdf: str
output_filename_zip: str
# Derived from `output_dir`.
sphinx_in: Path
sphinx_in_tmp: Path
sphinx_out: Path
sphinx_out_pdf: Path | None # Set only when `sphinx_build_pdf` is True.
# Sphinx command lines (only populated when the matching `sphinx_build*` flag is set).
sphinx_build_cmd: tuple[str | Path, ...]
sphinx_build_pdf_cmd: tuple[str | Path, ...]
sphinx_make_pdf_cmd: tuple[str | Path, ...]
# Sphinx log paths (set only when `log` is True alongside the matching `sphinx_build*` flag).
sphinx_build_log: Path | None
sphinx_build_pdf_log: Path | None
sphinx_make_pdf_log: Path | None
# Type-only declaration; bound by `main()`.
GLOBAL: Global
# ----------------------------------------------------------------------------
# Mutable State
class State:
# Non-fatal issues (e.g. C-API `PyGetSetDef` docstring problems) increment
# this counter. `main()` returns non-zero when it ends up greater than zero.
error_count: int = 0
# Set of example identifiers referenced during RST generation.
# Compared against `EXAMPLE_SET` at the end of `main()` to report unused examples.
example_set_used: set[str] = set()
@staticmethod
def reset() -> None:
State.error_count = 0
State.example_set_used = set()
# For now, ignore add-ons and internal sub-classes of `bpy.types.PropertyGroup`.
#
# Besides disabling this line, the main change will be to add a
# `toctree` to `write_rst_index` which contains the generated RST files.
# This `toctree` can be generated automatically.
#
# See: D6261 for reference.
USE_ONLY_BUILTIN_RNA_TYPES: Final = True
# Write a page for each static enum defined in:
# `source/blender/makesrna/RNA_enum_items.hh` so the enums can be linked to instead of being expanded everywhere.
USE_SHARED_RNA_ENUM_ITEMS_STATIC: Final = True
# Generate a list of types which support custom properties.
# This isn't listed anywhere, it's just linked to.
USE_RNA_TYPES_WITH_CUSTOM_PROPERTY_INDEX: Final = True
# Write additional RST so `sphinx_stub_gen.py` can produce mypy-compatible stubs
# (dunder methods, valid default reprs).
# Disable to fall back to the pre-stub-gen baseline RST.
USE_STUB_GEN: Final = True
# Other types are assumed to be `bpy.types.*`.
PRIMITIVE_TYPE_NAMES = {"bool", "bytearray", "bytes", "dict", "float", "int", "list", "set", "str", "tuple"}
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
from _bpy import rna_enum_items_static # type: ignore[import-not-found]
rna_enum_dict = rna_enum_items_static()
for key in ("rna_enum_dummy_NULL_items", "rna_enum_dummy_DEFAULT_items"):
del rna_enum_dict[key]
del key, rna_enum_items_static
# Build enum `{pointer: identifier}` map, so any enum property pointer can
# lookup an identifier using `InfoPropertyRNA.enum_pointer` as the key.
rna_enum_pointer_to_id_map = {
enum_prop.as_pointer(): key
for key, enum_items in rna_enum_dict.items()
# It's possible the first item is a heading (which has no identifier).
# skip these as the `EnumProperty.enum_items` does not expose them.
if (enum_prop := next(iter(enum_prop for enum_prop in enum_items if enum_prop.identifier), None))
}
def handle_args(argv: Sequence[str]) -> argparse.Namespace:
"""
Parse the arguments passed in ``argv``.
When invoked from Blender, callers should pass the slice of ``sys.argv``
after ``"--"`` (Blender ignores everything after ``--`` itself).
"""
# When --help is given, print the usage text
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
usage=__doc__
)
# Optional arguments.
parser.add_argument(
"-p", "--partial",
dest="partial",
type=str,
default="",
help="Use a wildcard to only build specific module(s)\n"
"Example: --partial\"=bmesh*\"\n",
required=False,
)
parser.add_argument(
"-f", "--fullrebuild",
dest="full_rebuild",
default=False,
action='store_true',
help="Rewrite all RST files in sphinx-in/ "
"(default=False)",
required=False,
)
parser.add_argument(
"-b", "--bpy",
dest="bpy",
default=False,
action='store_true',
help="Write the RST file of the bpy module "
"(default=False)",
required=False,
)
parser.add_argument(
"--api-changelog-generate",
dest="changelog",
default=False,
action='store_true',
help="Generate the API changelog RST file "
"(default=False, requires `--api-dump-index-path` parameter)",
required=False,
)
parser.add_argument(
"--api-dump-index-path",
dest="api_dump_index_path",
metavar='FILE',
type=Path,
default=None,
help="Path to the API dump index JSON file "
"(required when `--api-changelog-generate` is True)",
required=False,
)
parser.add_argument(
"-o", "--output",
dest="output_dir",
type=Path,
default=SCRIPT_DIR,
help="Path of the API docs (default=<script dir>)",
required=False,
)
parser.add_argument(
"-B", "--sphinx-build",
dest="sphinx_build",
default=False,
action='store_true',
help="Build the html docs by running:\n"
"sphinx-build <sphinx-in> <sphinx-out>\n"
"(default=False; does not depend on -P)",
required=False,
)
parser.add_argument(
"-P", "--sphinx-build-pdf",
dest="sphinx_build_pdf",
default=False,
action='store_true',
help="Build the pdf by running:\n"
"sphinx-build -b latex <sphinx-in> <sphinx-out-pdf>\n"
"(default=False; does not depend on -B)",
required=False,
)
parser.add_argument(
"-R", "--pack-reference",
dest="pack_reference",
default=False,
action='store_true',
help="Pack all necessary files in the deployed dir.\n"
"(default=False; use with -B and -P)",
required=False,
)
parser.add_argument(
"-l", "--log",
dest="log",
default=False,
action='store_true',
help=(
"Log the output of the API dump and sphinx|latex "
"warnings and errors (default=False).\n"
"If given, save logs in:\n"
"- OUTPUT_DIR/.bpy.log\n"
"- OUTPUT_DIR/.sphinx-build.log\n"
"- OUTPUT_DIR/.sphinx-build_pdf.log\n"
"- OUTPUT_DIR/.latex_make.log"
),
required=False,
)
return parser.parse_args(argv)
# ----------------------------------------------------------------------------
# BPY
BPY_LOGGER = logging.getLogger("bpy")
BPY_LOGGER.setLevel(logging.DEBUG)
# Source files we use, and need to copy to the OUTPUT_DIR
# to have working out-of-source builds.
# Paths are relative to `RST_DIR`; `..` parents are replaced by `__` in the RST destination,
# to avoid having to match Blender's source tree.
EXTRA_SOURCE_FILES = (
Path("../../../scripts/templates_py/bmesh_simple.py"),
Path("../../../scripts/templates_py/Gizmo/operator.py"),
Path("../../../scripts/templates_py/Gizmo/operator_target.py"),
Path("../../../scripts/templates_py/Gizmo/simple_3d.py"),
Path("../../../scripts/templates_py/Operator/simple.py"),
Path("../../../scripts/templates_py/UI/panel_simple.py"),
Path("../../../scripts/templates_py/UI/previews_custom_icon.py"),
Path("../examples/bmesh.ops.1.py"),
Path("../examples/bpy.app.translations.0.py"),
)
# Examples.
EXAMPLES_DIR = (SCRIPT_DIR / "examples").resolve()
EXAMPLE_SET = {p.stem for p in EXAMPLES_DIR.iterdir() if p.suffix == ".py"}
# RST files directory.
RST_DIR = (SCRIPT_DIR / "rst").resolve()
# Extra info, not api reference docs stored in `./rst/info_*`.
# Pairs of (file, description), the title makes from the RST files are displayed before the description.
INFO_DOCS = (
(Path("info_quickstart.rst"),
"New to Blender or scripting and want to get your feet wet?"),
(Path("info_overview.rst"),
"A more complete explanation of Python integration."),
(Path("info_api_reference.rst"),
"Examples of how to use the API reference docs."),
(Path("info_best_practice.rst"),
"Conventions to follow for writing good scripts."),
(Path("info_tips_and_tricks.rst"),
"Hints to help you while writing scripts for Blender."),
(Path("info_gotcha.rst"),
"Some of the problems you may encounter when writing scripts."),
(Path("info_advanced.rst"),
"Topics which may not be required for typical usage."),
(Path("change_log.rst"),
"List of changes since last Blender release"),
(Path("info_contributing.rst"),
"Guide for contributing to Blender's Python API documentation."),
)
# Referenced indirectly.
INFO_DOCS_OTHER = (
# Included by: `info_advanced.rst`.
Path("info_advanced_blender_as_bpy.rst"),
# Included by: `info_gotcha.rst`.
Path("info_gotchas_crashes.rst"),
Path("info_gotchas_threading.rst"),
Path("info_gotchas_internal_data_and_python_objects.rst"),
Path("info_gotchas_operators.rst"),
Path("info_gotchas_meshes.rst"),
Path("info_gotchas_armatures_and_bones.rst"),
Path("info_gotchas_file_paths_and_encoding.rst"),
)
# Hide the actual TOC, use a separate list that links to the items.
# This is done so a short description can be included with each link.
USE_INFO_DOCS_FANCY_INDEX: Final = True
# Only support for properties at the moment.
RNA_EXCLUDE: dict[str, set[str]] = {
# XXX messes up PDF!, really a bug but for now just workaround.
"PreferencesSystem": {"language", }
}
# Support suppressing errors when attributes collide with methods,
# use `noindex` on the attributes / data declarations.
#
# NOTE: in general this should be avoided but changing it would break the API,
# so explicitly suppress warnings instead.
#
# NOTE: Currently some API generation doesn't support this is it is not used yet,
# see references to `RST_NOINDEX_ATTR` in code comments.
#
# A set of tuple identifiers: `(module, type, attr)`.
RST_NOINDEX_ATTR = {
# Render is both a method and an attribute, from looking into this
# having both doesn't cause problems in practice since the `render` method
# is registered and called from C++ code where the attribute is accessed from the instance.
("bpy.types", "RenderEngine", "render"),
}
# Underscore-prefixed attributes to include in documentation
# (these would otherwise be skipped). Maps module name to the identifiers.
# NOTE: every inclusion must justify itself as this is something we should typically avoid.
PRIVATE_ATTR_INCLUDE = {
"bpy.props": {
# Without this type documented, the correct type can't be referenced for `bpy.props` definition.
"_PropertyDeferred",
},
}
def is_attr_private(module_name: str, attribute: str) -> bool:
"""Check if an attribute should be skipped as private (underscore-prefixed)."""
if not attribute.startswith("_"):
return False
exceptions = PRIVATE_ATTR_INCLUDE.get(module_name)
if exceptions is None:
return True
if attribute not in exceptions:
return True
return False
MODULE_GROUPING: dict[str, tuple[str | tuple[str, str], ...]] = {
"bmesh.types": (
("Base Mesh Type", "-"),
"BMesh",
("Mesh Elements", "-"),
"BMVert",
"BMEdge",
"BMFace",
"BMLoop",
("Sequence Accessors", "-"),
"BMElemSeq",
"BMVertSeq",
"BMEdgeSeq",
"BMFaceSeq",
"BMLoopSeq",
"BMIter",
("Selection History", "-"),
"BMEditSelSeq",
"BMEditSelIter",
("Custom-Data Layer Access", "-"),
"BMLayerAccessVert",
"BMLayerAccessEdge",
"BMLayerAccessFace",
"BMLayerAccessLoop",
"BMLayerCollection",
"BMLayerItem",
("Custom-Data Layer Types", "-"),
"BMLoopUV",
"BMDeformVert",
)
}
def global_create(argv: Sequence[str]) -> Global:
"""
Build a :class:`Global` from CLI arguments and the running Blender, and return it.
"""
args = handle_args(argv)
# Build the partial-derived configuration into locals.
filter_bpy_ops: tuple[str, ...] | None
filter_bpy_types: tuple[str, ...] | None
# Switch for quick testing so doc-builds don't take so long.
if not args.partial:
# Full build.
filter_bpy_ops = None
filter_bpy_types = None
exclude_info_docs = False
exclude_modules: list[str] = []
else:
# Can manually edit this too:
# filter_bpy_ops = ("import.scene", ) # allow
# filter_bpy_types = ("bpy_struct", "Operator", "ID") # allow
exclude_info_docs = True
exclude_modules = [
"aud",
"blf",
"blf.types",
"bl_math",
"imbuf",
"imbuf.types",
"bmesh",
"bmesh.ops",
"bmesh.types",
"bmesh.utils",
"bmesh.geometry",
"bpy.app",
"bpy.app.handlers",
"bpy.app.timers",
"bpy.app.translations",
"bpy.context",
"bpy.data",
"bpy.ops", # Supports filtering.
"bpy.path",
"bpy.props",
"bpy.types", # Supports filtering.
"bpy.utils",
"bpy.utils.previews",
"bpy.utils.units",
"bpy_extras",
"gpu",
"gpu.types",
"gpu.matrix",
"gpu.select",
"gpu.shader",
"gpu.state",
"gpu.texture",
"gpu.platform",
"gpu.capabilities",
"gpu_extras",
"idprop",
"idprop.types",
"mathutils",
"mathutils.bvhtree",
"mathutils.geometry",
"mathutils.interpolate",
"mathutils.kdtree",
"mathutils.noise",
"freestyle",
"freestyle.chainingiterators",
"freestyle.functions",
"freestyle.predicates",
"freestyle.shaders",
"freestyle.types",
"freestyle.utils",
]
# TODO: support `bpy.ops` and `bpy.types` filtering.
import fnmatch
exclude_modules = [m for m in exclude_modules if not fnmatch.fnmatchcase(m, args.partial)]
# Special support for `bpy.types.*`.
filter_bpy_ops = tuple([m[8:] for m in args.partial.split(":") if m.startswith("bpy.ops.")])
if filter_bpy_ops:
exclude_modules.remove("bpy.ops")
filter_bpy_types = tuple([m[10:] for m in args.partial.split(":") if m.startswith("bpy.types.")])
if filter_bpy_types:
exclude_modules.remove("bpy.types")
exclude_info_docs = (not fnmatch.fnmatchcase("info", args.partial))
BPY_LOGGER.debug(
"Partial Doc Build, Skipping: %s\n",
"\n ".join(sorted(exclude_modules)))
try:
__import__("aud")
except ImportError:
BPY_LOGGER.debug("Warning: Built without \"aud\" module, docs incomplete...")
exclude_modules.append("aud")
try:
__import__("freestyle")
except ImportError:
BPY_LOGGER.debug("Warning: Built without \"freestyle\" module, docs incomplete...")
exclude_modules.extend([
"freestyle",
"freestyle.chainingiterators",
"freestyle.functions",
"freestyle.predicates",
"freestyle.shaders",
"freestyle.types",
"freestyle.utils",
])
# Converting bytes to strings, due to #30154.
blender_revision = str(bpy.app.build_hash, 'utf_8')
blender_revision_timestamp = bpy.app.build_commit_timestamp
# '2.83.0 Beta' or '2.83.0' or '2.83.1'
blender_version_string = bpy.app.version_string
blender_version_dots = "{:d}.{:d}".format(bpy.app.version[0], bpy.app.version[1])
# Example: `2_83`.
blender_version_path = "{:d}_{:d}".format(bpy.app.version[0], bpy.app.version[1])
output_base_name = "blender_python_reference_{:s}".format(blender_version_path)
output_base_path = args.output_dir / output_base_name
output_filename_pdf = "{:s}.pdf".format(output_base_name)
output_filename_zip = "{:s}.zip".format(output_base_name)
sphinx_in = args.output_dir / "sphinx-in"
sphinx_in_tmp = args.output_dir / "sphinx-in-tmp"
sphinx_out = args.output_dir / "sphinx-out"
# HTML build.
sphinx_out_pdf: Path | None = None
sphinx_build_cmd: tuple[str | Path, ...] = ()
sphinx_build_pdf_cmd: tuple[str | Path, ...] = ()
sphinx_make_pdf_cmd: tuple[str | Path, ...] = ()
sphinx_build_log: Path | None = None
sphinx_build_pdf_log: Path | None = None
sphinx_make_pdf_log: Path | None = None
if args.sphinx_build:
if args.log:
sphinx_build_log = args.output_dir / ".sphinx-build.log"
sphinx_build_cmd = (
"sphinx-build",
"-w", sphinx_build_log,
sphinx_in, sphinx_out,
)
else:
sphinx_build_cmd = ("sphinx-build", sphinx_in, sphinx_out)
# PDF build.
if args.sphinx_build_pdf:
sphinx_out_pdf = args.output_dir / "sphinx-out_pdf"
sphinx_make_pdf_cmd = ("make", "-C", sphinx_out_pdf)
if args.log:
sphinx_build_pdf_log = args.output_dir / ".sphinx-build_pdf.log"
sphinx_make_pdf_log = args.output_dir / ".latex_make.log"
sphinx_build_pdf_cmd = (
"sphinx-build", "-b", "latex",
"-w", sphinx_build_pdf_log,
sphinx_in, sphinx_out_pdf,
)
else:
sphinx_build_pdf_cmd = (
"sphinx-build", "-b", "latex",
sphinx_in, sphinx_out_pdf,
)
return Global(
**vars(args),
exclude_modules=frozenset(exclude_modules),
filter_bpy_ops=filter_bpy_ops,
filter_bpy_types=filter_bpy_types,
exclude_info_docs=exclude_info_docs,
blender_revision=blender_revision,
blender_revision_timestamp=blender_revision_timestamp,
blender_version_string=blender_version_string,
blender_version_dots=blender_version_dots,
blender_version_path=blender_version_path,
output_base_name=output_base_name,
output_base_path=output_base_path,
output_filename_pdf=output_filename_pdf,
output_filename_zip=output_filename_zip,
sphinx_in=sphinx_in,
sphinx_in_tmp=sphinx_in_tmp,
sphinx_out=sphinx_out,
sphinx_out_pdf=sphinx_out_pdf,
sphinx_build_cmd=sphinx_build_cmd,
sphinx_build_pdf_cmd=sphinx_build_pdf_cmd,
sphinx_make_pdf_cmd=sphinx_make_pdf_cmd,
sphinx_build_log=sphinx_build_log,
sphinx_build_pdf_log=sphinx_build_pdf_log,
sphinx_make_pdf_log=sphinx_make_pdf_log,
)
# ----------------------------------------------------------------------------
# Change Log Generation
def generate_changelog() -> None:
import importlib.util
spec = importlib.util.spec_from_file_location(
"sphinx_changelog_gen",
SCRIPT_DIR / "sphinx_changelog_gen.py",
)
assert spec is not None and spec.loader is not None
sphinx_changelog_gen = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sphinx_changelog_gen)
assert GLOBAL.api_dump_index_path is not None
API_DUMP_INDEX_FILEPATH = GLOBAL.api_dump_index_path
API_DUMP_ROOT = API_DUMP_INDEX_FILEPATH.parent
API_DUMP_FILEPATH = API_DUMP_ROOT / GLOBAL.blender_version_dots / "api_dump.json"
API_CHANGELOG_FILEPATH = GLOBAL.sphinx_in_tmp / "change_log.rst"
sphinx_changelog_gen.main((
"--",
"--indexpath",
str(API_DUMP_INDEX_FILEPATH),
"dump",
"--filepath-out",
str(API_DUMP_FILEPATH),
))
sphinx_changelog_gen.main((
"--",
"--indexpath",
str(API_DUMP_INDEX_FILEPATH),
"changelog",
"--filepath-out",
str(API_CHANGELOG_FILEPATH),
))
# --------------------------------API DUMP--------------------------------------
# Unfortunately Python doesn't expose direct access to these types.
# Access them indirectly.
ClassMethodDescriptorType = type(dict.__dict__["fromkeys"])
MethodDescriptorType = type(dict.get)
GetSetDescriptorType = type(int.real)
StaticMethodType = type(staticmethod(lambda: None))
from types import (
BuiltinFunctionType,
BuiltinMethodType,
MemberDescriptorType,
MethodType,
FunctionType,
)
# These are C-API defined types (accessible via `bpy.types`) that are not known to RNA.
# The RNA wrapping code in `bpy_rna.cc` uses these types when creating `PyObject`s
# for RNA data. They define methods Python developers use, but since they are not RNA
# structs they are not discovered automatically and must be documented explicitly.
#
# There are two separate hierarchies (see `bpy_rna.cc` for the definitive type hierarchy):
#
# - `bpy_struct`: base for all RNA struct instances.
# - `bpy_prop` -> `bpy_prop_collection` -> `bpy_prop_collection_idprop`:
# used when RNA collection properties are accessed from Python.
#
USE_PYCAPI_TYPES: Final = True
_BPY_STRUCT_PYCAPI = "bpy_struct"
_BPY_PROP_PYCAPI = "bpy_prop"
_BPY_PROP_ARRAY_PYCAPI = "bpy_prop_array"
_BPY_PROP_COLLECTION_PYCAPI = "bpy_prop_collection"
_BPY_PROP_COLLECTION_IDPROP_PYCAPI = "bpy_prop_collection_idprop"
_BPY_FUNC_CAPI = "bpy_func"
_BPY_STRUCT_META_IDPROP_CAPI = "bpy_struct_meta_idprop"
# All core C-API defined types in `bpy.types` that back the RNA wrapping itself.
_BPY_TYPES_CORE_CAPI = frozenset((
_BPY_STRUCT_PYCAPI,
_BPY_PROP_PYCAPI,
_BPY_PROP_ARRAY_PYCAPI,
_BPY_PROP_COLLECTION_PYCAPI,
_BPY_PROP_COLLECTION_IDPROP_PYCAPI,
_BPY_FUNC_CAPI,
_BPY_STRUCT_META_IDPROP_CAPI,
))
_BPY_PROP_COLLECTION_ID = ":class:`{:s}`".format(_BPY_PROP_COLLECTION_PYCAPI) if USE_PYCAPI_TYPES else "collection"
bpy_struct: type = bpy.types.bpy_struct
bpy_prop: type = bpy.types.bpy_prop
bpy_prop_array: type = bpy.types.bpy_prop_array
bpy_prop_collection: type = bpy.types.bpy_prop_collection
bpy_prop_collection_idprop: type = bpy.types.bpy_prop_collection_idprop
def import_value_from_module(module_name: str, import_name: str) -> object:
ns: dict[str, object] = {}
exec_str = "from {:s} import {:s} as value".format(module_name, import_name)
exec(exec_str, ns, ns)
return ns["value"]
def execfile(filepath: Path | str) -> None:
filepath = str(filepath)
global_namespace = {"__file__": filepath, "__name__": "__main__"}
with open(filepath, encoding="utf-8") as file_handle:
exec(compile(file_handle.read(), filepath, 'exec'), global_namespace)
ESCAPE_RST_TRANS = str.maketrans({
"`": "\\`",
"|": "\\|",
"*": "\\*",
"\\": "\\\\",
})
def escape_rst(text: str) -> str:
"""
Escape plain text which may contain characters used by RST.
"""
return text.translate(ESCAPE_RST_TRANS)
def is_struct_seq(value: object) -> bool:
return isinstance(value, tuple) and type(value) != tuple and hasattr(value, "n_fields")
def undocumented_message(module_name: str, type_name: str | None, identifier: str) -> str:
BPY_LOGGER.debug(
"Undocumented: module %s, type: %s, id: %s is not documented",
module_name, type_name, identifier,
)
return "Undocumented, consider `contributing <https://developer.blender.org/>`__."
def example_extract_docstring(filepath: Path | str) -> tuple[str, int, bool]:
"""
Return (text, line_no, line_no_has_content) where:
- ``text`` is the docstring text.
- ``line_no`` is the line the docstring text ends.
- ``line_no_has_content`` when False, this file only contains a docstring.
There is no need to include the remainder.
"""
with open(filepath, "r", encoding="utf-8") as fh:
line = fh.readline()
line_no = 0
text = []
if line.startswith('"""'): # Assume nothing here.
line_no += 1
else:
return "", 0, True
for line in fh:
line_no += 1
if line.startswith('"""'):
break
text.append(line.rstrip())
line_no += 1
line_no_has_content = False
# Skip over blank lines so the Python code doesn't have blank lines at the top.
for line in fh:
if line.strip():
line_no_has_content = True
break
line_no += 1
return "\n".join(text).rstrip("\n"), line_no, line_no_has_content
def title_string(text: str, heading_char: str, double: bool = False) -> str:
filler = len(text) * heading_char
if double:
return "{:s}\n{:s}\n{:s}\n\n".format(filler, text, filler)
return "{:s}\n{:s}\n\n".format(text, filler)
def write_example_ref_impl(ident: str, fw: WriteFn, example_id: str, ext: str) -> None:
# Extract the comment.
# Forward slashes are required for the RST `.. literalinclude::` directive;
# sphinx resolves it relative to the RST file's location at build time.
filepath = "../examples/{:s}.{:s}".format(example_id, ext)
filepath_full = EXAMPLES_DIR / "{:s}.{:s}".format(example_id, ext)
text, line_no, line_no_has_content = example_extract_docstring(filepath_full)
if text:
# Ensure a blank line, needed since in some cases the indentation doesn't match the previous line.
# which causes Sphinx not to warn about bad indentation.
fw("\n")
for line in text.split("\n"):
fw("{:s}\n".format((ident + line).rstrip()))
fw("\n")
# Some files only contain a docstring.
if line_no_has_content:
fw("{:s}.. literalinclude:: {:s}\n".format(ident, filepath))
if line_no > 0:
fw("{:s} :lines: {:d}-\n".format(ident, line_no))
fw("\n")
State.example_set_used.add(example_id)
def write_example_ref(ident: str, fw: WriteFn, example_id: str, ext: str = "py") -> None:
# Support for numbered files `bpy.types.Operator` -> `bpy.types.Operator.0.py`.
i = 0
while True:
example_id_num = "{:s}.{:d}".format(example_id, i)
if example_id_num in EXAMPLE_SET:
write_example_ref_impl(ident, fw, example_id_num, ext)
else:
# Allow numbers to start at 0 or 1,
# historically they started at 1, but now 0 is supported too.
if i > 0:
break
i += 1
def write_indented_lines(ident: str, fn: WriteFn, text: str | None, strip: bool = True) -> None:
"""
Apply same indentation to all lines in a multi-lines text.
"""
if text is None:
return
lines = text.split("\n")
# Strip empty lines from the start/end.
while lines and not lines[0].strip():
del lines[0]
while lines and not lines[-1].strip():
del lines[-1]
if strip:
# Set indentation to `<indent>`.
ident_strip = 1000
for l in lines:
if l.strip():
ident_strip = min(ident_strip, len(l) - len(l.lstrip()))
for l in lines:
fn(ident + l[ident_strip:] + "\n")
else:
# Add <indent> number of blanks to the current indentation.
for l in lines:
fn(ident + l + "\n")
def pyfunc_owner_class(
py_func: Callable[..., object],
is_class: bool,
struct: stub.InfoStructRNA | None,
) -> type | None:
"""
Return the class ``py_func`` is accessed through, or None when undetermined.
This is the binding class (``__self__`` for bound methods,
the RNA struct's Python class for plain functions reached via an RNA struct),
not necessarily the class that defines ``py_func`` - inherited methods return the subclass.
"""
if type(py_func) == MethodType and isinstance(py_func.__self__, type):
return py_func.__self__
if is_class and struct is not None and type(py_func) == FunctionType:
return struct.py_class
return None
def repr_with_function_support(value: object) -> str:
"""
Like ``repr()``, but return ``__name__`` for callables whose ``repr()``
is not a valid Python expression (e.g. ``<built-in function print>`` -> ``print``).
Returns the original ``repr()`` when ``__name__`` itself is not a valid
identifier (e.g. lambdas have ``__name__ == "<lambda>"``); the caller's
substitution then falls through as a no-op rather than writing a broken
identifier into the signature.
"""
r = repr(value)
if r.startswith("<"):
name: str | None = getattr(value, "__name__", None)
if name is not None and name.isidentifier():
return name
return r
def pyfunc_is_inherited_method(py_class: type, py_func: Callable[..., object], identifier: str) -> bool:
"""
Test if ``py_func`` on py_class is shadowed on ``py_class.__base__``.
Only the immediate base is checked. Mix-in methods
(e.g. ``_GenericUI.append`` surfaced as ``Menu.append``)
survive because RNA bases come first by convention, so the mix-in is never ``__base__``.
"""
assert isinstance(py_class, type)
assert type(py_func) in (MethodType, FunctionType)
base = py_class.__base__
if base is None or base is object:
return False
base_attr = getattr(base, identifier, None)
if base_attr is None:
return False
own_underlying = getattr(py_func, "__func__", py_func)
base_underlying = getattr(base_attr, "__func__", base_attr)
if base_underlying is own_underlying:
return True
if isinstance(base_attr, bpy.types.bpy_func):
return True
return False
def pyfunc2sphinx(
ident: str,
fw: WriteFn,
module_name: str,
type_name: str | None,
identifier: str,
py_func: Callable[..., object],
*,
struct: stub.InfoStructRNA | None,
is_class: bool = True,
) -> None:
"""
function or class method to sphinx
"""
if (py_class := pyfunc_owner_class(py_func, is_class, struct)) is not None:
# Skip RNA-backed methods - docs come from the RNA definition.
# Including them would list every operator's `poll` example (and similar)
# in the docs which isn't useful. Excluding all methods would over-reach
# however, hiding utility methods defined in `_bpy_types.py`.
bl_rna = getattr(py_class, "bl_rna", None)
if bl_rna is not None and bl_rna.functions.get(identifier) is not None:
return
# Skip inherited methods - docs appear on the defining base.
if pyfunc_is_inherited_method(py_class, py_func, identifier):
return
sig = inspect.signature(py_func)
arg_str = str(sig)
if USE_STUB_GEN:
if "<" in arg_str:
for p in sig.parameters.values():
if p.default is not inspect.Parameter.empty:
bad_repr = repr(p.default)
if bad_repr != (fixed_repr := repr_with_function_support(p.default)):
arg_str = arg_str.replace(bad_repr, fixed_repr)
if not is_class:
func_type = "function"
# The rest are class methods.
elif arg_str.startswith("(self, ") or arg_str == "(self)":
arg_str = "()" if (arg_str == "(self)") else ("(" + arg_str[7:])
func_type = "method"
elif arg_str.startswith("(cls, "):
arg_str = "()" if (arg_str == "(cls)") else ("(" + arg_str[6:])
func_type = "classmethod"
else:
if type(py_func) == MethodType:
func_type = "classmethod"
else:
func_type = "staticmethod"
doc = py_func.__doc__
if (not doc) or (not doc.startswith(".. {:s}:: ".format(func_type))):
fw(ident + ".. {:s}:: {:s}{:s}\n\n".format(func_type, identifier, arg_str))
ident_temp = ident + " "
else:
ident_temp = ident
if doc:
write_indented_lines(ident_temp, fw, doc)
fw("\n")
del doc, ident_temp
if is_class:
assert type_name is not None
write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier)
else:
write_example_ref(ident + " ", fw, module_name + "." + identifier)
def py_descr2sphinx(
ident: str,
fw: WriteFn,
descr: object,
module_name: str,
type_name: str,
identifier: str,
is_class: bool,
) -> None:
if identifier.startswith("_"):
return
doc = descr.__doc__
if type(descr) == GetSetDescriptorType:
# Exclude `aud`, ideally it should confirm to our naming convention, currently it doesn't.
# TODO: resolve upstream.
if module_name == "aud" or module_name.startswith("aud."):
pass
else:
if not doc:
print(
"C-API PyGetSetDef {:s}.{:s}.{:s} has empty/missing docstring".format(
module_name, type_name, identifier,
),
file=sys.stderr,
)
State.error_count += 1
elif ":type:" not in doc:
print(
"C-API PyGetSetDef {:s}.{:s}.{:s} docstring is missing ':type:'".format(
module_name, type_name, identifier,
),
file=sys.stderr,
)
State.error_count += 1
if not doc:
doc = undocumented_message(module_name, type_name, identifier)
if type(descr) == GetSetDescriptorType:
directive = "attribute" if is_class else "data"
fw(ident + ".. {:s}:: {:s}\n\n".format(directive, identifier))
# NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used).
write_indented_lines(ident + " ", fw, doc, False)
fw("\n")
elif type(descr) == MemberDescriptorType: # Same as above but use "data".
fw(ident + ".. data:: {:s}\n\n".format(identifier))
# NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used).
write_indented_lines(ident + " ", fw, doc, False)
fw("\n")
elif type(descr) in {MethodDescriptorType, ClassMethodDescriptorType}:
write_indented_lines(ident, fw, doc, False)
fw("\n")
else:
raise TypeError("type was not GetSetDescriptorType, MethodDescriptorType or ClassMethodDescriptorType")
write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier)
fw("\n")
def py_c_func2sphinx(
ident: str,
fw: WriteFn,
module_name: str,
type_name: str | None,
identifier: str,
py_func: Callable[..., object],
is_class: bool = True,
) -> None:
"""
C/C++ defined function to Sphinx.
"""
# Dump the docstring, assume its formatted correctly.
if py_func.__doc__:
write_indented_lines(ident, fw, py_func.__doc__, False)
fw("\n")
else:
fw(ident + ".. function:: {:s}()\n\n".format(identifier))
fw(ident + " " + undocumented_message(module_name, type_name, identifier))
if is_class:
assert type_name is not None
write_example_ref(ident + " ", fw, module_name + "." + type_name + "." + identifier)
else:
write_example_ref(ident + " ", fw, module_name + "." + identifier)
fw("\n")
def pyprop2sphinx(ident: str, fw: WriteFn, identifier: str, py_prop: stub.PyProperty) -> None:
"""
Python property to sphinx
"""
# Read-only properties use "data" directive, variables use "attribute" directive.
if py_prop.fset is None:
fw(ident + ".. data:: {:s}\n\n".format(identifier))
else:
fw(ident + ".. attribute:: {:s}\n\n".format(identifier))
# NOTE: `RST_NOINDEX_ATTR` currently not supported (as it's not used).
write_indented_lines(ident + " ", fw, py_prop.__doc__)
fw("\n")
if py_prop.fset is None:
fw(ident + " (readonly)\n\n")
def pymodule2sphinx(
basepath: Path,
module_name: str,
module: object,
title: str,
module_all_extra: Sequence[str],
) -> None:
import types
attribute_set: set[str] = set()
filepath = basepath / (module_name + ".rst")
module_all = getattr(module, "__all__", None)
module_dir = sorted(dir(module))
if module_all:
module_dir = module_all
# TODO: currently only used for classes.
# Grouping support.
module_grouping = MODULE_GROUPING.get(module_name)
def module_grouping_index(name: str) -> int:
if module_grouping is not None:
try:
return module_grouping.index(name)
except ValueError:
pass
return -1
def module_grouping_heading(name: str) -> tuple[str, str]:
if module_grouping is not None:
i = module_grouping_index(name) - 1
if i >= 0:
item = module_grouping[i]
if type(item) == tuple:
return item
return "", ""
def module_grouping_sort_key(name: str) -> int:
return module_grouping_index(name)
# Done grouping support.
file = open(filepath, "w", encoding="utf-8")
fw = file.write
fw(title_string("{:s} ({:s})".format(title, module_name), "="))
fw(".. module:: {:s}\n\n".format(module_name))
if module.__doc__:
# Note, may contain sphinx syntax, don't mangle!
fw(module.__doc__.strip())
fw("\n\n")
# Write sub-modules.
# We could also scan files but this ensures `__all__` is used correctly.
if module_all or module_all_extra:
submod_ls: list[tuple[str, object]] = []
for submod_name in (module_all or ()):
submod = import_value_from_module(module_name, submod_name)
if type(submod) == types.ModuleType:
submod_ls.append((submod_name, submod))
for submod_name in module_all_extra:
if submod_name in attribute_set:
continue
submod = import_value_from_module(module_name, submod_name)
# No type checks, since there are non-module types we treat as modules
# such as `bpy.app.translations` & `bpy.app.handlers`.
submod_ls.append((submod_name, submod))
if submod_ls:
fw(".. toctree::\n")
fw(" :maxdepth: 1\n")
fw(" :caption: Submodules\n\n")
for submod_name, submod in submod_ls:
submod_name_full = "{:s}.{:s}".format(module_name, submod_name)
fw(" {:s}.rst\n".format(submod_name_full))
pymodule2sphinx(basepath, submod_name_full, submod, "{:s} submodule".format(module_name), ())
fw("\n")
del submod_ls
# Done writing sub-modules!
write_example_ref("", fw, module_name)
# Write members of the module.
# Only tested with `PyStructs` which are not exactly modules.
for key, descr in sorted(type(module).__dict__.items()):
if key.startswith("__"):
continue
if key in module_all_extra:
continue
# Naughty! We also add `getset` to `PyStruct`, this is not typical Python but also not incorrect.
# `type_name` is only used for examples and messages:
# `<class 'bpy.app.handlers'>` -> `bpy.app.handlers`.
type_name = str(type(module)).strip("<>").split(" ", 1)[-1][1:-1]
# The type typically contains the module in the case of PyStruct's (defined by Blender).
# Assign a temporary module name: `module_name_split`.
if module_name == type_name:
assert "." in module_name
module_name_split, type_name = module_name.rpartition(".")[0::2]
elif type_name.startswith(module_name + "."):
type_name = type_name.removeprefix(module_name + ".")
else:
module_name_split = module_name
if type(descr) == types.GetSetDescriptorType:
py_descr2sphinx("", fw, descr, module_name_split, type_name, key, is_class=False)
attribute_set.add(key)
del module_name_split
descr_sorted = []
for key, descr in sorted(type(module).__dict__.items()):
if key.startswith("__"):
continue
if type(descr) == MemberDescriptorType:
if descr.__doc__:
value = getattr(module, key, None)
value_type = type(value)
descr_sorted.append((key, descr, value, type(value)))
# Sort by the value type.
descr_sorted.sort(key=lambda descr_data: str(descr_data[3]))
for key, descr, value, value_type in descr_sorted:
if key in module_all_extra:
continue
# Must be documented as a sub-module.
if is_struct_seq(value):
continue
type_name = value_type.__name__
py_descr2sphinx("", fw, descr, module_name, type_name, key, is_class=False)
attribute_set.add(key)
del key, descr, descr_sorted
classes = []
submodules = []
# Use this list so we can sort by type.
module_dir_value_type = []
for attribute in module_dir:
if is_attr_private(module_name, attribute):
continue
if attribute in attribute_set:
continue
if attribute.startswith("n_"): # Annoying exception, needed for `bpy.app`.
continue
# Workaround for `bpy.app` documenting `.index()` and `.count()`.
if isinstance(module, tuple) and hasattr(tuple, attribute):
continue
value = getattr(module, attribute)
module_dir_value_type.append((attribute, value, type(value)))
# Sort by `str` of each type this way lists, functions etc are grouped.
module_dir_value_type.sort(key=lambda triple: str(triple[2]))
for attribute, value, value_type in module_dir_value_type:
if attribute in module_all_extra:
continue
if value_type == FunctionType:
pyfunc2sphinx("", fw, module_name, None, attribute, value, struct=None, is_class=False)
# Both the same at the moment but to be future proof.
elif value_type in {types.BuiltinMethodType, types.BuiltinFunctionType}:
# NOTE: can't get args from these, so dump the string as is
# this means any module used like this must have fully formatted docstrings.
py_c_func2sphinx("", fw, module_name, None, attribute, value, is_class=False)
elif value_type == type:
classes.append((attribute, value))
elif issubclass(value_type, types.ModuleType):
submodules.append((attribute, value))
elif issubclass(value_type, (bool, int, float, str, tuple)):
# Constant, not much fun we can do here except to list it.
# TODO: figure out some way to document these!
fw(".. data:: {:s}\n\n".format(attribute))
write_indented_lines(" ", fw, "Constant value {!r}".format(value), False)
fw("\n")
fw(" :type: {:s}\n\n".format(value_type.__name__))
else:
BPY_LOGGER.debug("\tnot documenting %s.%s of %r type", module_name, attribute, value_type.__name__)
continue
attribute_set.add(attribute)
# TODO: more types.
del module_dir_value_type
# TODO: `bpy_extras` does this already, `mathutils` not.
"""
if submodules:
fw("\n"
"**********\n"
"Submodules\n"
"**********\n"
"\n"
)
for attribute, submod in submodules:
fw("- :mod:`{:s}.{:s}`\n".format(module_name, attribute))
fw("\n")
"""
if module_grouping is not None:
classes.sort(key=lambda pair: module_grouping_sort_key(pair[0]))
# Write collected classes now.
for (type_name, value) in classes:
if module_grouping is not None:
heading, heading_char = module_grouping_heading(type_name)
if heading:
fw(title_string(heading, heading_char))
pyclass2sphinx(fw, module_name, type_name, value, True)
file.close()
class DunderParam(NamedTuple):
"""A documented parameter on a dunder method: RST type and description."""
param_type: str
description: str
class DunderOverload(NamedTuple):
"""A single ``@overload`` entry for a dunder: operand type and return type (RST)."""
operand_type: str
return_type: str
class DunderInfo(NamedTuple):
"""
Everything we know about a dunder (protocol) method.
Used both as a base entry (in ``DUNDER_METHODS``, all fields populated) and
as a per-class partial override (in ``CLASS_OVERRIDES``, only set fields are
applied via ``merge_dunder_info``). All fields are optional so overrides
can specify just what they change.
Field semantics:
- ``signature``: bracketed parameter list literally inserted into the directive
(e.g. ``"(other)"``, ``"()"``).
- ``params``: maps each parameter name to its ``DunderParam``.
- ``return_type``: RST rtype string, ``"self"`` for the enclosing class,
or ``""`` to omit the ``:rtype:`` field entirely.
- ``overloads``: when set, write one ``.. method::`` directive per entry
(subsequent entries use ``:noindex:``); in stubs this becomes ``@overload``.
"""
signature: str | None = None
params: dict[str, DunderParam] | None = None
return_type: str | None = None
overloads: list[DunderOverload] | None = None
def merge_dunder_info(base: DunderInfo, override: DunderInfo | None) -> DunderInfo:
"""Apply *override* over *base*; ``None`` fields on the override inherit from base."""
if override is None:
return base
return base._replace(**{
field: value for field, value in override._asdict().items() if value is not None
})
OTHER_OPERAND = {"other": DunderParam("Self", "The other operand.")}
DUNDER_METHODS: dict[str, DunderInfo] = {
# Binary arithmetic operators.
"__add__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__radd__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__sub__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__rsub__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__mul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__rmul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__truediv__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__rtruediv__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__matmul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__rmatmul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
# Unary operators.
"__neg__": DunderInfo("()", {}, "self"),
"__pos__": DunderInfo("()", {}, "self"),
"__invert__": DunderInfo("()", {}, "self"),
# In-place arithmetic operators.
"__iadd__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__isub__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__imul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__itruediv__": DunderInfo("(other)", OTHER_OPERAND, "self"),
"__imatmul__": DunderInfo("(other)", OTHER_OPERAND, "self"),
# Comparison and hashing.
# `__eq__` / `__ne__` take `object` (not `Self`) per Python convention -
# `a == b` is valid for any pair of types, even if it just returns False.
"__eq__": DunderInfo(
"(other)", {"other": DunderParam("object", "The other operand.")}, "bool"),
"__ne__": DunderInfo(
"(other)", {"other": DunderParam("object", "The other operand.")}, "bool"),
"__lt__": DunderInfo("(other)", OTHER_OPERAND, "bool"),
"__le__": DunderInfo("(other)", OTHER_OPERAND, "bool"),
"__gt__": DunderInfo("(other)", OTHER_OPERAND, "bool"),
"__ge__": DunderInfo("(other)", OTHER_OPERAND, "bool"),
"__hash__": DunderInfo("()", {}, "int"),
# Container protocol.
"__len__": DunderInfo("()", {}, "int"),
"__getitem__": DunderInfo("(key)", {"key": DunderParam("int", "Index or key.")}, "float"),
"__setitem__": DunderInfo("(key, value)", {
"key": DunderParam("int", "Index or key."),
"value": DunderParam("object", "Value to assign."),
}, ""),
"__contains__": DunderInfo(
"(item)", {"item": DunderParam("object", "Item to test for membership.")}, "bool"),
# String representation.
"__repr__": DunderInfo("()", {}, "str"),
"__str__": DunderInfo("()", {}, "str"),
# Iteration.
"__iter__": DunderInfo("()", {}, "self"),
"__next__": DunderInfo("()", {}, "Any"),
# Context manager protocol.
"__enter__": DunderInfo("()", {}, "self"),
"__exit__": DunderInfo("(exc_type, exc_value, traceback)", {
"exc_type": DunderParam("type | None", "Exception type, or ``None``."),
"exc_value": DunderParam("BaseException | None", "Exception instance, or ``None``."),
"traceback": DunderParam("BaseException | None", "Traceback object, or ``None``."),
}, "bool"),
}
del OTHER_OPERAND
class ClassOverrides(NamedTuple):
"""
Per-class customizations applied during RST generation. All fields optional.
Add new override categories as fields here rather than introducing
additional purpose-specific dicts.
"""
# Per-dunder partial overrides merged onto `DUNDER_METHODS` entries.
# Each value's None fields inherit from the base entry.
dunders: dict[str, DunderInfo] | None = None
# Dunder names to skip even when the slot is present on the type.
# Use for slots that exist in the type's protocol struct but always raise
# (e.g. `Vector.__imatmul__` exists but unconditionally errors).
excluded_dunders: set[str] | None = None
CLASS_OVERRIDES: dict[tuple[str, str], ClassOverrides] = {
("mathutils", "Matrix"): ClassOverrides(
dunders={
"__getitem__": DunderInfo(return_type=":class:`Vector`"),
# `Matrix_mul` accepts element-wise (Matrix) or scalar (float).
"__mul__": DunderInfo(overloads=[
DunderOverload(":class:`Matrix`", ":class:`Matrix`"),
DunderOverload("float", ":class:`Matrix`"),
]),
# `Matrix_imul` accepts element-wise (Matrix) or scalar (float).
"__imul__": DunderInfo(overloads=[
DunderOverload(":class:`Matrix`", ":class:`Matrix`"),
DunderOverload("float", ":class:`Matrix`"),
]),
# `__rmul__` only fires for `float * mat` (mat * mat goes through `__mul__`).
"__rmul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# Matrix multiplication: see `Matrix_matmul` in `mathutils_Matrix.cc`.
"__matmul__": DunderInfo(overloads=[
DunderOverload(":class:`Matrix`", ":class:`Matrix`"),
DunderOverload(":class:`Vector`", ":class:`Vector`"),
]),
},
# Ordering ops are NotImplemented; reverse arithmetic ops error on
# non-Matrix LHS. (`__rmul__` is kept: `Matrix_mul` handles `float * mat`.)
excluded_dunders={
"__lt__", "__le__", "__gt__", "__ge__",
"__radd__", "__rsub__", "__rmatmul__",
},
),
("mathutils", "Color"): ClassOverrides(
dunders={
# `Color_mul` only accepts a scalar (col * col is unsupported).
"__mul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# `Color_imul` only accepts a scalar.
"__imul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# `__rmul__` only fires for `float * col` (col * col goes through `__mul__`).
"__rmul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# `Color_div` only accepts a scalar divisor.
"__truediv__": DunderInfo(params={"other": DunderParam("float", "Scalar divisor.")}),
# `Color_idiv` only accepts a scalar divisor.
"__itruediv__": DunderInfo(params={"other": DunderParam("float", "Scalar divisor.")}),
},
# Ordering ops are NotImplemented; reverse arithmetic ops error on
# non-Color LHS. (`__rmul__` is kept: `Color_mul` handles `float * col`.)
excluded_dunders={
"__lt__", "__le__", "__gt__", "__ge__",
"__radd__", "__rsub__", "__rtruediv__",
},
),
("mathutils", "Euler"): ClassOverrides(
# `Euler_richcmpr` returns `NotImplemented` for ordering ops; only
# `__eq__` / `__ne__` are real. Ordering rotations isn't meaningful.
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("mathutils", "Vector"): ClassOverrides(
dunders={
# `Vector_mul` accepts element-wise (Vector) or scalar (float).
"__mul__": DunderInfo(overloads=[
DunderOverload(":class:`Vector`", ":class:`Vector`"),
DunderOverload("float", ":class:`Vector`"),
]),
# `Vector_imul` accepts element-wise (Vector) or scalar (float).
"__imul__": DunderInfo(overloads=[
DunderOverload(":class:`Vector`", ":class:`Vector`"),
DunderOverload("float", ":class:`Vector`"),
]),
# `__rmul__` only fires for `float * vec` (vec * vec goes through `__mul__`).
"__rmul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# `Vector_div` / `Vector_idiv` only accept a scalar divisor.
"__truediv__": DunderInfo(
params={"other": DunderParam("float", "Scalar divisor.")},
),
"__itruediv__": DunderInfo(
params={"other": DunderParam("float", "Scalar divisor.")},
),
# See `Vector_matmul` in `mathutils_Vector.cc`. Vector @ Vector is the dot product.
"__matmul__": DunderInfo(overloads=[
DunderOverload(":class:`Vector`", "float"),
DunderOverload(":class:`Matrix`", ":class:`Vector`"),
]),
},
# Reverse ops that error on non-Vector LHS. (`__rmul__` is kept:
# `Vector_mul` handles `float * vec`.)
excluded_dunders={
"__radd__",
"__rsub__",
"__rtruediv__",
"__rmatmul__",
# `Vector_imatmul` exists in `nb_inplace_matrix_multiply` but always
# raises TypeError, so `vec @= other` is unsupported in practice.
"__imatmul__",
},
),
("mathutils", "Quaternion"): ClassOverrides(
dunders={
# `Quaternion_mul` accepts element-wise (Quaternion) or scalar (float).
"__mul__": DunderInfo(overloads=[
DunderOverload(":class:`Quaternion`", ":class:`Quaternion`"),
DunderOverload("float", ":class:`Quaternion`"),
]),
# `Quaternion_imul` accepts element-wise (Quaternion) or scalar (float).
"__imul__": DunderInfo(overloads=[
DunderOverload(":class:`Quaternion`", ":class:`Quaternion`"),
DunderOverload("float", ":class:`Quaternion`"),
]),
# `__rmul__` only fires for `float * quat` (quat * quat goes through `__mul__`).
"__rmul__": DunderInfo(params={"other": DunderParam("float", "Scalar.")}),
# See `Quaternion_matmul` in `mathutils_Quaternion.cc`.
"__matmul__": DunderInfo(overloads=[
DunderOverload(":class:`Quaternion`", ":class:`Quaternion`"),
DunderOverload(":class:`Vector`", ":class:`Vector`"),
]),
},
# Ordering ops are NotImplemented; reverse arithmetic ops error on
# non-Quaternion LHS. (`__rmul__` is kept: `Quaternion_mul` handles
# `float * quat`.)
excluded_dunders={
"__lt__", "__le__", "__gt__", "__ge__",
"__radd__", "__rsub__", "__rmatmul__",
},
),
# bpy_prop_collection returns bpy_struct elements, not float.
("bpy.types", "bpy_prop_collection"): ClassOverrides(
dunders={
"__getitem__": DunderInfo(return_type=":class:`bpy_struct`"),
"__iter__": DunderInfo(return_type="typing.Iterator[:class:`bpy_struct`]"),
},
),
# `BPy_IDArray_GetItem` / `BPy_IDArray_SetItem` operate on values matching the
# array's `subtype` (float / double -> float, int -> int, boolean -> bool).
# A single array is typecode-homogeneous, so a slice yields a list of one
# of those types - matching `to_list()`'s rtype.
("idprop.types", "IDPropertyArray"): ClassOverrides(
dunders={
"__getitem__": DunderInfo(overloads=[
DunderOverload("int", "float | int | bool"),
DunderOverload("slice", "list[float] | list[int] | list[bool]"),
]),
"__setitem__": DunderInfo(
params={
"key": DunderParam("int", "Index."),
"value": DunderParam("float | int | bool", "Value to assign."),
},
),
},
),
# `BPy_IDGroup_Map_GetItem` / `BPy_IDGroup_Map_SetItem` require `str` keys
# and operate on wrapped values of varying types (see `BPy_IDGroup_WrapData`).
# `BPy_IDGroup_iter` returns the iterator from `ViewKeys`, which yields `str`.
("idprop.types", "IDPropertyGroup"): ClassOverrides(
dunders={
"__getitem__": DunderInfo(
params={"key": DunderParam("str", "Property name.")},
return_type="Any",
),
"__setitem__": DunderInfo(
params={
"key": DunderParam("str", "Property name."),
"value": DunderParam("Any", "Value to assign."),
},
),
"__iter__": DunderInfo(return_type=":class:`IDPropertyGroupIterKeys`"),
},
),
# The Iter / View types don't set `tp_richcompare`, so they inherit
# `object`'s slot wrappers; identity-based `__eq__`/`__ne__` work, but
# ordering ops raise TypeError at runtime. Drop the dead ones.
("idprop.types", "IDPropertyGroupIterKeys"): ClassOverrides(
# `BPy_Group_IterKeys_next` yields the property name as `str`.
dunders={"__next__": DunderInfo(return_type="str")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("idprop.types", "IDPropertyGroupIterValues"): ClassOverrides(
# `BPy_Group_IterValues_next` yields the wrapped property value.
dunders={"__next__": DunderInfo(return_type="Any")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("idprop.types", "IDPropertyGroupIterItems"): ClassOverrides(
# `BPy_Group_IterItems_next` yields a `(name, value)` tuple.
dunders={"__next__": DunderInfo(return_type="tuple[str, Any]")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("idprop.types", "IDPropertyGroupViewKeys"): ClassOverrides(
# `BPy_Group_ViewKeys_iter` returns a fresh `IterKeys`, not the view itself.
dunders={"__iter__": DunderInfo(return_type=":class:`IDPropertyGroupIterKeys`")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("idprop.types", "IDPropertyGroupViewValues"): ClassOverrides(
# `BPy_Group_ViewValues_iter` returns a fresh `IterValues`.
dunders={"__iter__": DunderInfo(return_type=":class:`IDPropertyGroupIterValues`")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
("idprop.types", "IDPropertyGroupViewItems"): ClassOverrides(
# `BPy_Group_ViewItems_iter` returns a fresh `IterItems`.
dunders={"__iter__": DunderInfo(return_type=":class:`IDPropertyGroupIterItems`")},
excluded_dunders={"__lt__", "__le__", "__gt__", "__ge__"},
),
# bmesh element sequence types return their element, not float.
("bmesh.types", "BMElemSeq"): ClassOverrides(
dunders={"__getitem__": DunderInfo(
return_type=":class:`BMVert` | :class:`BMEdge` | :class:`BMFace`")},
),
("bmesh.types", "BMVertSeq"): ClassOverrides(
dunders={"__getitem__": DunderInfo(return_type=":class:`BMVert`")},
),
("bmesh.types", "BMEdgeSeq"): ClassOverrides(
dunders={"__getitem__": DunderInfo(return_type=":class:`BMEdge`")},
),
("bmesh.types", "BMFaceSeq"): ClassOverrides(
dunders={"__getitem__": DunderInfo(return_type=":class:`BMFace`")},
),
("bmesh.types", "BMEditSelSeq"): ClassOverrides(
dunders={"__getitem__": DunderInfo(
return_type=":class:`BMVert` | :class:`BMEdge` | :class:`BMFace`")},
),
("bmesh.types", "BMLayerCollection"): ClassOverrides(
dunders={"__getitem__": DunderInfo(return_type=":class:`BMLayerItem`")},
),
}
EMPTY_CLASS_OVERRIDES = ClassOverrides()
def write_dunder_methods(fw: WriteFn, class_value: type, module_name: str, type_name: str) -> None:
"""
Write known dunder (protocol) methods defined on this type.
A dunder counts as defined here when it appears directly in
``class_value.__dict__`` with a non-``None`` value. Inherited dunders are
documented on the defining base; a ``None`` entry (e.g. ``dict.__hash__``)
marks the type as opting out and is skipped.
"""
overrides = CLASS_OVERRIDES.get((module_name, type_name), EMPTY_CLASS_OVERRIDES)
excluded = overrides.excluded_dunders or set()
dunder_keys = {
key for key in DUNDER_METHODS
if key not in excluded and class_value.__dict__.get(key) is not None
}
if not dunder_keys:
return
# Wrap in a `.. details::` block (defined in `conf.py`)
# so the underscore methods are nested in a `<details>` drop-down.
# The stub generator handles this so it has no effect on the generated stub.
fw(" .. details:: Special Methods\n\n")
class_dunders = overrides.dunders or {}
for key in sorted(dunder_keys):
info = merge_dunder_info(DUNDER_METHODS[key], class_dunders.get(key))
# Base entries always populate signature/params/return_type, so these are non-None here.
assert info.signature is not None and info.params is not None and info.return_type is not None
if info.overloads is not None:
assert len(info.params) == 1, "dunder overloads only supported for single-param dunders"
(pname, param), = info.params.items()
for i, overload in enumerate(info.overloads):
fw(" .. method:: {:s}{:s}\n".format(key, info.signature))
# `:noindex:` on subsequent overloads keeps Sphinx from raising
# duplicate-target warnings.
if i > 0:
fw(" :noindex:\n")
fw("\n")
fw(" :param {:s}: {:s}\n".format(pname, param.description))
fw(" :type {:s}: {:s}\n".format(pname, overload.operand_type))
fw(" :rtype: {:s}\n\n".format(overload.return_type))
continue
rtype = info.return_type
fw(" .. method:: {:s}{:s}\n\n".format(key, info.signature))
for pname, param in info.params.items():
fw(" :param {:s}: {:s}\n".format(pname, param.description))
fw(" :type {:s}: {:s}\n".format(pname, param.param_type))
if rtype == "self":
fw(" :rtype: :class:`{:s}`\n\n".format(type_name))
elif rtype:
fw(" :rtype: {:s}\n\n".format(rtype))
else:
fw("\n")
def pyclass2sphinx(
fw: WriteFn,
module_name: str,
type_name: str,
value: type,
write_class_examples: bool,
) -> None:
# NOTE: for `.. class::` identifiers, the type name alone is enough
# because the module has already been set via `.. module::`.
if value.__doc__:
if value.__doc__.startswith(".. class::"):
fw(value.__doc__)
else:
fw(".. class:: {:s}\n\n".format(type_name))
write_indented_lines(" ", fw, value.__doc__, True)
else:
fw(".. class:: {:s}\n\n".format(type_name))
fw("\n")
if write_class_examples:
write_example_ref(" ", fw, module_name + "." + type_name)
descr_items = [(key, descr) for key, descr in sorted(value.__dict__.items()) if not key.startswith("_")]
for key, descr in descr_items:
if type(descr) == ClassMethodDescriptorType:
py_descr2sphinx(" ", fw, descr, module_name, type_name, key, is_class=True)
# Needed for pure Python classes.
for key, descr in descr_items:
if type(descr) == FunctionType:
pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, struct=None, is_class=True)
for key, descr in descr_items:
if type(descr) == MethodDescriptorType:
py_descr2sphinx(" ", fw, descr, module_name, type_name, key, is_class=True)
for key, descr in descr_items:
if type(descr) == GetSetDescriptorType:
py_descr2sphinx(" ", fw, descr, module_name, type_name, key, is_class=True)
# Needed for pure Python classes.
for key, descr in descr_items:
if type(descr) == classmethod:
descr = getattr(value, key)
pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, struct=None, is_class=True)
for key, descr in descr_items:
if type(descr) == StaticMethodType:
descr = getattr(value, key)
if type(descr) in {BuiltinMethodType, BuiltinFunctionType}:
# CAPI-defined static methods already contain RST directives
# in their docstrings, write them directly.
write_indented_lines(" ", fw, descr.__doc__ or "Undocumented", False)
fw("\n")
else:
# Python-defined static methods need signature extraction.
pyfunc2sphinx(" ", fw, module_name, type_name, key, descr, struct=None, is_class=True)
if USE_STUB_GEN:
write_dunder_methods(fw, value, module_name, type_name)
fw("\n\n")
# Changes In Blender will force errors here.
context_type_map = {
# Support multiple types for each item, where each list item is a possible type:
# `context_member: [(RNA type, is_collection), ...]`
"active_action": [("Action", False)],
"active_annotation_layer": [("AnnotationLayer", False)],
"active_bone": [("EditBone", False), ("Bone", False)],
"active_file": [("FileSelectEntry", False)],
"active_node": [("Node", False)],
"active_object": [("Object", False)],
"active_operator": [("Operator", False)],
"active_pose_bone": [("PoseBone", False)],
"active_strip": [("Strip", False)],
"active_editable_fcurve": [("FCurve", False)],
"active_nla_strip": [("NlaStrip", False)],
"active_nla_track": [("NlaTrack", False)],
"annotation_data": [("GreasePencil", False)],
"annotation_data_owner": [("ID", False)],
"armature": [("Armature", False)],
"asset": [("AssetRepresentation", False)],
"asset_library_reference": [("AssetLibraryReference", False)],
"bone": [("Bone", False)],
"brush": [("Brush", False)],
"camera": [("Camera", False)],
"cloth": [("ClothModifier", False)],
"collection": [("LayerCollection", False)],
"collision": [("CollisionModifier", False)],
"curve": [("Curve", False)],
"dynamic_paint": [("DynamicPaintModifier", False)],
"edit_bone": [("EditBone", False)],
"edit_image": [("Image", False)],
"edit_mask": [("Mask", False)],
"edit_movieclip": [("MovieClip", False)],
"edit_object": [("Object", False)],
"edit_text": [("Text", False)],
"editable_bones": [("EditBone", True)],
"editable_objects": [("Object", True)],
"editable_fcurves": [("FCurve", True)],
"fluid": [("FluidModifier", False)],
"gpencil": [("GreasePencil", False)],
"grease_pencil": [("GreasePencil", False)],
"curves": [("Curves", False)],
"id": [("ID", False)],
"image_paint_object": [("Object", False)],
"lattice": [("Lattice", False)],
"light": [("Light", False)],
"lightprobe": [("LightProbe", False)],
"line_style": [("FreestyleLineStyle", False)],
"markers": [("TimelineMarker", True)],
"material": [("Material", False)],
"material_slot": [("MaterialSlot", False)],
"mesh": [("Mesh", False)],
"meta_ball": [("MetaBall", False)],
"object": [("Object", False)],
"objects_in_mode": [("Object", True)],
"objects_in_mode_unique_data": [("Object", True)],
"particle_edit_object": [("Object", False)],
"particle_settings": [("ParticleSettings", False)],
"particle_system": [("ParticleSystem", False)],
"particle_system_editable": [("ParticleSystem", False)],
"pointcloud": [("PointCloud", False)],
"pose_bone": [("PoseBone", False)],
"pose_object": [("Object", False)],
"property": [("AnyType", False), ("str", False), ("int", False)],
"scene": [("Scene", False)],
"sculpt_object": [("Object", False)],
"selectable_objects": [("Object", True)],
"selected_assets": [("AssetRepresentation", True)],
"selected_bones": [("EditBone", True)],
"selected_editable_actions": [("Action", True)],
"selected_editable_bones": [("EditBone", True)],
"selected_editable_fcurves": [("FCurve", True)],
"selected_editable_keyframes": [("Keyframe", True)],
"selected_editable_objects": [("Object", True)],
"selected_editable_strips": [("Strip", True)],
"selected_files": [("FileSelectEntry", True)],
"selected_ids": [("ID", True)],
"selected_nla_strips": [("NlaStrip", True)],
"selected_markers": [("TimelineMarker", True)],
"selected_movieclip_tracks": [("MovieTrackingTrack", True)],
"selected_nodes": [("Node", True)],
"selected_objects": [("Object", True)],
"selected_pose_bones": [("PoseBone", True)],
"selected_pose_bones_from_active_object": [("PoseBone", True)],
"selected_strips": [("Strip", True)],
"selected_visible_actions": [("Action", True)],
"selected_visible_fcurves": [("FCurve", True)],
"sequencer_scene": [("Scene", False)],
"strips": [("Strip", True)],
"strip": [("Strip", False)],
"strip_modifier": [("StripModifier", False)],
"soft_body": [("SoftBodyModifier", False)],
"speaker": [("Speaker", False)],
"texture": [("Texture", False)],
"texture_node": [("Node", False)],
"texture_slot": [("TextureSlot", False)],
"texture_user": [("ID", False)],
"texture_user_property": [("Property", False)],
"tool_settings": [("ToolSettings", False)],
"ui_list": [("UIList", False)],
"vertex_paint_object": [("Object", False)],
"view_layer": [("ViewLayer", False)],
"visible_bones": [("EditBone", True)],
"visible_objects": [("Object", True)],
"visible_pose_bones": [("PoseBone", True)],
"visible_fcurves": [("FCurve", True)],
"weight_paint_object": [("Object", False)],
"volume": [("Volume", False)],
"world": [("World", False)],
}
if bpy.app.build_options.experimental_features:
experimental: dict[str, list[tuple[str, bool]]] = {
# No experimental members in context currently.
}
for key, value in experimental.items():
assert key not in context_type_map, "Duplicate, the member must be removed from one of the dictionaries"
context_type_map[key] = value
del experimental
def format_operator_as_module(op_id: str) -> str:
# `FOO_OT_bar` -> `foo.bar`.
mod, _, fn = op_id.partition("_OT_")
assert fn, "Expected to have an `_OT_` separator."
return "{:s}.{:s}".format(mod.lower(), fn)
def format_description_and_type_info(description: str, type_info: Sequence[str]) -> str:
if type_info:
# Add some information at the end of the description,
# this doesn't really fit all that well anywhere, but it's often short and not worth
# the vertical space used by having its own line.
# However, if the description is already multi-line, then we do need to add a separate line,
# otherwise this would get mixed up in dot-points or notes.
if "\n" in description:
sep = "\n\n"
elif description:
sep = " "
else:
sep = ""
description = "{:s}{:s}({:s})".format(description, sep, ", ".join(type_info))
return description
def pycontext2sphinx(basepath: Path) -> None:
# Not actually a module, only write this file so we can reference in the TOC.
filepath = basepath / "bpy.context.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Context Access (bpy.context)", "="))
fw(".. module:: bpy.context\n")
fw("\n")
fw("The context members available depend on the area of Blender which is currently being accessed.\n")
fw("\n")
fw("Note that all context values are read-only,\n")
fw("but may be modified through the data API or by running operators.\n")
fw("\n")
fw(".. data:: context\n")
fw("\n")
fw(" Access to the current window-manager and data context.\n")
fw("\n")
fw(" :type: :class:`bpy.types.Context`\n")
def pycontext_members2sphinx(ident: str, fw: WriteFn, written_props: set[str]) -> None:
# Write context members into `bpy.types.Context`.
# Track all unique properties to avoid duplicates.
unique = set(written_props)
# Internal API call only intended to be used to extract context members.
from _bpy import context_members
context_member_map = context_members()
del context_members
# Track all context strings to validate `context_type_map`.
unique_context_strings = set()
for ctx_str, ctx_members in sorted(context_member_map.items()):
subsection = "{:s} Context".format(ctx_str.split("_")[0].title())
fw("\n{:s}.. rubric:: {:s}\n\n".format(ident, subsection))
for member in ctx_members:
unique_all_len = len(unique)
unique.add(member)
member_visited = unique_all_len == len(unique)
unique_context_strings.add(member)
fw("{:s}.. data:: {:s}\n".format(ident, member))
# Avoid warnings about the member being included multiple times.
if member_visited:
fw("{:s} :noindex:\n".format(ident))
fw("\n")
if (member_types := context_type_map.get(member)) is None:
raise SystemExit(
"Error: context key {!r} not found in context_type_map; update {:s}".format(member, __file__)
) from None
if len(member_types) == 0:
raise SystemExit(
"Error: context key {!r} empty in context_type_map; update {:s}".format(member, __file__)
)
type_strs = []
for member_type, is_seq in member_types:
if member_type.isidentifier():
class_str = ":class:`{:s}`".format(member_type)
if is_seq:
type_strs.append("list[{:s}]".format(class_str))
else:
type_strs.append(class_str)
else:
type_strs.append(member_type)
fw("{:s} :type: {:s}\n\n".format(ident, " | ".join(type_strs)))
write_example_ref(ident + " ", fw, "bpy.context." + member)
# A bit of a hack: add a trailing rubric so that the methods which follow
# aren't visually grouped under the last context heading.
fw("\n{:s}.. rubric:: Methods\n\n".format(ident))
# Generate type-map:
# for member in sorted(unique_context_strings):
# print(' "{:s}": ("", False),'.format(member))
if len(context_type_map) > len(unique_context_strings):
warnings.warn(
"Some types are not used: {:s}".format(
str([member for member in context_type_map if member not in unique_context_strings]),
))
else:
pass # Will have raised an error above.
def pyrna_enum2sphinx(prop: stub.InfoPropertyRNA, use_empty_descriptions: bool = False) -> str:
"""
Write a bullet point list of enum + descriptions.
"""
# Write a link to the enum if this is part of `rna_enum_pointer_map`.
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
if (result := pyrna_enum2sphinx_shared_link(prop)) is not None:
return result
if use_empty_descriptions:
ok = True
else:
ok = False
for identifier, name, description in prop.enum_items:
if description:
ok = True
break
if ok:
return "".join([
"- ``{:s}``\n"
"{:s}.\n".format(
identifier,
# Account for multi-line enum descriptions, allowing this to be a block of text.
indent(" -- ".join(escape_rst(val) for val in (name, description) if val) or "Undocumented", " "),
)
for identifier, name, description in prop.enum_items
])
return ""
def pyrna_deprecated_directive(ident: str, deprecated: stub.RnaDeprecated) -> str:
note, version, removal_version = deprecated
# Show a short 2 number version where possible to reduce noise.
version_str = "{:d}.{:d}.{:d}".format(*version).removesuffix(".0")
removal_version_str = "{:d}.{:d}.{:d}".format(*removal_version).removesuffix(".0")
return (
"{:s}.. deprecated:: {:s} removal planned in version {:s}\n"
"\n"
"{:s} {:s}\n"
).format(
ident, version_str, removal_version_str,
ident, note,
)
def pyrna2sphinx(basepath: Path) -> None:
"""
``bpy.types`` and ``bpy.ops``.
"""
# `structs, funcs, ops, props = rna_info.BuildRNAInfo()`
structs, _funcs, ops, _props = rna_info_BuildRNAInfo_cache()
if USE_ONLY_BUILTIN_RNA_TYPES:
# Ignore properties that use non `bpy.types` properties.
structs_exclude = {
v.identifier for v in structs.values()
if v.module_name != "bpy.types"
}
for v in structs.values():
for p in v.properties:
for identifier in (
getattr(p.srna, "identifier", None),
getattr(p.fixed_type, "identifier", None),
):
if identifier is not None:
if identifier in structs_exclude:
RNA_EXCLUDE.setdefault(v.identifier, set()).add(identifier)
del structs_exclude
structs = {
k: v for k, v in structs.items()
if v.module_name == "bpy.types"
}
if GLOBAL.filter_bpy_types is not None:
structs = {
k: v for k, v in structs.items()
if k[1] in GLOBAL.filter_bpy_types
if v.module_name == "bpy.types"
}
if GLOBAL.filter_bpy_ops is not None:
ops = {k: v for k, v in ops.items() if v.module_name in GLOBAL.filter_bpy_ops}
# Build set of struct identifiers that are collection wrappers
# (e.g. BlendDataObjects wraps `BlendData.objects`). These should
# inherit from `bpy_prop_collection` rather than `bpy_struct`.
_collection_wrapper_ids: set[str] = set()
for _struct in structs.values():
for _prop in _struct.properties:
if _prop.type == "collection" and _prop.srna is not None:
_collection_wrapper_ids.add(_prop.srna.identifier)
def write_param(ident: str, fw: WriteFn, prop: stub.InfoPropertyRNA, is_return: bool = False) -> None:
if is_return:
id_name = "return"
id_type = "rtype"
identifier = ""
else:
id_name = "param"
id_type = "type"
identifier = " {:s}".format(prop.identifier)
enum_descr_override: str | None = None
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
enum_descr_override = pyrna_enum2sphinx_shared_link(prop)
type_descr, type_info = prop.get_type_description(
as_arg=not is_return,
as_ret=is_return,
class_fmt=":class:`{:s}`",
mathutils_fmt=":class:`mathutils.{:s}`",
literal_fmt="``{!r}``", # String with quotes.
collection_id=_BPY_PROP_COLLECTION_ID,
enum_descr_override=enum_descr_override,
)
# Only for `bpy.ops.*` parameters.
if prop.fixed_type is not None and prop.fixed_type.is_operator_properties():
# Support operator macros where properties from another operator are passed in.
# These *would* be an `OperatorProperties` type, however, there isn't a convenient
# way to construct this data type, so - coercing them from a `dict` is supported.
# Since this is a special case in the RNA API, we need to override the type.
# Link to the operator to find the supported arguments.
type_info.append(
":func:`bpy.ops.{:s}` keyword arguments".format(format_operator_as_module(prop.identifier)),
)
type_descr = "dict[str, Any]"
prop_name = prop.name
prop_description = format_description_and_type_info(prop.description, type_info)
# If the link has been written, no need to inline the enum items.
enum_text = "" if enum_descr_override else pyrna_enum2sphinx(prop)
# Don't accidentally use this again (some value have been manipulated).
del prop
if prop_name or prop_description or enum_text:
fw(ident + ":{:s}{:s}: ".format(id_name, identifier))
if prop_name or prop_description:
fw(", ".join(val for val in (prop_name, prop_description.replace("\n", "")) if val) + "\n")
# Special exception, can't use generic code here for enums.
if enum_text:
fw("\n")
write_indented_lines(ident + " ", fw, enum_text)
del enum_text
# End enum exception.
fw(ident + ":{:s}{:s}: {:s}\n".format(id_type, identifier, type_descr))
def write_struct(struct: stub.InfoStructRNA) -> None:
# if not struct.identifier.startswith("Sc") and not struct.identifier.startswith("I"):
# return
# if not struct.identifier == "Object":
# return
struct_module_name = struct.module_name
if USE_ONLY_BUILTIN_RNA_TYPES:
assert struct_module_name == "bpy.types"
filepath = basepath / "{:s}.{:s}.rst".format(struct_module_name, struct.identifier)
file = open(filepath, "w", encoding="utf-8")
fw = file.write
base_id = getattr(struct.base, "identifier", "")
struct_id = struct.identifier
if USE_PYCAPI_TYPES:
if not base_id:
# Collection wrapper structs inherit from `bpy_prop_collection`,
# all other root structs inherit from `bpy_struct`.
if struct_id in _collection_wrapper_ids:
base_id = _BPY_PROP_COLLECTION_PYCAPI
else:
base_id = _BPY_STRUCT_PYCAPI
if base_id:
title = "{:s}({:s})".format(struct_id, base_id)
else:
title = struct_id
fw(title_string(title, "="))
fw(".. currentmodule:: {:s}\n\n".format(struct_module_name))
# Docs first? OK.
write_example_ref("", fw, "{:s}.{:s}".format(struct_module_name, struct_id))
base_ids = [base.identifier for base in struct.get_bases()]
if USE_PYCAPI_TYPES:
if not base_ids:
if struct_id in _collection_wrapper_ids:
base_ids.append(_BPY_PROP_COLLECTION_PYCAPI)
else:
base_ids.append(_BPY_STRUCT_PYCAPI)
else:
base_ids.append(_BPY_STRUCT_PYCAPI)
base_ids.reverse()
if base_ids:
if len(base_ids) > 1:
fw("base classes --- ")
else:
fw("base class --- ")
fw(", ".join((":class:`{:s}`".format(base_id)) for base_id in base_ids))
fw("\n\n")
subclass_ids = [
s.identifier for s in structs.values()
if s.base is struct
if not rna_info.rna_id_ignore(s.identifier)
]
subclass_ids.sort()
if subclass_ids:
fw(".. toctree::\n")
# Add a title and limit to direct sub classes.
fw(" :caption: Subclasses\n")
fw(" :maxdepth: 1\n\n")
for s in subclass_ids:
fw(" {:s}.{:s}.rst\n".format(struct_module_name, s))
fw("\n")
base_id = getattr(struct.base, "identifier", "")
if USE_PYCAPI_TYPES:
if not base_id:
if struct_id in _collection_wrapper_ids:
base_id = _BPY_PROP_COLLECTION_PYCAPI
else:
base_id = _BPY_STRUCT_PYCAPI
if base_id:
fw(".. class:: {:s}({:s})\n\n".format(struct_id, base_id))
else:
fw(".. class:: {:s}\n\n".format(struct_id))
write_indented_lines(" ", fw, struct.description, False)
fw("\n")
# Properties sorted in alphabetical order.
sorted_struct_properties = sorted(struct.properties, key=lambda prop: prop.identifier)
# Support excluding props.
struct_exclude = RNA_EXCLUDE.get(struct_id, ())
for prop in sorted_struct_properties:
identifier = prop.identifier
# Support excluding props.
if identifier in struct_exclude:
continue
enum_descr_override = None
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
enum_descr_override = pyrna_enum2sphinx_shared_link(prop)
type_descr, type_info = prop.get_type_description(
class_fmt=":class:`{:s}`",
mathutils_fmt=":class:`mathutils.{:s}`",
literal_fmt="``{!r}``", # String with quotes.
collection_id=_BPY_PROP_COLLECTION_ID,
enum_descr_override=enum_descr_override,
)
# Read-only properties use "data" directive, variables properties use "attribute" directive.
if "readonly" in type_info:
fw(" .. data:: {:s}\n".format(identifier))
else:
fw(" .. attribute:: {:s}\n".format(identifier))
# Also write `noindex` on request.
if ("bpy.types", struct_id, identifier) in RST_NOINDEX_ATTR:
fw(" :noindex:\n")
fw("\n")
prop_description = format_description_and_type_info(prop.description, type_info)
if prop_description:
write_indented_lines(" ", fw, prop_description, False)
fw("\n")
if (deprecated := prop.deprecated) is not None:
fw(pyrna_deprecated_directive(" ", deprecated))
fw("\n")
# Special exception, can't use generic code here for enums.
if prop.type == "enum":
# If the link has been written, no need to inline the enum items.
enum_text = "" if enum_descr_override else pyrna_enum2sphinx(prop)
if enum_text:
write_indented_lines(" ", fw, enum_text)
fw("\n")
del enum_text
# End enum exception.
fw(" :type: {:s}\n\n".format(type_descr))
# Screen context members (only for Context struct).
if struct_id == "Context":
written_props = {prop.identifier for prop in sorted_struct_properties
if prop.identifier not in struct_exclude}
pycontext_members2sphinx(" ", fw, written_props)
# Python attributes.
py_properties = struct.get_py_properties()
py_prop = None
for identifier, py_prop in py_properties:
pyprop2sphinx(" ", fw, identifier, py_prop)
del py_properties, py_prop
# C/Python attributes: `GetSetDescriptorType`.
key = descr = None
for key, descr in sorted(struct.get_py_c_properties_getset()):
py_descr2sphinx(" ", fw, descr, "bpy.types", struct_id, key, is_class=True)
del key, descr
for func in struct.functions:
args_kw_only_index = next((i for i, prop in enumerate(func.args) if not prop.is_required), -1)
if args_kw_only_index == -1:
args_str = ", ".join(prop.get_arg_default(force=False) for prop in func.args)
else:
args_str = ", ".join([
*[prop.get_arg_default(force=False) for prop in func.args[:args_kw_only_index]],
# Keyword only.
"*",
*[prop.get_arg_default(force=False) for prop in func.args[args_kw_only_index:]],
])
del args_kw_only_index
fw(" .. {:s}:: {:s}({:s})\n\n".format(
"classmethod" if func.is_classmethod else "method",
func.identifier,
args_str,
))
fw(" {:s}\n\n".format(func.description))
for prop in func.args:
write_param(" ", fw, prop)
if len(func.return_values) == 1:
write_param(" ", fw, func.return_values[0], is_return=True)
elif func.return_values: # Multiple return values.
fw(" :return:\n")
type_descrs = []
for prop in func.return_values:
# TODO: pyrna_enum2sphinx for multiple return values,
# actually don't think we even use this but still!
enum_descr_override = None
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
enum_descr_override = pyrna_enum2sphinx_shared_link(prop)
type_descr, _type_info = prop.get_type_description(
as_ret=True, class_fmt=":class:`{:s}`",
mathutils_fmt=":class:`mathutils.{:s}`",
literal_fmt="``{!r}``", # String with quotes.
collection_id=_BPY_PROP_COLLECTION_ID,
enum_descr_override=enum_descr_override,
)
type_descrs.append(type_descr)
descr = prop.description
if not descr:
descr = prop.name
# In rare cases `descr` may be empty.
fw(" ``{:s}``, {:s}\n\n".format(
prop.identifier,
", ".join((val for val in (descr, type_descr) if val))
))
if (deprecated := prop.deprecated) is not None:
fw(pyrna_deprecated_directive(" ", deprecated))
fw("\n")
fw(" :rtype: tuple[{:s}]\n".format(", ".join(type_descrs)))
write_example_ref(" ", fw, struct_module_name + "." + struct_id + "." + func.identifier)
fw("\n")
# Python methods.
py_funcs = struct.get_py_functions()
py_func = None
for identifier, py_func in py_funcs:
pyfunc2sphinx(" ", fw, "bpy.types", struct_id, identifier, py_func, struct=struct, is_class=True)
del py_funcs, py_func
py_funcs = struct.get_py_c_functions()
py_func = None
for identifier, py_func in py_funcs:
py_c_func2sphinx(" ", fw, "bpy.types", struct_id, identifier, py_func, is_class=True)
lines: list[str] = []
if struct.base or USE_PYCAPI_TYPES:
bases = list(reversed(struct.get_bases()))
# Properties.
del lines[:]
if USE_PYCAPI_TYPES:
descr_items = [
(key, descr) for key, descr in sorted(bpy_struct.__dict__.items())
if not key.startswith("__")
]
for key, descr in descr_items:
if type(descr) == GetSetDescriptorType:
lines.append(" - :class:`{:s}.{:s}`\n".format(_BPY_STRUCT_PYCAPI, key))
for base in bases:
for prop in base.properties:
lines.append(" - :class:`{:s}.{:s}`\n".format(base.identifier, prop.identifier))
for identifier, py_prop in base.get_py_properties():
lines.append(" - :class:`{:s}.{:s}`\n".format(base.identifier, identifier))
if lines:
fw(title_string("Inherited Properties", "-"))
fw(".. hlist::\n")
fw(" :columns: 2\n\n")
for line in lines:
fw(line)
fw("\n")
# Functions.
del lines[:]
if USE_PYCAPI_TYPES:
for key, descr in descr_items:
if type(descr) == MethodDescriptorType:
lines.append(" - :class:`{:s}.{:s}`\n".format(_BPY_STRUCT_PYCAPI, key))
for base in bases:
for func in base.functions:
lines.append(" - :class:`{:s}.{:s}`\n".format(base.identifier, func.identifier))
for identifier, py_func in base.get_py_functions():
lines.append(" - :class:`{:s}.{:s}`\n".format(base.identifier, identifier))
for identifier, py_func in base.get_py_c_functions():
lines.append(" - :class:`{:s}.{:s}`\n".format(base.identifier, identifier))
if lines:
fw(title_string("Inherited Functions", "-"))
fw(".. hlist::\n")
fw(" :columns: 2\n\n")
for line in lines:
fw(line)
fw("\n")
del lines[:]
if struct.references:
fw(title_string("References", "-"))
fw(".. hlist::\n")
fw(" :columns: 2\n\n")
# Context does its own thing.
# "active_object": [("Object", False)],
for ref_attr, ref_types in sorted(context_type_map.items()):
for ref_type, _ in ref_types:
if ref_type == struct_id:
fw(" - :mod:`bpy.context.{:s}`\n".format(ref_attr))
del ref_attr, ref_types
for ref in struct.references:
ref_split = ref.split(".")
if len(ref_split) > 2:
ref = ref_split[-2] + "." + ref_split[-1]
fw(" - :class:`{:s}`\n".format(ref))
fw("\n")
# Docs last?, disable for now.
# write_example_ref("", fw, "bpy.types.{:s}".format(struct_id))
file.close()
if "bpy.types" not in GLOBAL.exclude_modules:
for struct in structs.values():
# TODO: rna_info should filter these out!
if struct.is_operator_properties():
continue
write_struct(struct)
def bpy_pycapi_type(
class_module_name: str,
class_value: type,
class_name: str,
descr_str: str,
*,
use_subclasses: bool,
base_class: str | None,
) -> None:
filepath = basepath / "{:s}.{:s}.rst".format(class_module_name, class_name)
file = open(filepath, "w", encoding="utf-8")
fw = file.write
fw(title_string(class_name, "="))
fw(".. currentmodule:: {:s}\n\n".format(class_module_name))
if base_class is not None:
fw("base classes --- :class:`{:s}`\n\n".format(base_class))
if use_subclasses:
subclass_ids = [
s.identifier for s in structs.values()
if s.base is None
if not rna_info.rna_id_ignore(s.identifier)
]
if subclass_ids:
fw(".. toctree::\n")
# Add a title and limit to direct sub classes.
fw(" :caption: Subclasses\n")
fw(" :maxdepth: 1\n\n")
for s in sorted(subclass_ids):
fw(" {:s}.{:s}.rst\n".format(class_module_name, s))
fw("\n")
if base_class is not None:
fw(".. class:: {:s}({:s})\n\n".format(class_name, base_class))
else:
fw(".. class:: {:s}\n\n".format(class_name))
fw(" {:s}\n\n".format(descr_str))
descr_items = [
(key, descr) for key, descr in sorted(class_value.__dict__.items())
if not key.startswith("__")
]
for key, descr in descr_items:
if type(descr) == MethodDescriptorType:
py_descr2sphinx(" ", fw, descr, class_module_name, class_name, key, is_class=True)
for key, descr in descr_items:
if type(descr) == GetSetDescriptorType:
py_descr2sphinx(" ", fw, descr, class_module_name, class_name, key, is_class=True)
if USE_STUB_GEN:
write_dunder_methods(fw, class_value, class_module_name, class_name)
file.close()
# Write Python C-API classes.
if USE_PYCAPI_TYPES:
bpy_pycapi_type(
"bpy.types", bpy_struct, _BPY_STRUCT_PYCAPI,
"built-in base class for all classes in bpy.types.",
use_subclasses=True,
base_class=None,
)
bpy_pycapi_type(
"bpy.types", bpy_prop, _BPY_PROP_PYCAPI,
"built-in base class for all property classes.",
use_subclasses=False,
base_class=None,
)
bpy_pycapi_type(
"bpy.types", bpy_prop_array, _BPY_PROP_ARRAY_PYCAPI,
"built-in class used for array properties.",
use_subclasses=False,
base_class=_BPY_PROP_PYCAPI,
)
bpy_pycapi_type(
"bpy.types", bpy_prop_collection, _BPY_PROP_COLLECTION_PYCAPI,
"built-in class used for all collections.",
use_subclasses=False,
base_class=_BPY_PROP_PYCAPI,
)
bpy_pycapi_type(
"bpy.types", bpy_prop_collection_idprop, _BPY_PROP_COLLECTION_IDPROP_PYCAPI,
"built-in class used for user defined collections.",
use_subclasses=False,
base_class=_BPY_PROP_COLLECTION_PYCAPI,
)
# Operators.
def write_ops() -> None:
API_BASEURL = "https://projects.blender.org/blender/blender/src/branch/main/scripts"
op_modules: dict[str, list[stub.InfoOperatorRNA]] = {}
op: stub.InfoOperatorRNA | None = None
for op in ops.values():
op_modules.setdefault(op.module_name, []).append(op)
del op
for op_module_name, ops_mod in op_modules.items():
filepath = basepath / "bpy.ops.{:s}.rst".format(op_module_name)
file = open(filepath, "w", encoding="utf-8")
fw = file.write
title = "{:s} Operators".format(op_module_name.replace("_", " ").title())
fw(title_string(title, "="))
fw(".. module:: bpy.ops.{:s}\n\n".format(op_module_name))
ops_mod.sort(key=lambda op: op.func_name)
for op in ops_mod:
args = []
for prop in op.args:
arg_default = prop.get_arg_default(force=True)
# NOTE: prop.fixed_type.bl_rna.base.identifier == "OperatorProperties"
if prop.fixed_type and prop.fixed_type.is_operator_properties():
if arg_default.endswith("=None"):
arg_default = arg_default.removesuffix("None") + "{}"
args.append(arg_default)
args_str = ", ".join(args)
# All operator arguments are keyword only (denoted by the leading `*`).
fw(".. function:: {:s}({:s}{:s})\n\n".format(op.func_name, "*, " if args_str else "", args_str))
# If the description isn't valid, we output the standard warning
# with a link to the wiki so that people can help.
if not op.description or op.description == "(undocumented operator)":
operator_description = undocumented_message("bpy.ops", op.module_name, op.func_name)
else:
operator_description = op.description
# Set `strip` to false as `operator_description` must never be indented.
write_indented_lines(" ", fw, operator_description, strip=False)
fw("\n")
for prop in op.args:
write_param(" ", fw, prop)
fw(" :return: Result of the operator call.\n")
fw(" :rtype: set[Literal[:ref:`rna_enum_operator_return_items`]]\n")
loc_file, loc_line = op.get_location()
if loc_file is not None and loc_line is not None:
fw(" :File: `{:s}\\:{:d} <{:s}/{:s}#L{:d}>`__\n\n".format(
loc_file, loc_line, API_BASEURL, loc_file, loc_line
))
if op.args:
fw("\n")
file.close()
if "bpy.ops" not in GLOBAL.exclude_modules:
write_ops()
def write_rst_index(basepath: Path) -> None:
"""
Write the RST file of the main page, needed for sphinx: ``index.html``.
"""
filepath = basepath / "index.rst"
file = open(filepath, "w", encoding="utf-8")
fw = file.write
fw(title_string("Blender {:s} Python API Documentation".format(GLOBAL.blender_version_dots), "%", double=True))
fw("\n")
fw("Welcome to the Python API documentation for `Blender <https://www.blender.org>`__, ")
fw("the free and open source 3D creation suite.\n")
fw("\n")
# fw("`A PDF version of this document is also available <{:s}>`_\n".format(GLOBAL.output_filename_pdf))
fw("This site can be used offline: `Download the full documentation (zipped HTML files) <{:s}>`__\n".format(
GLOBAL.output_filename_zip,
))
fw("\n")
if not GLOBAL.exclude_info_docs:
fw(".. toctree::\n")
if USE_INFO_DOCS_FANCY_INDEX:
fw(" :hidden:\n")
fw(" :maxdepth: 1\n")
fw(" :caption: Documentation\n\n")
for info, info_desc in INFO_DOCS:
fw(" {:s}\n".format(info.name))
fw("\n")
if USE_INFO_DOCS_FANCY_INDEX:
# Show a fake TOC, allowing for an extra description to be shown as well as the title.
fw(title_string("Documentation", "="))
for info, info_desc in INFO_DOCS:
fw("- :doc:`{:s}`: {:s}\n".format(info.stem, info_desc))
fw("\n")
fw(".. toctree::\n")
fw(" :maxdepth: 1\n")
fw(" :caption: Application Modules\n\n")
app_modules = (
"bpy.context", # NOTE: not actually a module.
"bpy.data", # NOTE: not actually a module.
"bpy.msgbus", # NOTE: not actually a module.
"bpy.ops",
"bpy.types",
# Python modules.
"bpy.utils",
"bpy.path",
"bpy.app",
# Python C-API modules.
"bpy.props",
)
for mod in app_modules:
if mod not in GLOBAL.exclude_modules:
fw(" {:s}\n".format(mod))
fw("\n")
fw(".. toctree::\n")
fw(" :maxdepth: 1\n")
fw(" :caption: Standalone Modules\n\n")
standalone_modules = (
# Sub-modules are added in parent page.
"aud",
"bl_math",
"blf",
"bmesh",
"bpy_extras",
"freestyle",
"gpu",
"gpu_extras",
"idprop",
"imbuf",
"mathutils",
)
for mod in standalone_modules:
if mod not in GLOBAL.exclude_modules:
fw(" {:s}\n".format(mod))
fw("\n")
fw(title_string("Indices", "="))
fw("- :ref:`genindex`\n")
fw("- :ref:`modindex`\n\n")
# Special case, this `bmesh.ops.rst` is extracted from C++ source.
if "bmesh.ops" not in GLOBAL.exclude_modules:
execfile(SCRIPT_DIR / "rst_from_bmesh_opdefines.py")
file.close()
def write_rst_bpy(basepath: Path) -> None:
"""
Write RST file of ``bpy`` module (disabled by default)
"""
if not GLOBAL.bpy:
return
filepath = basepath / "bpy.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw("\n")
title = ":mod:`bpy` --- Blender Python Module"
fw(title_string(title, "="))
fw(".. module:: bpy.types\n\n")
def write_rst_types_index(basepath: Path) -> None:
"""
Write the RST file of ``bpy.types`` module (index)
"""
if "bpy.types" in GLOBAL.exclude_modules:
return
filepath = basepath / "bpy.types.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Types (bpy.types)", "="))
fw(".. module:: bpy.types\n\n")
fw(".. toctree::\n")
# Hide page headings such as example sections from main TOC.
fw(" :titlesonly:\n\n")
fw(" bpy.types.bpy_struct.rst\n\n")
fw(" bpy.types.bpy_prop.rst\n")
fw(" bpy.types.bpy_prop_array.rst\n")
fw(" bpy.types.bpy_prop_collection_idprop.rst\n")
fw(" bpy.types.bpy_prop_collection.rst\n")
for type_name in bpy_types_capi_iter():
identifier = "bpy.types." + type_name
if identifier in GLOBAL.exclude_modules:
continue
fw(" {:s}.rst\n".format(identifier))
fw("\n")
# This needs to be included somewhere, while it's hidden, list to avoid warnings.
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
fw(".. toctree::\n")
fw(" :hidden:\n")
fw(" :maxdepth: 1\n\n")
fw(" Shared Enum Types <bpy_types_enum_items/index>\n\n")
# This needs to be included somewhere, while it's hidden, list to avoid warnings.
if USE_RNA_TYPES_WITH_CUSTOM_PROPERTY_INDEX:
fw(".. toctree::\n")
fw(" :hidden:\n")
fw(" :maxdepth: 1\n\n")
fw(" Types with Custom Property Support <bpy_types_custom_properties>\n\n")
def write_rst_ops_index(basepath: Path) -> None:
"""
Write the RST file of bpy.ops module (index)
"""
if "bpy.ops" in GLOBAL.exclude_modules:
return
filepath = basepath / "bpy.ops.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Operators (bpy.ops)", "="))
fw(".. module:: bpy.ops\n\n")
write_example_ref("", fw, "bpy.ops")
fw(".. toctree::\n")
fw(" :caption: Submodules\n")
# Only show top-level entries (avoids unreasonably large pages).
fw(" :maxdepth: 1\n")
fw(" :glob:\n\n")
fw(" bpy.ops.*\n\n")
def bpy_types_capi_iter() -> Iterator[str]:
"""
Yield names of C-API defined ``bpy.types.*`` classes (e.g. ``GeometrySet``).
These are classes added to ``bpy.types`` from C/C++ that are not RNA-derived
(i.e. not sub-classes of ``bpy_struct``, so they aren't picked up by :func:`pyrna2sphinx`.
"""
for name in dir(bpy.types):
if name.startswith("_"):
continue
# Core C-API types in `bpy.types` that back the RNA wrapping itself
# (not user-facing C-API types).
if name in _BPY_TYPES_CORE_CAPI:
continue
attr = getattr(bpy.types, name)
if not isinstance(attr, type):
continue
# Skip RNA-derived types (sub-classes of `bpy_struct`).
if issubclass(attr, bpy_struct):
continue
yield name
def write_rst_bpy_types_capi(basepath: Path) -> None:
"""
Write the RST files for C-API defined ``bpy.types.*`` classes.
"""
for type_name in bpy_types_capi_iter():
identifier = "bpy.types." + type_name
if identifier in GLOBAL.exclude_modules:
continue
filepath = basepath / (identifier + ".rst")
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string(type_name, "="))
# Needed for Sphinx cross-referencing.
fw(".. currentmodule:: bpy.types\n\n")
write_example_ref("", fw, identifier)
pyclass2sphinx(fw, "bpy.types", type_name, getattr(bpy.types, type_name), False)
State.example_set_used.add(identifier)
def write_rst_msgbus(basepath: Path) -> None:
"""
Write the RST files of ``bpy.msgbus`` module
"""
if 'bpy.msgbus' in GLOBAL.exclude_modules:
return
# Write the index.
filepath = basepath / "bpy.msgbus.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Message Bus (bpy.msgbus)", "="))
write_example_ref("", fw, "bpy.msgbus")
fw(".. toctree::\n")
fw(" :glob:\n\n")
fw(" bpy.msgbus.*\n\n")
# Write the contents.
pymodule2sphinx(basepath, 'bpy.msgbus', bpy.msgbus, 'Message Bus', ())
State.example_set_used.add("bpy.msgbus")
def write_rst_data(basepath: Path) -> None:
"""
Write the RST file of ``bpy.data`` module.
"""
if "bpy.data" in GLOBAL.exclude_modules:
return
# Not actually a module, only write this file so we can reference in the TOC.
filepath = basepath / "bpy.data.rst"
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Data Access (bpy.data)", "="))
fw(".. module:: bpy.data\n")
fw("\n")
fw("This module is used for all Blender/Python access.\n")
fw("\n")
fw(".. data:: data\n")
fw("\n")
fw(" Access to Blender's internal data\n")
fw("\n")
fw(" :type: :class:`bpy.types.BlendData`\n")
fw("\n")
fw(".. literalinclude:: ../examples/bpy.data.0.py\n")
State.example_set_used.add("bpy.data")
def pyrna_enum2sphinx_shared_link(prop: stub.InfoPropertyRNA) -> str | None:
"""
Return a reference to the enum used by ``prop`` or None when not found.
"""
if (
(prop.type == "enum") and
(pointer := prop.enum_pointer) and
(identifier := rna_enum_pointer_to_id_map.get(pointer))
):
return ":ref:`{:s}`".format(identifier)
return None
def write_rst_enum_items(
basepath: Path,
key: str,
key_no_prefix: str,
enum_items: Sequence[stub.RnaEnumItem],
) -> None:
"""
Write a single page for a static enum in RST.
This helps avoiding very large lists being in-lined in many places which is an issue
especially with icons in ``bpy.types.UILayout``. See #87008.
"""
filepath = basepath / "{:s}.rst".format(key_no_prefix)
with open(filepath, "w", encoding="utf-8") as fh:
fw = fh.write
# fw(".. noindex::\n\n")
fw(".. _{:s}:\n\n".format(key))
fw(title_string(key_no_prefix.replace("_", " ").title(), "#"))
for item in enum_items:
identifier = item.identifier
name = item.name
description = item.description
if identifier:
fw(":{:s}: {:s}\n".format(item.identifier, (escape_rst(name) + ".") if name else ""))
if description:
fw("\n")
write_indented_lines(" ", fw, escape_rst(description) + ".")
else:
fw("\n")
else:
if name:
fw("\n\n**{:s}**\n\n".format(name))
else:
fw("\n\n----\n\n")
if description:
fw(escape_rst(description) + ".")
fw("\n\n")
def write_rst_enum_items_and_index(basepath: Path) -> None:
"""
Write shared enum items.
"""
subdir = "bpy_types_enum_items"
basepath_bpy_types_rna_enum = basepath / subdir
basepath_bpy_types_rna_enum.mkdir(parents=True, exist_ok=True)
with open(basepath_bpy_types_rna_enum / "index.rst", "w", encoding="utf-8") as fh:
fw = fh.write
fw(title_string("Shared Enum Items", "#"))
fw(".. toctree::\n")
fw("\n")
for key, enum_items in rna_enum_dict.items():
if not key.startswith("rna_enum_"):
raise Exception(
"Found RNA enum identifier that doesn't use the 'rna_enum_' prefix, found {!r}!".format(
key,
))
key_no_prefix = key.removeprefix("rna_enum_")
fw(" {:s}\n".format(key_no_prefix))
for key, enum_items in rna_enum_dict.items():
key_no_prefix = key.removeprefix("rna_enum_")
write_rst_enum_items(basepath_bpy_types_rna_enum, key, key_no_prefix, enum_items)
fw("\n")
def write_rst_rna_types_with_custom_property_support(basepath: Path) -> None:
from bpy.types import bpy_struct_meta_idprop # type: ignore[import-not-found]
types_exclude = {
"IDPropertyWrapPtr", # Internal type, exclude form public docs.
}
types_found = []
for ty_id in dir(bpy.types):
if ty_id.startswith("_"):
continue
if ty_id in types_exclude:
continue
ty = getattr(bpy.types, ty_id)
if not isinstance(ty, bpy_struct_meta_idprop):
continue
# Don't include every sub-type as it's very noisy and not helpful.
if any((isinstance(ty_base, bpy_struct_meta_idprop) for ty_base in ty.__bases__)):
continue
types_found.append(ty_id)
types_found.sort()
with open(basepath / "bpy_types_custom_properties.rst", "w", encoding="utf-8") as fh:
fw = fh.write
fw(".. _bpy_types-custom_properties:\n\n")
fw(title_string("Types with Custom Property Support", "="))
fw("\n")
fw("The following types (and their sub-types) have custom-property access.\n\n")
fw("For examples on using custom properties see the quick-start section on\n")
fw(":ref:`info_quickstart-custom_properties`.\n")
fw("\n")
for ty_id in types_found:
fw("- :class:`bpy.types.{:s}`\n".format(ty_id))
def write_rst_importable_modules(basepath: Path) -> None:
"""
Write the RST files of importable modules.
"""
importable_modules = {
# Python_modules.
"bpy.path": "Path Utilities",
"bpy.utils": "Utilities",
"bpy_extras": "Extra Utilities",
"gpu_extras": "GPU Utilities",
# C_modules.
"aud": "Audio System",
"blf": "Font Drawing",
"blf.types": "Font Drawing Types",
"imbuf": "Image Buffer",
"imbuf.types": "Image Buffer Types",
"gpu": "GPU Module",
"gpu.types": "GPU Types",
"gpu.matrix": "GPU Matrix Utilities",
"gpu.select": "GPU Select Utilities",
"gpu.shader": "GPU Shader Utilities",
"gpu.state": "GPU State Utilities",
"gpu.texture": "GPU Texture Utilities",
"gpu.platform": "GPU Platform Utilities",
"gpu.capabilities": "GPU Capabilities Utilities",
"gpu.compute": "GPU Compute Utilities",
"bmesh": "BMesh Module",
"bmesh.ops": "BMesh Operators",
"bmesh.types": "BMesh Types",
"bmesh.utils": "BMesh Utilities",
"bmesh.geometry": "BMesh Geometry Utilities",
"bpy.app": "Application Data",
"bpy.app.handlers": "Application Handlers",
"bpy.app.translations": "Application Translations",
"bpy.app.icons": "Application Icons",
"bpy.app.timers": "Application Timers",
"bpy.props": "Property Definitions",
"idprop": "ID Properties Module",
"idprop.types": "ID Property Access",
"mathutils": "Math Types & Utilities",
"mathutils.geometry": "Geometry Utilities",
"mathutils.bvhtree": "BVHTree Utilities",
"mathutils.kdtree": "KDTree Utilities",
"mathutils.interpolate": "Interpolation Utilities",
"mathutils.noise": "Noise Utilities",
"bl_math": "Additional Math Functions",
"freestyle": "Freestyle Module",
"freestyle.types": "Freestyle Types",
"freestyle.predicates": "Freestyle Predicates",
"freestyle.functions": "Freestyle Functions",
"freestyle.chainingiterators": "Freestyle Chaining Iterators",
"freestyle.shaders": "Freestyle Shaders",
"freestyle.utils": "Freestyle Utilities",
}
# This is needed since some of the sub-modules listed above are not actual modules.
# Examples include `bpy.app.translations` & `bpy.app.handlers`.
#
# Most of these are `PyStructSequence` internally,
# however we don't want to document all of these as modules since some only contain
# a few values (version number for e.g).
#
# If we remove this logic and document all `PyStructSequence` as sub-modules it means
# `bpy.app.timers` for example would be presented on the same level as library information
# access such as `bpy.app.sdl` which doesn't seem useful since it hides more useful
# module-like objects among library data access.
importable_modules_parent_map: dict[str, list[str]] = {}
for mod_name in importable_modules: # Iterate over keys.
if mod_name in GLOBAL.exclude_modules:
continue
if "." in mod_name:
mod_name, submod_name = mod_name.rsplit(".", 1)
importable_modules_parent_map.setdefault(mod_name, []).append(submod_name)
for mod_name, mod_descr in importable_modules.items():
if mod_name in GLOBAL.exclude_modules:
continue
module_all_extra = importable_modules_parent_map.get(mod_name, ())
module = __import__(mod_name, fromlist=[mod_name.rsplit(".", 1)[-1]])
pymodule2sphinx(basepath, mod_name, module, mod_descr, module_all_extra)
def copy_handwritten_rsts(basepath: Path) -> None:
# Info docs.
if not GLOBAL.exclude_info_docs:
for info, _info_desc in INFO_DOCS:
shutil.copy2(RST_DIR / info, basepath)
for info in INFO_DOCS_OTHER:
shutil.copy2(RST_DIR / info, basepath)
# TODO: put this docs in Blender's code and use import as per modules above.
handwritten_modules = [
"bmesh.ops", # Generated by `rst_from_bmesh_opdefines.py`.
# Includes.
"include__bmesh",
]
for mod_name in handwritten_modules:
if mod_name not in GLOBAL.exclude_modules:
# Copy2 keeps time/date stamps.
shutil.copy2(RST_DIR / "{:s}.rst".format(mod_name), basepath)
# Change-log.
shutil.copy2(RST_DIR / "change_log.rst", basepath)
# Copy images, could be smarter but just glob for now.
for p in RST_DIR.iterdir():
if p.suffix == ".png":
shutil.copy2(p, basepath)
def copy_handwritten_extra(basepath: Path) -> None:
for f_src_rel in EXTRA_SOURCE_FILES:
f_src = RST_DIR / f_src_rel
f_dst = basepath.joinpath(*("__" if part == ".." else part for part in f_src_rel.parts))
f_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(f_src, f_dst)
def copy_sphinx_files(basepath: Path) -> None:
shutil.copytree(
SCRIPT_DIR / "static",
basepath / "static",
copy_function=shutil.copy,
)
shutil.copytree(
SCRIPT_DIR / "templates",
basepath / "templates",
copy_function=shutil.copy,
)
shutil.copy2(SCRIPT_DIR / "conf.py", basepath)
def format_config(basepath: Path) -> None:
"""
Updates ``conf.py`` with context information from Blender.
"""
from string import Template
# Ensure the string literals can contain any characters by closing the surrounding quotes
# and declare a separate literal via `repr()`.
def declare_in_quotes(string: str) -> str:
return "\" {!r} \"".format(string)
substitutions = {
"BLENDER_VERSION_STRING": declare_in_quotes(GLOBAL.blender_version_string),
"BLENDER_VERSION_DOTS": declare_in_quotes(GLOBAL.blender_version_dots),
"BLENDER_REVISION_TIMESTAMP": declare_in_quotes(str(GLOBAL.blender_revision_timestamp)),
"BLENDER_REVISION": declare_in_quotes(GLOBAL.blender_revision),
}
filepath = basepath / "conf.py"
# Read the template string from the template file.
with open(filepath, 'r', encoding="utf-8") as fh:
template_file = fh.read()
with open(filepath, 'w', encoding="utf-8") as fh:
fh.write(Template(template_file).substitute(substitutions))
def rna2sphinx(basepath: Path) -> None:
# Main page.
write_rst_index(basepath)
# Context.
if "bpy.context" not in GLOBAL.exclude_modules:
pycontext2sphinx(basepath)
# Internal modules.
write_rst_bpy(basepath) # `bpy`, disabled by default
write_rst_types_index(basepath) # `bpy.types`.
write_rst_ops_index(basepath) # `bpy.ops`.
write_rst_msgbus(basepath) # `bpy.msgbus`.
write_rst_bpy_types_capi(basepath) # `bpy.types.*` (C-API defined).
pyrna2sphinx(basepath) # `bpy.types.*` & `bpy.ops.*`.
write_rst_data(basepath) # `bpy.data`.
write_rst_importable_modules(basepath)
# `bpy_types_enum_items/*` (referenced from `bpy.types`).
if USE_SHARED_RNA_ENUM_ITEMS_STATIC:
write_rst_enum_items_and_index(basepath)
if USE_RNA_TYPES_WITH_CUSTOM_PROPERTY_INDEX:
write_rst_rna_types_with_custom_property_support(basepath)
# Copy the other RST files.
copy_handwritten_rsts(basepath)
# Copy source files referenced.
copy_handwritten_extra(basepath)
def align_sphinx_in_to_sphinx_in_tmp(dir_src: Path, dir_dst: Path) -> None:
"""
Move changed files from GLOBAL.sphinx_in_tmp to GLOBAL.sphinx_in
"""
import filecmp
# Possible the dir doesn't exist when running recursively.
dir_dst.mkdir(parents=True, exist_ok=True)
sphinx_dst_files = {p.name for p in dir_dst.iterdir()}
sphinx_src_files = {p.name for p in dir_src.iterdir()}
# Remove deprecated files that have been removed.
for f in sorted(sphinx_dst_files):
if f not in sphinx_src_files:
BPY_LOGGER.debug("\tdeprecated: %s", f)
f_dst = dir_dst / f
if f_dst.is_dir():
shutil.rmtree(f_dst, True)
else:
f_dst.unlink()
# Freshen with new files.
for f in sorted(sphinx_src_files):
f_src = dir_src / f
f_dst = dir_dst / f
if f_src.is_dir():
align_sphinx_in_to_sphinx_in_tmp(f_src, f_dst)
else:
do_copy = True
if f in sphinx_dst_files:
if filecmp.cmp(f_src, f_dst):
do_copy = False
if do_copy:
BPY_LOGGER.debug("\tupdating: %s", f)
shutil.copy(f_src, f_dst)
def refactor_sphinx_log(sphinx_logfile: Path) -> None:
refactored_log: list[tuple[str, str, str]] = []
with open(sphinx_logfile, "r", encoding="utf-8") as original_logfile:
lines = set(original_logfile.readlines())
for line in lines:
if 'warning' in line.lower() or 'error' in line.lower():
parts = line.strip().split(None, 2)
if len(parts) == 3:
location, kind, msg = parts
location = str(Path(location).relative_to(GLOBAL.sphinx_in, walk_up=True))
refactored_log.append((kind, location, msg))
with open(sphinx_logfile, "w", encoding="utf-8") as refactored_logfile:
for log in sorted(refactored_log):
refactored_logfile.write("%-12s %s\n %s\n" % log)
def setup_monkey_patch() -> None:
execfile(SCRIPT_DIR / "sphinx_doc_gen_monkeypatch.py")
# Each entry is `(snapshot, original_list)`; restore by writing the snapshot back.
SetupData = dict[str, list[tuple[list[object], list[object]]]]
# Avoid adding too many changes here.
def setup_blender() -> SetupData:
# Remove handlers since the functions get included
# in the docstring and don't have meaningful names.
lists_to_restore: list[tuple[list[object], list[object]]] = []
for var in bpy.app.handlers:
if isinstance(var, list):
lists_to_restore.append((var[:], var))
var.clear()
return {
"lists_to_restore": lists_to_restore,
}
def teardown_blender(setup_data: SetupData) -> None:
for var_src, var_dst in setup_data["lists_to_restore"]:
var_dst[:] = var_src
def main(argv: Sequence[str] | None = None) -> int:
global GLOBAL
if argv is None:
argv = []
if "--" in sys.argv:
argv = sys.argv[sys.argv.index("--") + 1:]
GLOBAL = global_create(argv)
State.reset()
# First monkey patch to load in fake members.
setup_monkey_patch()
# Perform changes to Blender itself.
setup_data = setup_blender()
# Eventually, create the directories.
for dir_path in [GLOBAL.output_dir, GLOBAL.sphinx_in]:
if not dir_path.exists():
dir_path.mkdir()
# Eventually, log in files.
if GLOBAL.log:
bpy_logfile = GLOBAL.output_dir / ".bpy.log"
bpy_logfilehandler = logging.FileHandler(bpy_logfile, mode="w")
bpy_logfilehandler.setLevel(logging.DEBUG)
BPY_LOGGER.addHandler(bpy_logfilehandler)
# Using a `FileHandler` seems to disable the `stdout`, so we add a `StreamHandler`.
bpy_log_stdout_handler = logging.StreamHandler(stream=sys.stdout)
bpy_log_stdout_handler.setLevel(logging.DEBUG)
BPY_LOGGER.addHandler(bpy_log_stdout_handler)
# In case of out-of-source build, copy the needed directories.
if GLOBAL.output_dir != SCRIPT_DIR:
# Examples directory.
examples_dir_copy = GLOBAL.output_dir / "examples"
if examples_dir_copy.exists():
shutil.rmtree(examples_dir_copy, True)
shutil.copytree(
EXAMPLES_DIR,
examples_dir_copy,
ignore=shutil.ignore_patterns(*(".svn",)),
copy_function=shutil.copy,
)
# Start from a clean directory every time.
if GLOBAL.sphinx_in_tmp.exists():
shutil.rmtree(GLOBAL.sphinx_in_tmp, True)
try:
GLOBAL.sphinx_in_tmp.mkdir()
except Exception:
pass
# Copy extra files needed for theme.
copy_sphinx_files(GLOBAL.sphinx_in_tmp)
# Write information needed for `conf.py`.
format_config(GLOBAL.sphinx_in_tmp)
# Dump the API in RST files.
rna2sphinx(GLOBAL.sphinx_in_tmp)
if GLOBAL.changelog:
generate_changelog()
if GLOBAL.full_rebuild:
# Only for full updates.
shutil.rmtree(GLOBAL.sphinx_in, True)
shutil.copytree(
GLOBAL.sphinx_in_tmp,
GLOBAL.sphinx_in,
copy_function=shutil.copy,
)
if GLOBAL.sphinx_build and GLOBAL.sphinx_out.exists():
shutil.rmtree(GLOBAL.sphinx_out, True)
if GLOBAL.sphinx_build_pdf:
assert GLOBAL.sphinx_out_pdf is not None
if GLOBAL.sphinx_out_pdf.exists():
shutil.rmtree(GLOBAL.sphinx_out_pdf, True)
else:
# Move changed files in `GLOBAL.sphinx_in`.
align_sphinx_in_to_sphinx_in_tmp(GLOBAL.sphinx_in_tmp, GLOBAL.sphinx_in)
# Report which example files weren't used.
example_set_unused = EXAMPLE_SET - State.example_set_used
if example_set_unused:
BPY_LOGGER.debug("\nUnused examples found in '%s'...", EXAMPLES_DIR)
for f in sorted(example_set_unused):
BPY_LOGGER.debug(" %s.py", f)
BPY_LOGGER.debug(" %d total\n", len(example_set_unused))
del example_set_unused
# Eventually, build the HTML docs.
if GLOBAL.sphinx_build:
import subprocess
subprocess.call(GLOBAL.sphinx_build_cmd)
# Sphinx-build log cleanup+sort.
if GLOBAL.log:
assert GLOBAL.sphinx_build_log is not None
if GLOBAL.sphinx_build_log.stat().st_size:
refactor_sphinx_log(GLOBAL.sphinx_build_log)
# Eventually, build the PDF docs.
if GLOBAL.sphinx_build_pdf:
import subprocess
subprocess.call(GLOBAL.sphinx_build_pdf_cmd)
assert GLOBAL.sphinx_make_pdf_log is not None # FIXME: only set when `log=True`.
with open(GLOBAL.sphinx_make_pdf_log, "w", encoding="utf-8") as fh:
subprocess.call(GLOBAL.sphinx_make_pdf_cmd, stdout=fh)
# Sphinx-build log cleanup+sort.
if GLOBAL.log:
assert GLOBAL.sphinx_build_pdf_log is not None
if GLOBAL.sphinx_build_pdf_log.stat().st_size:
refactor_sphinx_log(GLOBAL.sphinx_build_pdf_log)
# Eventually, prepare the dir to be deployed online (`GLOBAL.output_base_path`).
if GLOBAL.pack_reference:
if GLOBAL.sphinx_build:
# Delete GLOBAL.output_base_path.
if GLOBAL.output_base_path.exists():
shutil.rmtree(GLOBAL.output_base_path, True)
# Copy GLOBAL.sphinx_out to the GLOBAL.output_base_path.
ignores = (".doctrees", ".buildinfo")
shutil.copytree(
GLOBAL.sphinx_out,
GLOBAL.output_base_path,
ignore=shutil.ignore_patterns(*ignores),
)
# Zip GLOBAL.output_base_path.
basename = GLOBAL.output_dir / GLOBAL.output_base_name
tmp_path = shutil.make_archive(
str(basename), "zip",
root_dir=GLOBAL.output_dir,
base_dir=GLOBAL.output_base_name,
)
final_path = GLOBAL.output_base_path / GLOBAL.output_filename_zip
Path(tmp_path).rename(final_path)
if GLOBAL.sphinx_build_pdf:
# Copy the pdf to `GLOBAL.output_base_path`.
assert GLOBAL.sphinx_out_pdf is not None
shutil.copy(
GLOBAL.sphinx_out_pdf / "contents.pdf",
GLOBAL.output_base_path / GLOBAL.output_filename_pdf,
)
teardown_blender(setup_data)
return 1 if State.error_count else 0
if __name__ == '__main__':
sys.exit(main())