Source code for sphinx_immaterial.apidoc.cpp.apigen

"""C++ API documentation generation extension."""

import collections
import contextlib
import dataclasses
import json
import os
import re
from typing import cast, Dict, List, Optional, Tuple, Set, Type, Literal

import docutils.core
import docutils.nodes
import docutils.parsers.rst.directives
import docutils.utils
import pydantic
import pydantic.dataclasses
import sphinx.application
import sphinx.builders
import sphinx.builders.html
import sphinx.domains.c
import sphinx.domains.cpp
import sphinx.environment
import sphinx.errors
import sphinx.util.cfamily
import sphinx.util.docfields
import sphinx.util.logging


from .. import apigen_utils
from ... import sphinx_utils
from . import api_parser
from .signodes import desc_cpp_explicit

logger = sphinx.util.logging.getLogger(__name__)

APIDOC_SECTION_ID_PREFIX = "cpp-api-"


[docs] @pydantic.dataclasses.dataclass class ApigenConfig: """Specifies a C++ API generation configuration.""" document_prefix: str """Sphinx document path prefix for the per-entity documentation pages. .. important:: This should use the Linux path separator :python:`"/"` (even if building on Windows). """ api_parser_config: Optional[api_parser.Config] = None """Configuration for generating an API description. This option and `.api_data` are mutually exclusive. """ api_data: Optional[str] = None """Path to already-parsed API description. This option and `.api_parser_config` are mutually exclusive. """
CppApiEntity = api_parser.CppApiEntity EntityId = str @dataclasses.dataclass class CppApiData: groups: Dict[str, List[EntityId]] = dataclasses.field(default_factory=dict) entities: Dict[EntityId, CppApiEntity] = dataclasses.field(default_factory=dict) nonitpick: Set[Tuple[str, str, int]] = dataclasses.field(default_factory=set) def get_entity_sub_groups( self, entity_id: str, member: bool ) -> Dict[str, List[EntityId]]: entity = self.entities[entity_id] return entity.get( cast( Literal["related_members", "related_nonmembers"], "related_members" if member else "related_nonmembers", ), cast(Dict[str, List[EntityId]], {}), ) def get_documented_entities(self): seen = set() def process_group(entity_ids: List[str]): for entity_id in entity_ids: if entity_id in seen: continue seen.add(entity_id) yield entity_id for member in (True, False): for related_ids in self.get_entity_sub_groups( entity_id, member=member ).values(): yield from process_group(related_ids) for group_members in self.groups.values(): yield from process_group(group_members) def get_entity_toc_title(self, entity: CppApiEntity) -> str: return entity["name"] def get_entity_scope(self, entity: CppApiEntity) -> str: components = [] cur_entity = entity while True: parent_id = cur_entity.get("parent") if parent_id is None: break parent_entity = self.entities.get(parent_id) assert parent_entity is not None cur_entity = parent_entity name_with_args = cur_entity["name"] if not cur_entity.get("specializes"): name_with_args += _format_template_arguments(cur_entity) components.append(name_with_args) components.reverse() if components: components.append("") return cur_entity.get("scope", "") + "::".join(components) def get_entity_object_name(self, entity: CppApiEntity) -> str: name = self.get_entity_scope(entity) + entity["name"] special_id = entity.get("special_id") if special_id: name += f"[{special_id}]" return name def get_entity_page_name(self, entity: CppApiEntity) -> str: return entity["page_name"] def get_entity_page_path(self, entity: CppApiEntity) -> str: return entity["document_prefix"] + self.get_entity_page_name(entity) def _get_cpp_api_data( env: sphinx.environment.BuildEnvironment, warn: bool = False ) -> CppApiData: KEY = "_sphinx_immaterial_cpp_apigen_data" data = getattr(env, KEY, None) if data is not None: return data apigen_configs = getattr(env.app, _CONFIG_ATTR) data = CppApiData() for apigen_config in apigen_configs: if apigen_config.api_parser_config is not None: json_data = api_parser.generate_output(apigen_config.api_parser_config) else: with open( os.path.join(env.app.srcdir, apigen_config.api_data), "r", encoding="utf-8", ) as f: json_data = cast(api_parser.JsonApiData, json.load(f)) for entity in json_data["entities"].values(): entity["document_prefix"] = apigen_config.document_prefix data.entities.update(json_data["entities"]) for group_id, group in json_data["groups"].items(): data.groups.setdefault(group_id, []).extend(group) data.nonitpick.update( set((x["target"], x["file"], x["line"]) for x in json_data["nonitpick"]) ) if warn: for diag in json_data["warnings"]: logger.warning( diag["message"], location=api_parser.json_location_to_string(diag["location"]), ) for diag in json_data["errors"]: logger.error( diag["message"], location=api_parser.json_location_to_string(diag["location"]), ) setattr(env, KEY, data) return data def _transform_doc_comment( env: sphinx.environment.BuildEnvironment, doc: api_parser.JsonDocComment, summary: bool, ) -> Optional[docutils.statemachine.StringList]: if not doc: return None text = doc["text"] location = doc["location"] filename = location["file"] lineno = location["line"] if summary: # Find first blank line first_blank_line = re.search(r"^\s*$", text, flags=re.MULTILINE) if first_blank_line: text = text[: first_blank_line.start()] out = docutils.statemachine.StringList() sphinx_utils.append_multiline_string_to_stringlist( out, ".. highlight-push::\n\n" + env.config.cpp_apigen_rst_prolog + "\n\n", "<cpp_apigen_rst_prolog>", -2, ) sphinx_utils.append_multiline_string_to_stringlist(out, text, filename, lineno - 1) sphinx_utils.append_multiline_string_to_stringlist( out, "\n\n" + env.config.cpp_apigen_rst_epilog + "\n\n.. highlight-pop::\n\n", "<cpp_apigen_rst_epilog>", -2, ) return out def _format_template_parameters(entity: CppApiEntity) -> str: template_parameters = entity.get("template_parameters") if template_parameters is None: return "" tparam_list_str = ", ".join(x["declaration"] for x in template_parameters) tparams = f"template <{tparam_list_str}> " requires = entity.get("requires") if requires: requires_expr = " && ".join(requires) tparams += f"requires {requires_expr} " return tparams def _format_template_arguments(entity: CppApiEntity) -> str: if entity.get("specializes"): # Template arguments already included in `entity["name"]`. return "" template_parameters = entity.get("template_parameters") if not template_parameters: return "" strs = [] for param in template_parameters: arg = param["name"] if not arg: continue if param["pack"]: arg += "..." strs.append(arg) args_str = ", ".join(strs) return f"<{args_str}>" def _validate_signature_line_tags( strip_tags: List[Type[sphinx.addnodes.desc_sig_element]], children: List[docutils.nodes.Node], ): for strip_tag, child in zip(strip_tags, children): if isinstance(child, sphinx.addnodes.desc_signature_line) and child.children: child = child.children[0] if not isinstance(child, strip_tag): raise ValueError( f"Expected {strip_tag!r} tag, but received: {type(child)!r}" ) def _strip_template_parameters( api_data: CppApiData, entity: CppApiEntity, signode: sphinx.addnodes.desc_signature, strip_self_parameters: bool = False, strip_ancestors_parameters: bool = False, ): cur_entity = entity strip_leading: List[str] = [] strip_trailing: List[str] = [] if strip_self_parameters: if entity.get("requires") and entity.get("template_parameters") is None: strip_trailing.append("trailingRequiresClause") depth = 0 while True: if depth > 0 and not strip_ancestors_parameters: break if depth > 0 or strip_self_parameters: if cur_entity.get("template_parameters") is not None: strip_leading.append("templateParams") if cur_entity.get("requires"): strip_leading.append("requiresClause") depth += 1 parent_id = cur_entity.get("parent") if parent_id is None: break cur_entity = api_data.entities[parent_id] signature_lines: List[Tuple[int, sphinx.addnodes.desc_signature_line]] = [ (i, child) for i, child in enumerate(signode) if isinstance(child, sphinx.addnodes.desc_signature_line) ] if len(signature_lines) < len(strip_leading) + len(strip_trailing): raise ValueError("Missing children") for i, line_type in enumerate(strip_leading): assert signature_lines[i][1].sphinx_line_type == line_type for i, line_type in enumerate(strip_trailing): assert ( signature_lines[len(signature_lines) - 1 - i][1].sphinx_line_type == line_type ) remove = ( signature_lines[: len(strip_leading)] + signature_lines[len(signature_lines) - len(strip_trailing) :] ) remove.reverse() for i, _ in remove: del signode[i] def _summarize_explicit(signode: sphinx.addnodes.desc_signature) -> None: for node in signode.findall(condition=desc_cpp_explicit): if len(node.children) == 1: continue node.children[2:-1] = sphinx.addnodes.desc_sig_punctuation("...", "...") def _format_entity_alias( entity: api_parser.TypeAliasEntity, full_name: str ) -> Tuple[str, str]: underlying_type = entity["underlying_type"] signature = f"{full_name}" if underlying_type is not None: signature += f" = {underlying_type}" return ("cpp:type", signature) def _format_entity_constructor( entity: api_parser.FunctionEntity, full_name: str ) -> Tuple[str, str]: return _format_entity_function(entity, full_name) def _format_entity_class( entity: api_parser.ClassEntity, full_name: str ) -> Tuple[str, str]: prefix = " ".join(entity["prefix"]) signature = f"{prefix} {full_name}" base_strs = [] for base in entity["bases"]: base_strs.append(f'{base["access"]} {base["type"]}') if base_strs: signature += " : " signature += ", ".join(base_strs) return (f'cpp:{entity["keyword"]}', signature) def _format_entity_function( entity: api_parser.FunctionEntity, full_name: str ) -> Tuple[str, str]: signature = entity["declaration"].replace(entity["name_substitute"], full_name) return ("cpp:function", signature) def _format_entity_method( entity: api_parser.FunctionEntity, full_name: str ) -> Tuple[str, str]: return _format_entity_function(entity, full_name) def _format_entity_conversion_function( entity: api_parser.FunctionEntity, full_name: str ) -> Tuple[str, str]: return _format_entity_function(entity, full_name) def _format_entity_var(entity: api_parser.VarEntity, full_name: str) -> Tuple[str, str]: declaration = entity["declaration"].replace(entity["name_substitute"], full_name) signature = declaration initializer = entity["initializer"] if initializer is not None: signature += initializer return ("cpp:var", signature) def _format_entity_enum( entity: api_parser.EnumEntity, full_name: str ) -> Tuple[str, str]: signature = full_name directive_name = "cpp:enum" if entity["keyword"]: directive_name += f'-{entity["keyword"]}' return (directive_name, signature) def _format_entity_macro( entity: api_parser.MacroEntity, full_name: str ) -> Tuple[str, str]: signature = full_name parameters = entity["parameters"] if parameters is not None: signature += f'({", ".join(parameters)})' return ("c:macro", signature) def _format_template_prefix( api_data: CppApiData, entity: CppApiEntity, summary: bool ) -> str: inner_prefix = _format_template_parameters(entity) if summary: return inner_prefix prefixes = [inner_prefix] while True: parent_id = entity.get("parent") if parent_id is None: break entity = api_data.entities[parent_id] prefixes.append(_format_template_parameters(entity)) prefixes.reverse() return " ".join(prefixes) def _format_trailing_requires(entity: CppApiEntity, summary: bool) -> str: template_parameters = entity.get("template_parameters") requires = entity.get("requires") if template_parameters is not None or not requires: return "" return " requires " + " && ".join(expr for expr in requires) def _format_entity( api_data: CppApiData, entity: CppApiEntity, summary: bool, include_scope: bool ): full_name = api_data.get_entity_scope(entity) if include_scope else "" full_name += entity["name"] kind = entity["kind"] if summary and kind in ("class", "alias", "var"): # Add template arguments full_name += _format_template_arguments(entity) cpp_directive, signature_text = globals()[f"_format_entity_{kind}"]( entity, full_name ) signature_line = ( _format_template_prefix(api_data, entity, summary=summary) + signature_text + _format_trailing_requires(entity, summary=summary) ) if kind != "macro": signature_line += ";" return cpp_directive, signature_line @contextlib.contextmanager def save_cpp_scope(env: sphinx.environment.BuildEnvironment): parent_symbol = env.temp_data.get("cpp:parent_symbol", False) namespace_stack = env.temp_data.get("cpp:namespace_stack", False) if namespace_stack: namespace_stack = list(namespace_stack) parent_key = env.ref_context.get("cpp:parent_key", False) yield if parent_symbol is not False: env.temp_data["cpp:parent_symbol"] = parent_symbol else: env.temp_data.pop("cpp:parent_symbol", None) if namespace_stack is not False: env.temp_data["cpp:namespace_stack"] = namespace_stack else: env.temp_data.pop("cpp:namespace_stack", None) if parent_key is not False: env.ref_context["cpp:parent_key"] = parent_key else: env.ref_context.pop("cpp:parent_key", None) def _add_entity_description( env: sphinx.environment.BuildEnvironment, api_data: CppApiData, entity: CppApiEntity, contentnode: docutils.nodes.Element, summary: bool, state: docutils.parsers.rst.states.RSTState, ) -> None: kind = entity["kind"] out = docutils.statemachine.StringList() location = entity["location"] parent_id = entity.get("parent") if parent_id and summary: include_scope = False else: include_scope = True if parent_id: parent = api_data.entities[parent_id] scope_template_prefix = _format_template_prefix( api_data, api_data.entities[parent_id], summary=False ) else: parent = None scope_template_prefix = "" scope = api_data.get_entity_scope(entity).rstrip(":") sphinx_utils.append_directive_to_stringlist( out, "cpp:namespace", scope_template_prefix + scope if not include_scope else "nullptr", source_path=location["file"], source_line=location["line"] - 1, ) cpp_directive, signature_line = _format_entity( api_data, entity, summary, include_scope ) signature_lines = [] num_include_lines = 0 if not summary: include_path = location["file"] signature_lines.append(f'#include "{include_path}"') num_include_lines = 1 signature_lines.append(signature_line) siblings = entity.get("siblings") entity_ids = [entity["id"]] if siblings: for sibling_id in siblings: sibling = api_data.entities[sibling_id] other_cpp_directive, other_signature_line = _format_entity( api_data, sibling, summary, include_scope ) assert other_cpp_directive == cpp_directive signature_lines.append(other_signature_line) entity_ids.append(sibling["id"]) doc = entity["doc"] assert doc is not None body = _transform_doc_comment(env, doc, summary=summary) sphinx_utils.append_directive_to_stringlist( out, cpp_directive, "\n".join(signature_lines), options={ "noindexentry": True, "noindex": summary, "node-id": "", "symbol-ids": json.dumps(entity_ids), }, source_path=location["file"], source_line=location["line"] - 1, content=body, ) with apigen_utils.save_rst_defaults(env), save_cpp_scope(env): nodes = [ x for x in sphinx_utils.parse_rst(state=state, text=out) if isinstance(x, sphinx.addnodes.desc) ] assert len(nodes) == 1 objdesc = nodes[0] desc_name_nodes: List[docutils.nodes.Element] = [] for signode in objdesc.children[num_include_lines:-1]: assert isinstance(signode, sphinx.addnodes.desc_signature) # Find the node in the signature containing the actual unqualified name. for sub_node in signode.findall(condition=sphinx.addnodes.desc_name): if "sig-name-nonprimary" in sub_node["classes"]: continue desc_name_nodes.append(sub_node) break else: raise ValueError("Failed to find desc_name node") obj_content = cast(sphinx.addnodes.desc_content, objdesc.children[-1]) for entity_id, signode in zip(entity_ids, objdesc.children[num_include_lines:-1]): assert isinstance(signode, sphinx.addnodes.desc_signature) _strip_template_parameters( api_data, api_data.entities[entity_id], signode, strip_ancestors_parameters=not summary, strip_self_parameters=summary, ) if summary: _summarize_explicit(signode) if summary: objdesc["classes"].append("summary") # Insert a link around the `desc_name` field for i, desc_name_node in enumerate(desc_name_nodes): xref_node = sphinx.addnodes.pending_xref( "", desc_name_node.deepcopy(), refdomain="std", reftype="doc", reftarget="/" + api_data.get_entity_page_path(entity), refwarn=True, refexplicit=True, ) desc_name_node.replace_self(xref_node) desc_name_nodes[i] = xref_node else: if entity["kind"] == "enum": _add_enumerators( env=env, api_data=api_data, entity=entity, obj_content=obj_content, parent_scope=scope_template_prefix + scope, state=state, ) member_groups = api_data.get_entity_sub_groups(entity["id"], member=True) _merge_summary_nodes_into( env=env, api_data=api_data, state=state, contentnode=obj_content, groups=member_groups, top_level_group=False, ) contentnode += nodes def _add_enumerators( env: sphinx.environment.BuildEnvironment, api_data: CppApiData, entity: api_parser.EnumEntity, parent_scope: str, obj_content: sphinx.addnodes.desc_content, state: docutils.parsers.rst.states.RSTState, ) -> None: parent_object_name = api_data.get_entity_object_name(entity) for child in entity["enumerators"]: name = child["name"] location = child["location"] out = docutils.statemachine.StringList() sphinx_utils.append_directive_to_stringlist( out, "cpp:namespace", f'{parent_scope}::{entity["name"]}', source_path=location["file"], source_line=location["line"] - 1, ) anchor = f"e-{name}" child_doc = child["doc"] assert child_doc is not None sphinx_utils.append_directive_to_stringlist( out, "cpp:enumerator", child["decl"], options={ "noindexentry": True, "symbol-ids": json.dumps([child["id"]]), "node-id": anchor, }, source_path=location["file"], source_line=location["line"] - 1, content=_transform_doc_comment(env, child_doc, summary=False), ) with apigen_utils.save_rst_defaults(env), save_cpp_scope(env): nodes = [ x for x in sphinx_utils.parse_rst(state=state, text=out) if isinstance(x, sphinx.addnodes.desc) ] assert len(nodes) == 1 enumerator_objdesc = nodes[0] obj_content += enumerator_objdesc enumerator_signode, enumerator_doc_node = enumerator_objdesc.children assert isinstance(enumerator_signode, sphinx.addnodes.desc_signature) enumerator_signode["toc_title"] = name def _add_group_summary( env: sphinx.environment.BuildEnvironment, api_data: CppApiData, state: docutils.parsers.rst.states.RSTState, entities: List[EntityId], parent: docutils.nodes.Element, notoc: bool = False, ) -> None: toc_entries = [] name_counts = collections.Counter( api_data.entities[entity_id]["name"] for entity_id in entities ) for entity_id in entities: entity = api_data.entities[entity_id] path = api_data.get_entity_page_path(entity) _add_entity_description( env=env, api_data=api_data, entity=entity, contentnode=parent, summary=True, state=state, ) title = entity["name"] if name_counts[title] > 1: special_id = entity.get("special_id") if special_id is not None: title += f" [{special_id}]" if not notoc: toc_entries.append((title, path)) parent.extend( sphinx_utils.make_toctree_node( state=state, toc_entries=toc_entries, options={"hidden": True}, source_path=__name__, ) ) def _merge_summary_nodes_into( env: sphinx.environment.BuildEnvironment, api_data: CppApiData, state: docutils.parsers.rst.states.RSTState, contentnode: docutils.nodes.Element, groups: Dict[str, List[EntityId]], top_level_group: bool, ) -> None: """Merges the member summary into `contentnode`. Members are organized into groups. The group is either specified explicitly by a `Group:` field in the docstring, or determined automatically by `_get_group_name`. If there is an existing section, the member summary is appended to it. Otherwise, a new section is created. Args: contentnode: The existing container to which the member summaries will be added. If `contentnode` contains sections, those sections correspond to group names. """ apigen_utils.merge_groups_into( parent=contentnode, group_id_prefix=APIDOC_SECTION_ID_PREFIX if top_level_group else "", groups=groups, insert_group=lambda entities, section: _add_group_summary( env=env, api_data=api_data, state=state, entities=entities, parent=section ), ) class CppApigenEntityPageDirective(sphinx.util.docutils.SphinxDirective): """Documents an entity and summarizes its members, if applicable.""" has_content = False final_argument_whitespace = True required_arguments = 1 optional_arguments = 0 def run(self) -> List[docutils.nodes.Node]: # Wrap in a section section = docutils.nodes.section() section["ids"].append("") # Sphinx treates the first child of a `section` node as the title, # regardless of its type. We use a comment node to avoid adding a title # that would be redundant with the object description. comment_placeholder = docutils.nodes.comment("", "") section += comment_placeholder api_data = _get_cpp_api_data(self.env) entity_id = self.arguments[0] entity = api_data.entities.get(entity_id) if entity is None: logger.error( "Undefined C++ entity: %r", entity_id, location=self.get_location() ) return [] _add_entity_description( env=self.env, api_data=api_data, entity=entity, contentnode=section, summary=False, state=self.state, ) comment_placeholder.replace_self( docutils.nodes.comment("", api_data.get_entity_object_name(entity)) ) non_member_groups = api_data.get_entity_sub_groups(entity_id, member=False) _merge_summary_nodes_into( env=self.env, api_data=api_data, state=self.state, contentnode=section, groups=non_member_groups, top_level_group=False, ) return [section] class CppApigenTopLevelGroupDirective(sphinx.util.docutils.SphinxDirective): """Summarizes the members of a top-level group.""" has_content = False final_argument_whitespace = True required_arguments = 1 optional_arguments = 0 option_spec = { "notoc": docutils.parsers.rst.directives.flag, } def run(self) -> List[docutils.nodes.Node]: env = self.env api_data = _get_cpp_api_data(env) group_id = docutils.nodes.make_id(self.arguments[0]) entities = api_data.groups.get(group_id) if not entities: logger.warning( "No entities in group %r", group_id, location=self.get_location(), ) return [] contentnode = docutils.nodes.section() _add_group_summary( env=self.env, api_data=api_data, state=self.state, entities=entities, parent=contentnode, notoc="notoc" in self.options, ) return contentnode.children class CppApigenEntitySummaryDirective(sphinx.util.docutils.SphinxDirective): """Generates a summary/link to a C++ entity.""" has_content = False final_argument_whitespace = True required_arguments = 1 optional_arguments = 0 option_spec = { "notoc": docutils.parsers.rst.directives.flag, } def run(self) -> List[docutils.nodes.Node]: env = self.env api_data = _get_cpp_api_data(env) entity_id = self.arguments[0] entity = api_data.entities.get(entity_id) if entity is None: logger.warning( "No C++ entity named: %r", entity_id, location=self.get_location(), ) return [] contentnode = docutils.nodes.section() _add_group_summary( env=self.env, api_data=api_data, state=self.state, entities=[entity_id], parent=contentnode, notoc="notoc" in self.options, ) return contentnode.children def _builder_inited(app: sphinx.application.Sphinx) -> None: """Generates the rST files for API members.""" api_data = _get_cpp_api_data(app.env, warn=True) apigen_configs = getattr(app, _CONFIG_ATTR) writer = apigen_utils.GeneratedDocumentWriter( app=app, case_insensitive_filesystem=app.config.cpp_apigen_case_insensitive_filesystem, output_prefixes=[ apigen_config.document_prefix for apigen_config in apigen_configs ], generator_module=__name__, ) writer.prepare_output_directories() for entity in api_data.entities.values(): page_name = entity.get("page_name") if page_name is None: continue entity["page_name"] = apigen_utils.make_unique_docname( page_name, writer.case_insensitive_filesystem ) def get_entities(): for entity_id in api_data.get_documented_entities(): entity = api_data.entities[entity_id] content = sphinx_utils.format_directive( "cpp-apigen-entity-page", entity_id, ) object_name = api_data.get_entity_object_name(entity) docname = api_data.get_entity_page_path(entity) yield (docname, object_name, content) writer.write_files(get_entities()) def _warn_missing_reference( app: sphinx.application.Sphinx, domain, node: sphinx.addnodes.pending_xref ) -> bool: if not isinstance(domain, sphinx.domains.cpp.CPPDomain): return False api_data = _get_cpp_api_data(app.env) source, line = docutils.utils.get_source_line(node) return (node["reftarget"], source, line) in api_data.nonitpick _CONFIG_ATTR = "_sphinx_immaterial_cpp_apigen_configs" def _config_inited( app: sphinx.application.Sphinx, config: sphinx.config.Config ) -> None: apigen_configs = cast( List[ApigenConfig], pydantic.TypeAdapter(List[ApigenConfig]).validate_python( config.cpp_apigen_configs ), ) setattr(app, _CONFIG_ATTR, apigen_configs) def setup(app: sphinx.application.Sphinx): app.connect("config-inited", _config_inited) app.connect("builder-inited", _builder_inited) app.connect("warn-missing-reference", _warn_missing_reference) app.add_directive("cpp-apigen-entity-page", CppApigenEntityPageDirective) app.add_directive("cpp-apigen-group", CppApigenTopLevelGroupDirective) app.add_directive("cpp-apigen-entity-summary", CppApigenEntitySummaryDirective) app.add_config_value( "cpp_apigen_configs", default=[], rebuild="env", types=(List[ApigenConfig],) ) app.add_config_value( "cpp_apigen_case_insensitive_filesystem", default=None, types=(bool, type(None)), rebuild="env", ) app.add_config_value( "cpp_apigen_rst_prolog", types=(str,), default="", rebuild="env" ) app.add_config_value( "cpp_apigen_rst_epilog", types=(str,), default="", rebuild="env" ) return {"parallel_read_safe": True, "parallel_write_safe": True}

Last update: Mar 08, 2024