mirror of
https://github.com/blender/blender
synced 2026-09-27 01:34:15 +03:00
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
3713 lines
135 KiB
Python
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())
|