From 298dbdca15f5c73f7ca76df9c60cc8569c5e3394 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 16:09:47 -0700 Subject: [PATCH 1/8] Replace deprecated docutils traverse with findall --- .../apidoc/cpp/cpp_resolve_c_xrefs.py | 2 +- .../apidoc/cpp/parameter_objects.py | 6 +++--- sphinx_immaterial/apidoc/cpp/signodes.py | 2 +- .../cpp/strip_namespaces_from_signatures.py | 2 +- sphinx_immaterial/apidoc/format_signatures.py | 4 ++-- sphinx_immaterial/apidoc/object_toc.py | 2 +- sphinx_immaterial/apidoc/python/apigen.py | 18 +++++++++--------- .../apidoc/python/domain_fixes.py | 10 +++++----- sphinx_immaterial/sphinx_utils.py | 4 ++-- 9 files changed, 25 insertions(+), 25 deletions(-) diff --git a/sphinx_immaterial/apidoc/cpp/cpp_resolve_c_xrefs.py b/sphinx_immaterial/apidoc/cpp/cpp_resolve_c_xrefs.py index 7732490cb..f29fc9770 100644 --- a/sphinx_immaterial/apidoc/cpp/cpp_resolve_c_xrefs.py +++ b/sphinx_immaterial/apidoc/cpp/cpp_resolve_c_xrefs.py @@ -107,7 +107,7 @@ def run( c_parent_key = self.env.ref_context.get("c:parent_key") if c_parent_key is not None: for node in nodes: - for refnode in node.traverse(condition=sphinx.addnodes.pending_xref): + for refnode in node.findall(condition=sphinx.addnodes.pending_xref): if refnode.get("refdomain") == "cpp": refnode["c:parent_key"] = c_parent_key return nodes, messages diff --git a/sphinx_immaterial/apidoc/cpp/parameter_objects.py b/sphinx_immaterial/apidoc/cpp/parameter_objects.py index 78c153776..23c3b9fc8 100644 --- a/sphinx_immaterial/apidoc/cpp/parameter_objects.py +++ b/sphinx_immaterial/apidoc/cpp/parameter_objects.py @@ -198,12 +198,12 @@ def add_replacement( param_node_copy.line = line sig_param_nodes[name] = param_node_copy del name_node[node_identifier_key] - for name_node_copy in param_node_copy.traverse(condition=type(name_node)): + for name_node_copy in param_node_copy.findall(condition=type(name_node)): if name_node_copy.get(node_identifier_key): return name_node_copy raise ValueError("Could not locate name node within parameter") - for sig_param_node in signode.traverse(condition=sphinx.addnodes.desc_sig_name): + for sig_param_node in signode.findall(condition=sphinx.addnodes.desc_sig_name): desc_param_node = sig_param_node.parent if not isinstance(desc_param_node, sphinx.addnodes.desc_parameter): continue @@ -212,7 +212,7 @@ def add_replacement( new_sig_param_node = add_replacement(sig_param_node, sig_param_node.parent) new_sig_param_node["classes"].append("sig-name") - for desc_sig_name_node in signode.traverse(condition=sphinx.addnodes.desc_sig_name): + for desc_sig_name_node in signode.findall(condition=sphinx.addnodes.desc_sig_name): parent = desc_sig_name_node.parent if not isinstance(parent, sphinx.addnodes.desc_name): continue diff --git a/sphinx_immaterial/apidoc/cpp/signodes.py b/sphinx_immaterial/apidoc/cpp/signodes.py index 0f92e2771..f58c69ef0 100644 --- a/sphinx_immaterial/apidoc/cpp/signodes.py +++ b/sphinx_immaterial/apidoc/cpp/signodes.py @@ -54,7 +54,7 @@ def describe_signature_as_introducer( orig_describe_signature_as_introducer( self, fake_parent, mode, env, symbol, lineSpec ) - for x in fake_parent.traverse(condition=sphinx.addnodes.desc_name): + for x in fake_parent.findall(condition=sphinx.addnodes.desc_name): # Ensure template parameter names aren't styled as the main entity # name. x["classes"].append("sig-name-nonprimary") diff --git a/sphinx_immaterial/apidoc/cpp/strip_namespaces_from_signatures.py b/sphinx_immaterial/apidoc/cpp/strip_namespaces_from_signatures.py index 8f508ce60..2b04d7468 100644 --- a/sphinx_immaterial/apidoc/cpp/strip_namespaces_from_signatures.py +++ b/sphinx_immaterial/apidoc/cpp/strip_namespaces_from_signatures.py @@ -12,7 +12,7 @@ def _strip_namespaces_from_signature( ): # Collect nodes to remove first, then remove them in reverse order. removals = [] - for child in node.traverse(condition=sphinx.addnodes.desc_sig_name): + for child in node.findall(condition=sphinx.addnodes.desc_sig_name): parent = child.parent if not isinstance(parent, sphinx.addnodes.pending_xref): continue diff --git a/sphinx_immaterial/apidoc/format_signatures.py b/sphinx_immaterial/apidoc/format_signatures.py index a8070f531..f351be63e 100644 --- a/sphinx_immaterial/apidoc/format_signatures.py +++ b/sphinx_immaterial/apidoc/format_signatures.py @@ -58,7 +58,7 @@ class CollectSignaturesTransform(sphinx.transforms.SphinxTransform): def apply(self, **kwargs: Any) -> None: collected_signatures = _get_collected_signatures(self.env) - for node in self.document.traverse(sphinx.addnodes.desc_signature): + for node in self.document.findall(sphinx.addnodes.desc_signature): parent = node.parent domain: str = parent.get("domain") objtype: str = parent.get("objtype") @@ -249,7 +249,7 @@ def apply(self, **kwargs: Any) -> None: formatted_signatures = getattr(self.env, _FORMATTED_SIGNATURES, None) if formatted_signatures is None: return - for node in self.document.traverse(sphinx.addnodes.desc_signature): + for node in self.document.findall(sphinx.addnodes.desc_signature): signature_id = node.get(_SIGNATURE_FORMAT_ID) if signature_id is None: continue diff --git a/sphinx_immaterial/apidoc/object_toc.py b/sphinx_immaterial/apidoc/object_toc.py index e47d204e5..99e178d74 100644 --- a/sphinx_immaterial/apidoc/object_toc.py +++ b/sphinx_immaterial/apidoc/object_toc.py @@ -51,7 +51,7 @@ def _make_section_from_desc( title = signature.get("toc_title", None) if not title: title = "" - for child in signature.traverse(): + for child in signature.findall(): if isinstance( child, (sphinx.addnodes.desc_name, sphinx.addnodes.desc_addname) ): diff --git a/sphinx_immaterial/apidoc/python/apigen.py b/sphinx_immaterial/apidoc/python/apigen.py index 823af2f37..190dd4fd5 100644 --- a/sphinx_immaterial/apidoc/python/apigen.py +++ b/sphinx_immaterial/apidoc/python/apigen.py @@ -207,7 +207,7 @@ def _get_overloads_from_documenter( def _has_default_value(node: sphinx.addnodes.desc_parameter): - for sub_node in node.traverse(condition=docutils.nodes.literal): + for sub_node in node.findall(condition=docutils.nodes.literal): if "default_value" in sub_node.get("classes"): return True return False @@ -229,7 +229,7 @@ def _must_shorten(): return len(node.astext()) > column_limit parameterlist: Optional[sphinx.addnodes.desc_parameterlist] = None - for parameterlist in node.traverse(condition=sphinx.addnodes.desc_parameterlist): + for parameterlist in node.findall(condition=sphinx.addnodes.desc_parameterlist): break if parameterlist is None: @@ -409,7 +409,7 @@ def _ensure_module_name_in_signature(signode: sphinx.addnodes.desc_signature) -> :param signode: Signature to modify in place. """ - for node in signode.traverse(condition=sphinx.addnodes.desc_addname): + for node in signode.findall(condition=sphinx.addnodes.desc_addname): modname = signode.get("module") if modname and not node.astext().startswith(modname + "."): node.insert(0, docutils.nodes.Text(modname + ".")) @@ -463,7 +463,7 @@ def _mark_subscript_parameterlist(signode: sphinx.addnodes.desc_signature) -> No :param node: Signature to modify in place. """ - for sub_node in signode.traverse(condition=sphinx.addnodes.desc_parameterlist): + for sub_node in signode.findall(condition=sphinx.addnodes.desc_parameterlist): sub_node["parens"] = ("[", "]") @@ -476,13 +476,13 @@ def _clean_init_signature(signode: sphinx.addnodes.desc_signature) -> None: :param node: Signature to modify in place. """ # Remove first parameter. - for param in signode.traverse(condition=sphinx.addnodes.desc_parameter): + for param in signode.findall(condition=sphinx.addnodes.desc_parameter): if param.children[0].astext() == "self": param.parent.remove(param) break # Remove return type. - for node in signode.traverse(condition=sphinx.addnodes.desc_returns): + for node in signode.findall(condition=sphinx.addnodes.desc_returns): node.parent.remove(node) @@ -496,7 +496,7 @@ def _clean_class_getitem_signature(signode: sphinx.addnodes.desc_signature) -> N """ # Remove `static` prefix - for prefix in signode.traverse(condition=sphinx.addnodes.desc_annotation): + for prefix in signode.findall(condition=sphinx.addnodes.desc_annotation): prefix.parent.remove(prefix) break @@ -624,7 +624,7 @@ def _generate_entity_summary( ) for sig_node in cast(List[sphinx.addnodes.desc_signature], objdesc.children[:-1]): # Insert a link around the `desc_name` field - for sub_node in sig_node.traverse(condition=sphinx.addnodes.desc_name): + for sub_node in sig_node.findall(condition=sphinx.addnodes.desc_name): if include_in_toc: sub_node["classes"].append("pseudo-toc-entry") xref_node = sphinx.addnodes.pending_xref( @@ -726,7 +726,7 @@ def _merge_summary_nodes_into( """ sections: Dict[str, docutils.nodes.section] = {} - for section in contentnode.traverse(condition=docutils.nodes.section): + for section in contentnode.findall(condition=docutils.nodes.section): if section["ids"]: sections[section["ids"][0]] = section diff --git a/sphinx_immaterial/apidoc/python/domain_fixes.py b/sphinx_immaterial/apidoc/python/domain_fixes.py index 302317a00..96c3f03de 100644 --- a/sphinx_immaterial/apidoc/python/domain_fixes.py +++ b/sphinx_immaterial/apidoc/python/domain_fixes.py @@ -134,7 +134,7 @@ def parse_arglist( arglist: str, env: Optional[sphinx.environment.BuildEnvironment] = None ) -> sphinx.addnodes.desc_parameterlist: result = orig_parse_arglist(arglist, env) - for node in result.traverse(condition=docutils.nodes.inline): + for node in result.findall(condition=docutils.nodes.inline): if "default_value" not in node["classes"]: continue node.replace_self( @@ -468,12 +468,12 @@ def add_replacement( param_node_copy.line = line sig_param_nodes[name] = param_node_copy del name_node[node_identifier_key] - for name_node_copy in param_node_copy.traverse(condition=type(name_node)): + for name_node_copy in param_node_copy.findall(condition=type(name_node)): if name_node_copy.get(node_identifier_key): return name_node_copy raise ValueError("Could not locate name node within parameter") - for desc_param_node in signode.traverse(condition=sphinx.addnodes.desc_parameter): + for desc_param_node in signode.findall(condition=sphinx.addnodes.desc_parameter): for sig_param_node in desc_param_node: if not isinstance(sig_param_node, sphinx.addnodes.desc_sig_name): continue @@ -814,7 +814,7 @@ def _maybe_strip_type_annotations( for signode in obj_desc[:-1]: assert isinstance(signode, sphinx.addnodes.desc_signature) if strip_self_type_annotations: - for param in signode.traverse(condition=sphinx.addnodes.desc_parameter): + for param in signode.findall(condition=sphinx.addnodes.desc_parameter): if param.children[0].astext() == "self": # Remove any annotations on `self` del param.children[1:] @@ -829,7 +829,7 @@ def _maybe_strip_type_annotations( fullname = modname + "." + fullname if strip_return_type_annotations.fullmatch(fullname): # Remove return type. - for node in signode.traverse(condition=sphinx.addnodes.desc_returns): + for node in signode.findall(condition=sphinx.addnodes.desc_returns): node.parent.remove(node) diff --git a/sphinx_immaterial/sphinx_utils.py b/sphinx_immaterial/sphinx_utils.py index 0cac52bfd..26742693d 100644 --- a/sphinx_immaterial/sphinx_utils.py +++ b/sphinx_immaterial/sphinx_utils.py @@ -150,7 +150,7 @@ def summarize_element_text( # Recurisvely extract first paragraph while True: - for p in node.traverse(condition=docutils.nodes.paragraph): + for p in node.findall(condition=docutils.nodes.paragraph): if p is node: continue node = p @@ -184,7 +184,7 @@ def make_toctree_node( source_line=source_line, ) toctree: Optional[sphinx.addnodes.toctree] = None - for node in toctree_nodes[-1].traverse(condition=sphinx.addnodes.toctree): + for node in toctree_nodes[-1].findall(condition=sphinx.addnodes.toctree): toctree = node break if toctree is None: From 80acb31e924201b2d9b9474adeec87a490ee9906 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Wed, 27 Jul 2022 14:02:46 -0700 Subject: [PATCH 2/8] Refactor Python domain and monkey patching to prevent issues in unit tests This commit separates all of the Python domain fixes into separate files, as was done for C++. Additionally, this commit ensures that any monkey patching is done during module initialization rather than during extension setup, to avoid applying the same monkey patch more than once if there is more than one build performed in the same Python process, as happens during unit tests. --- sphinx_immaterial/__init__.py | 3 +- sphinx_immaterial/apidoc/object_toc.py | 24 +- .../apidoc/python/annotation_style.py | 31 + .../apidoc/python/attribute_style.py | 34 + .../apidoc/python/autodoc_property_type.py | 3 + sphinx_immaterial/apidoc/python/default.py | 27 + .../apidoc/python/domain_fixes.py | 934 ------------------ .../python/napoleon_admonition_classes.py | 20 + sphinx_immaterial/apidoc/python/object_ids.py | 125 +++ .../apidoc/python/parameter_objects.py | 520 ++++++++++ .../apidoc/python/section_titles.py | 36 + .../apidoc/python/strip_property_prefix.py | 52 + .../strip_self_and_return_type_annotations.py | 74 ++ .../python/style_default_values_as_code.py | 33 + .../apidoc/python/subscript_methods.py | 12 + sphinx_immaterial/apidoc/python/synopses.py | 150 +++ 16 files changed, 1128 insertions(+), 950 deletions(-) create mode 100644 sphinx_immaterial/apidoc/python/annotation_style.py create mode 100644 sphinx_immaterial/apidoc/python/attribute_style.py create mode 100644 sphinx_immaterial/apidoc/python/default.py create mode 100644 sphinx_immaterial/apidoc/python/napoleon_admonition_classes.py create mode 100644 sphinx_immaterial/apidoc/python/object_ids.py create mode 100644 sphinx_immaterial/apidoc/python/parameter_objects.py create mode 100644 sphinx_immaterial/apidoc/python/section_titles.py create mode 100644 sphinx_immaterial/apidoc/python/strip_property_prefix.py create mode 100644 sphinx_immaterial/apidoc/python/strip_self_and_return_type_annotations.py create mode 100644 sphinx_immaterial/apidoc/python/style_default_values_as_code.py create mode 100644 sphinx_immaterial/apidoc/python/subscript_methods.py create mode 100644 sphinx_immaterial/apidoc/python/synopses.py diff --git a/sphinx_immaterial/__init__.py b/sphinx_immaterial/__init__.py index 91923f9cb..efa400053 100644 --- a/sphinx_immaterial/__init__.py +++ b/sphinx_immaterial/__init__.py @@ -310,8 +310,7 @@ def setup(app): app.connect("config-inited", _config_inited) app.setup_extension(apidoc_formatting.__name__) - app.setup_extension("sphinx_immaterial.apidoc.python.domain_fixes") - app.setup_extension("sphinx_immaterial.apidoc.python.type_annotation_transforms") + app.setup_extension("sphinx_immaterial.apidoc.python.default") app.setup_extension("sphinx_immaterial.apidoc.cpp.default") app.setup_extension(nav_adapt.__name__) app.setup_extension("sphinx_immaterial.postprocess_html") diff --git a/sphinx_immaterial/apidoc/object_toc.py b/sphinx_immaterial/apidoc/object_toc.py index 99e178d74..838447be8 100644 --- a/sphinx_immaterial/apidoc/object_toc.py +++ b/sphinx_immaterial/apidoc/object_toc.py @@ -4,23 +4,19 @@ import docutils.nodes import sphinx.addnodes import sphinx.application -import sphinx.environment.collectors.toctree +from sphinx.environment.collectors.toctree import TocTreeCollector from . import apidoc_formatting -def _monkey_patch_toc_tree_process_doc(app: sphinx.application.Sphinx): - """Enables support for also finding Sphinx domain objects. - - Args: - app: Sphinx application. - """ - TocTreeCollector = sphinx.environment.collectors.toctree.TocTreeCollector +def _monkey_patch_toc_tree_process_doc(): + """Enables support for also finding Sphinx domain objects.""" # Apply the monkey patch orig_process_doc = TocTreeCollector.process_doc def _make_section_from_desc( + app: sphinx.application.Sphinx, source: sphinx.addnodes.desc, ) -> Optional[docutils.nodes.section]: env = app.env @@ -118,7 +114,7 @@ def _make_section_from_term( return section def _patched_process_doc( - self: sphinx.environment.collectors.toctree.TocTreeCollector, + self: TocTreeCollector, app: sphinx.application.Sphinx, doctree: docutils.nodes.document, ) -> None: @@ -149,7 +145,7 @@ def _collect( return elif isinstance(source, sphinx.addnodes.desc): # Object description. Try to create synthetic section. - new_node = _make_section_from_desc(source) + new_node = _make_section_from_desc(app, source) if new_node is not None: target += new_node target = new_node @@ -182,6 +178,10 @@ def _collect( TocTreeCollector.process_doc = _patched_process_doc # type: ignore +_monkey_patch_toc_tree_process_doc() + +def setup(app: sphinx.application.Sphinx): + # TocTreeCollector is registered before our extension is. In order for the # monkey patching to take effect, we need to unregister it and re-register it. for read_listener in app.events.listeners["doctree-read"]: @@ -190,10 +190,6 @@ def _collect( obj.disable(app) app.add_env_collector(TocTreeCollector) break - - -def setup(app: sphinx.application.Sphinx): - _monkey_patch_toc_tree_process_doc(app) return { "parallel_read_safe": True, "parallel_write_safe": True, diff --git a/sphinx_immaterial/apidoc/python/annotation_style.py b/sphinx_immaterial/apidoc/python/annotation_style.py new file mode 100644 index 000000000..91484503b --- /dev/null +++ b/sphinx_immaterial/apidoc/python/annotation_style.py @@ -0,0 +1,31 @@ +from typing import Optional, List + +import docutils.nodes +import sphinx.domains.python +import sphinx.environment + + +def ensure_wrapped_in_desc_type( + nodes: List[docutils.nodes.Node], +) -> List[docutils.nodes.Node]: + if len(nodes) != 1 or not isinstance(nodes[0], sphinx.addnodes.desc_type): + nodes = [sphinx.addnodes.desc_type("", "", *nodes)] + return nodes + + +def _monkey_patch_python_parse_annotation(): + """Ensures that type annotations in signatures are wrapped in `desc_type`. + + This allows them to be distinguished from parameter names in CSS rules. + """ + orig_parse_annotation = sphinx.domains.python._parse_annotation + + def parse_annotation( + annotation: str, env: Optional[sphinx.environment.BuildEnvironment] = None + ) -> List[docutils.nodes.Node]: + return ensure_wrapped_in_desc_type(orig_parse_annotation(annotation, env)) + + sphinx.domains.python._parse_annotation = parse_annotation + + +_monkey_patch_python_parse_annotation() diff --git a/sphinx_immaterial/apidoc/python/attribute_style.py b/sphinx_immaterial/apidoc/python/attribute_style.py new file mode 100644 index 000000000..8229cdb59 --- /dev/null +++ b/sphinx_immaterial/apidoc/python/attribute_style.py @@ -0,0 +1,34 @@ +from typing import Type, Tuple + +import docutils.nodes +import sphinx.addnodes +import sphinx.domains.python + + +def _monkey_patch_pyattribute_handle_signature( + directive_cls: Type[sphinx.domains.python.PyObject], +): + """Modifies PyAttribute or PyVariable to improve styling of signature.""" + + def handle_signature( + self, sig: str, signode: sphinx.addnodes.desc_signature + ) -> Tuple[str, str]: + result = super(directive_cls, self).handle_signature(sig, signode) + typ = self.options.get("type") + if typ: + signode += sphinx.addnodes.desc_sig_punctuation("", " : ") + signode += sphinx.domains.python._parse_annotation(typ, self.env) + + value = self.options.get("value") + if value: + signode += sphinx.addnodes.desc_sig_punctuation("", " = ") + signode += docutils.nodes.literal( + text=value, classes=["code", "python"], language="python" + ) + return result + + directive_cls.handle_signature = handle_signature # type: ignore + + +_monkey_patch_pyattribute_handle_signature(sphinx.domains.python.PyAttribute) +_monkey_patch_pyattribute_handle_signature(sphinx.domains.python.PyVariable) diff --git a/sphinx_immaterial/apidoc/python/autodoc_property_type.py b/sphinx_immaterial/apidoc/python/autodoc_property_type.py index 23047b471..efa839e6f 100644 --- a/sphinx_immaterial/apidoc/python/autodoc_property_type.py +++ b/sphinx_immaterial/apidoc/python/autodoc_property_type.py @@ -112,3 +112,6 @@ def handle_signature( return fullname, prefix PyProperty.handle_signature = handle_signature + + +apply_property_documenter_type_annotation_fix() diff --git a/sphinx_immaterial/apidoc/python/default.py b/sphinx_immaterial/apidoc/python/default.py new file mode 100644 index 000000000..65041c87d --- /dev/null +++ b/sphinx_immaterial/apidoc/python/default.py @@ -0,0 +1,27 @@ +import sphinx.application + +from . import annotation_style +from . import object_ids +from . import synopses +from . import parameter_objects +from . import style_default_values_as_code +from . import subscript_methods +from . import attribute_style +from . import napoleon_admonition_classes +from . import strip_property_prefix +from . import section_titles +from . import autodoc_property_type +from . import type_annotation_transforms +from . import strip_self_and_return_type_annotations + + +def setup(app: sphinx.application.Sphinx): + app.setup_extension(parameter_objects.__name__) + app.setup_extension(strip_property_prefix.__name__) + app.setup_extension(type_annotation_transforms.__name__) + app.setup_extension(strip_self_and_return_type_annotations.__name__) + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/sphinx_immaterial/apidoc/python/domain_fixes.py b/sphinx_immaterial/apidoc/python/domain_fixes.py index 96c3f03de..3dbf7b25e 100644 --- a/sphinx_immaterial/apidoc/python/domain_fixes.py +++ b/sphinx_immaterial/apidoc/python/domain_fixes.py @@ -41,941 +41,7 @@ logger = sphinx.util.logging.getLogger(__name__) -def _ensure_wrapped_in_desc_type( - nodes: List[docutils.nodes.Node], -) -> List[docutils.nodes.Node]: - if len(nodes) != 1 or not isinstance(nodes[0], sphinx.addnodes.desc_type): - nodes = [sphinx.addnodes.desc_type("", "", *nodes)] - return nodes - - -def _monkey_patch_python_doc_fields(): - def make_field( - self: PyTypedField, - types: Dict[str, List[docutils.nodes.Node]], - domain: str, - items: Sequence[Tuple[str, str]], - env: Optional[sphinx.environment.BuildEnvironment] = None, - inliner: Optional[docutils.parsers.rst.states.Inliner] = None, - location: Optional[docutils.nodes.Node] = None, - ) -> docutils.nodes.field: - bodynode = docutils.nodes.definition_list() - bodynode["classes"].append("api-field") - bodynode["classes"].append("highlight") - - def handle_item(fieldarg: str, content: Any) -> docutils.nodes.Node: - node = docutils.nodes.definition_list_item() - term_node = docutils.nodes.term() - term_node["paramname"] = fieldarg - term_node += sphinx.addnodes.desc_name(fieldarg, fieldarg) - fieldtype = types.pop(fieldarg, None) - if fieldtype: - term_node += sphinx.addnodes.desc_sig_punctuation("", " : ") - fieldtype_node = sphinx.addnodes.desc_type() - if len(fieldtype) == 1 and isinstance( - fieldtype[0], docutils.nodes.Text - ): - typename = fieldtype[0].astext() - term_node["paramtype"] = typename - fieldtype_node.extend( - _ensure_wrapped_in_desc_type( - self.make_xrefs( - cast(str, self.typerolename), - domain, - typename, - docutils.nodes.Text, - env=env, - inliner=inliner, - location=location, - ) - ) - ) - else: - fieldtype_node += fieldtype - term_node += fieldtype_node - node += term_node - def_node = docutils.nodes.definition() - p = docutils.nodes.paragraph() - p += content - def_node += p - node += def_node - return node - - for fieldarg, content in items: - bodynode += handle_item(fieldarg, content) - fieldname = docutils.nodes.field_name("", cast(str, self.label)) - fieldbody = docutils.nodes.field_body("", bodynode) - return docutils.nodes.field("", fieldname, fieldbody) - - PyTypedField.make_field = make_field - - -def _monkey_patch_python_parse_annotation(): - """Ensures that type annotations in signatures are wrapped in `desc_type`. - - This allows them to be distinguished from parameter names in CSS rules. - """ - orig_parse_annotation = sphinx.domains.python._parse_annotation - - def parse_annotation( - annotation: str, env: Optional[sphinx.environment.BuildEnvironment] = None - ) -> List[docutils.nodes.Node]: - return _ensure_wrapped_in_desc_type(orig_parse_annotation(annotation, env)) - - sphinx.domains.python._parse_annotation = parse_annotation - - -def _monkey_patch_python_parse_arglist(): - """Ensures default values in signatures are styled as code.""" - - orig_parse_arglist = sphinx.domains.python._parse_arglist - - def parse_arglist( - arglist: str, env: Optional[sphinx.environment.BuildEnvironment] = None - ) -> sphinx.addnodes.desc_parameterlist: - result = orig_parse_arglist(arglist, env) - for node in result.findall(condition=docutils.nodes.inline): - if "default_value" not in node["classes"]: - continue - node.replace_self( - docutils.nodes.literal( - text=node.astext(), - classes=["code", "python", "default_value"], - language="python", - ) - ) - return result - - sphinx.domains.python._parse_arglist = parse_arglist - - -def _monkey_patch_python_get_signature_prefix( - directive_cls: Type[sphinx.domains.python.PyObject], -) -> None: - orig_get_signature_prefix = directive_cls.get_signature_prefix - - def get_signature_prefix(self, sig: str) -> Union[str, List[docutils.nodes.Node]]: - prefix = orig_get_signature_prefix(self, sig) - if not self.env.config.python_strip_property_prefix: - return prefix - if sphinx.version_info >= (4, 3): - prefix = cast(List[docutils.nodes.Node], prefix) - assert isinstance(prefix, list) - for prop_idx, node in enumerate(prefix): - if node == "property": - assert isinstance( - prefix[prop_idx + 1], sphinx.addnodes.desc_sig_space - ) - prefix = list(prefix) - del prefix[prop_idx : prop_idx + 2] - break - return prefix - prefix = cast(str, prefix) # type: ignore - assert isinstance(prefix, str) - parts = prefix.strip().split(" ") - if "property" in parts: - parts.remove("property") - if parts: - return " ".join(parts) + " " - return "" - - directive_cls.get_signature_prefix = get_signature_prefix # type: ignore - - -def _monkey_patch_pyattribute_handle_signature( - directive_cls: Type[sphinx.domains.python.PyObject], -): - """Modifies PyAttribute or PyVariable to improve styling of signature.""" - - def handle_signature( - self, sig: str, signode: sphinx.addnodes.desc_signature - ) -> Tuple[str, str]: - result = super(directive_cls, self).handle_signature(sig, signode) - typ = self.options.get("type") - if typ: - signode += sphinx.addnodes.desc_sig_punctuation("", " : ") - signode += sphinx.domains.python._parse_annotation(typ, self.env) - - value = self.options.get("value") - if value: - signode += sphinx.addnodes.desc_sig_punctuation("", " = ") - signode += docutils.nodes.literal( - text=value, classes=["code", "python"], language="python" - ) - return result - - directive_cls.handle_signature = handle_signature # type: ignore - - -desc_parameterlist = sphinx.addnodes.desc_parameterlist - - -def _monkey_patch_parameterlist_to_support_subscript(): - def astext(self: desc_parameterlist) -> str: - open_paren, close_paren = self.get("parens", ("(", ")")) - return f"{open_paren}{super(desc_parameterlist, self).astext()}{close_paren}" - - desc_parameterlist.astext = astext # type: ignore - - -GoogleDocstring = sphinx.ext.napoleon.docstring.GoogleDocstring - - -def _monkey_patch_napoleon_admonition_classes(): - def _add_admonition_class(method_name: str, class_name: str) -> None: - orig_method = getattr(GoogleDocstring, method_name) - - def wrapper(self: GoogleDocstring, section: str) -> List[str]: - result = orig_method(self, section) - result.insert(1, f" :class: {class_name}") - return result - - setattr(GoogleDocstring, method_name, wrapper) - - _add_admonition_class("_parse_examples_section", "example") - - -class PyParamXRefRole(sphinx.domains.python.PyXRefRole): - def process_link( - self, - env: sphinx.environment.BuildEnvironment, - refnode: docutils.nodes.Element, - has_explicit_title: bool, - title: str, - target: str, - ) -> Tuple[str, str]: - refnode["py:func"] = env.ref_context.get("py:func") - return super().process_link(env, refnode, has_explicit_title, title, target) - - -def _monkey_patch_python_domain_to_store_func_in_ref_context(): - orig_before_content = PyObject.before_content - - def before_content(self: PyObject) -> None: - orig_before_content(self) - - if not isinstance( - self, (sphinx.domains.python.PyFunction, sphinx.domains.python.PyMethod) - ): - return - setattr( - self, "_prev_ref_context_py_func", self.env.ref_context.get("py:func", None) - ) - if self.names: - fullname = self.names[-1][0] - else: - fullname = None - - if fullname: - classname = self.env.ref_context.get("py:class") - if classname and fullname.startswith(classname + "."): - fullname = fullname[len(classname) + 1 :] - self.env.ref_context["py:func"] = fullname - else: - self.env.ref_context.pop("py:func", None) - - PyObject.before_content = before_content - - orig_after_content = PyObject.after_content - - def after_content(self: PyObject) -> None: - orig_after_content(self) - if not isinstance( - self, (sphinx.domains.python.PyFunction, sphinx.domains.python.PyMethod) - ): - return - - prev_py_func = getattr(self, "_prev_ref_context_py_func", None) - if prev_py_func is None: - self.env.ref_context.pop("py:func", None) - else: - self.env.ref_context["py:func"] = prev_py_func - - PyObject.after_content = after_content - - -def _monkey_patch_python_domain_to_resolve_params(): - """Adds support to the Python domain for resolving parameter references.""" - - orig_resolve_xref = PythonDomain.resolve_xref - - def resolve_xref( - self: PythonDomain, - env: sphinx.environment.BuildEnvironment, - fromdocname: str, - builder: sphinx.builders.Builder, - typ: str, - target: str, - node: sphinx.addnodes.pending_xref, - contnode: docutils.nodes.Element, - ) -> Optional[docutils.nodes.Element]: - if typ == "param": - func_name = node.get("py:func") - if func_name and "." not in target: - target = "%s.%s" % (func_name, target) - result = orig_resolve_xref( - self, env, fromdocname, builder, typ, target, node, contnode - ) - if ( - typ == "param" - and result is None - and node.get("implicit_sig_param_ref", False) - ): - # Suppress missing reference warnings for automatically-added - # references to parameter descriptions. - raise sphinx.errors.NoUri - return result - - PythonDomain.resolve_xref = resolve_xref - - orig_resolve_any_xref = PythonDomain.resolve_any_xref - - def resolve_any_xref( - self: PythonDomain, - env: sphinx.environment.BuildEnvironment, - fromdocname: str, - builder: sphinx.builders.Builder, - target: str, - node: sphinx.addnodes.pending_xref, - contnode: docutils.nodes.Element, - ) -> List[Tuple[str, docutils.nodes.Element]]: - results = orig_resolve_any_xref( - self, env, fromdocname, builder, target, node, contnode - ) - # Don't resolve parameters as any refs, as they introduce too many - # ambiguities. - return [r for r in results if r[0] != "py:param"] - - PythonDomain.resolve_any_xref = resolve_any_xref - - -def _monkey_patch_python_domain_to_add_object_synopses_to_references(): - def _add_synopsis( - self: PythonDomain, - env: sphinx.environment.BuildEnvironment, - refnode: docutils.nodes.Element, - ) -> None: - name = refnode.get("reftitle") - obj = self.objects.get(name) - if obj is None: - return - refnode["reftitle"] = apidoc_formatting.format_object_description_tooltip( - env, - apidoc_formatting.get_object_description_options( - env, self.name, obj.objtype - ), - base_title=name, - synopsis=self.data["synopses"].get(name), - ) - - orig_resolve_xref = PythonDomain.resolve_xref - - def resolve_xref( - self: PythonDomain, - env: sphinx.environment.BuildEnvironment, - fromdocname: str, - builder: sphinx.builders.Builder, - typ: str, - target: str, - node: sphinx.addnodes.pending_xref, - contnode: docutils.nodes.Element, - ) -> Optional[docutils.nodes.Element]: - refnode = orig_resolve_xref( - self, env, fromdocname, builder, typ, target, node, contnode - ) - if refnode is not None: - _add_synopsis(self, env, refnode) - return refnode - - PythonDomain.resolve_xref = resolve_xref - - orig_resolve_any_xref = PythonDomain.resolve_any_xref - - def resolve_any_xref( - self: PythonDomain, - env: sphinx.environment.BuildEnvironment, - fromdocname: str, - builder: sphinx.builders.Builder, - target: str, - node: sphinx.addnodes.pending_xref, - contnode: docutils.nodes.Element, - ) -> List[Tuple[str, docutils.nodes.Element]]: - results = orig_resolve_any_xref( - self, env, fromdocname, builder, target, node, contnode - ) - for _, refnode in results: - _add_synopsis(self, env, refnode) - return results - - PythonDomain.resolve_any_xref = resolve_any_xref - - -OBJECT_PRIORITY_DEFAULT = 1 -OBJECT_PRIORITY_IMPORTANT = 0 -OBJECT_PRIORITY_UNIMPORTANT = 2 -OBJECT_PRIORITY_EXCLUDE_FROM_SEARCH = -1 - - -def _monkey_patch_python_domain_to_deprioritize_params_in_search(): - """Ensures parameters have OBJECT_PRIORITY_UNIMPORTANT.""" - orig_get_objects = PythonDomain.get_objects - - def get_objects( - self: PythonDomain, - ) -> Iterator[Tuple[str, str, str, str, str, int]]: - for obj in orig_get_objects(self): - if obj[2] != "parameter": - yield obj - else: - yield ( - obj[0], - obj[1], - obj[2], - obj[3], - obj[4], - OBJECT_PRIORITY_UNIMPORTANT, - ) - - PythonDomain.get_objects = get_objects - - -def _add_parameter_links_to_signature( - env: sphinx.environment.BuildEnvironment, - signode: sphinx.addnodes.desc_signature, - symbol: str, -) -> Dict[str, docutils.nodes.Element]: - """Cross-links parameter names in signature to parameter objects. - - Returns: - Map of parameter name to original (not linked) parameter node. - """ - sig_param_nodes: Dict[str, docutils.nodes.Element] = {} - - replacements = [] - node_identifier_key = "sphinx_immaterial_param_name_identifier" - - def add_replacement( - name_node: docutils.nodes.Element, param_node: docutils.nodes.Element - ) -> docutils.nodes.Element: - replacements.append((name_node, param_node)) - name = name_node.astext() - # Mark `name_node` so that it can be identified after the deep copy of its - # ancestor `param_node`. - name_node[node_identifier_key] = True - param_node_copy = param_node.deepcopy() - source, line = docutils.utils.get_source_line(param_node) - param_node_copy.source = source - param_node_copy.line = line - sig_param_nodes[name] = param_node_copy - del name_node[node_identifier_key] - for name_node_copy in param_node_copy.findall(condition=type(name_node)): - if name_node_copy.get(node_identifier_key): - return name_node_copy - raise ValueError("Could not locate name node within parameter") - - for desc_param_node in signode.findall(condition=sphinx.addnodes.desc_parameter): - for sig_param_node in desc_param_node: - if not isinstance(sig_param_node, sphinx.addnodes.desc_sig_name): - continue - new_sig_param_node = add_replacement(sig_param_node, desc_param_node) - new_sig_param_node["classes"].append("sig-name") - break - - for name_node, param_node in replacements: - name = name_node.astext() - refnode = sphinx.addnodes.pending_xref( - "", - name_node.deepcopy(), - refdomain="py", - reftype="param", - reftarget=f"{symbol}.{name}", - refwarn=False, - ) - refnode["implicit_sig_param_ref"] = True - name_node.replace_self(refnode) - - return sig_param_nodes - - -def _add_parameter_documentation_ids( - directive: sphinx.domains.python.PyObject, - env: sphinx.environment.BuildEnvironment, - obj_content: sphinx.addnodes.desc_content, - sig_param_nodes_for_signature: List[Dict[str, docutils.nodes.Element]], - symbols: List[str], - noindex: bool, -) -> None: - - qualify_parameter_ids = "nonodeid" not in directive.options - - param_options = apidoc_formatting.get_object_description_options( - env, "py", "parameter" - ) - - py = cast(sphinx.domains.python.PythonDomain, env.get_domain("py")) - - def cross_link_single_parameter( - param_name: str, param_node: docutils.nodes.term - ) -> None: - # Determine the number of unique declarations of this parameter. - # - # If this single object description has multiple signatures, the same - # parameter name may be declared in more than one of those signatures. - # In the parameter description, we will replace the bare parameter name - # with the list of all of the distinct declarations of the parameter. - # Identical declarations in more than one signature will only be - # included once. - unique_decls: Dict[str, Tuple[int, docutils.nodes.Element]] = {} - for i, sig_param_nodes in enumerate(sig_param_nodes_for_signature): - desc_param_node = sig_param_nodes.get(param_name) - if desc_param_node is None: - continue - desc_param_node = cast(docutils.nodes.Element, desc_param_node) - decl_text = desc_param_node.astext().strip() - unique_decls.setdefault(decl_text, (i, desc_param_node)) - if not unique_decls: - all_params = {} - for sig_param_nodes in sig_param_nodes_for_signature: - all_params.update(sig_param_nodes) - logger.warning( - "Parameter name %r does not match any of the parameters " - "defined in the signature: %r", - param_name, - list(all_params.keys()), - location=param_node, - ) - return - - if not noindex: - synopsis: Optional[str] - generate_synopses = param_options["generate_synopses"] - if generate_synopses is not None: - synopsis = sphinx_utils.summarize_element_text( - cast(docutils.nodes.definition, param_node.parent[-1]), - generate_synopses, - ) - else: - synopsis = None - - unqualified_param_id = f"p-{param_name}" - - param_symbols = set() - - # Set ids of the parameter node. - for symbol_i, _ in unique_decls.values(): - symbol = symbols[symbol_i] - param_symbol = f"{symbol}.{param_name}" - if param_symbol in param_symbols: - continue - param_symbols.add(param_symbol) - - if synopsis: - py.data["synopses"][param_symbol] = synopsis - - if qualify_parameter_ids: - node_id = sphinx.util.nodes.make_id( - env, directive.state.document, "", param_symbol - ) - param_node["ids"].append(node_id) - else: - node_id = unqualified_param_id - - py.note_object(param_symbol, "parameter", node_id, location=param_node) - - if param_options["include_in_toc"]: - toc_title = param_name - param_node["toc_title"] = toc_title - - if not qualify_parameter_ids: - param_node["ids"].append(unqualified_param_id) - - # If a parameter type was specified explicitly, don't replace it from - # the signature. - if not param_node.get("paramtype"): - # Replace the bare parameter name with the unique parameter - # declarations. - del param_node[:] - new_param_nodes = [] - - for i, desc_param_node in unique_decls.values(): - new_param_node = param_node.deepcopy() - if i != 0: - del new_param_node["ids"][:] - source, line = docutils.utils.get_source_line(desc_param_node) - new_children = list(c.deepcopy() for c in desc_param_node.children) - new_param_node.extend(new_children) - for child in new_children: - child.source = source - child.line = line - new_param_nodes.append(new_param_node) - param_node.parent[:1] = new_param_nodes - - # Find all parameter descriptions within the object description body. Make - # sure not to find parameter descriptions within nested object descriptions. - # For example, if this is a class object description, we don't want to find - # parameter descriptions within a nested function object description. - for child in obj_content: - if not isinstance(child, docutils.nodes.field_list): - continue - for field in child: - assert isinstance(field, docutils.nodes.field) - field_body = field[-1] - assert isinstance(field_body, docutils.nodes.field_body) - for field_body_child in field_body.children: - if ( - not isinstance(field_body_child, docutils.nodes.definition_list) - or "api-field" not in field_body_child["classes"] - ): - continue - for definition in field_body_child.children: - if ( - not isinstance(definition, docutils.nodes.definition_list_item) - or len(definition.children) == 0 - ): - continue - term = definition[0] - if not isinstance(term, docutils.nodes.term): - continue - param_name = term.get("paramname") - if not param_name: - continue - cross_link_single_parameter(param_name, term) - - -def _cross_link_parameters( - directive: sphinx.domains.python.PyObject, - app: sphinx.application.Sphinx, - signodes: List[sphinx.addnodes.desc_signature], - content: sphinx.addnodes.desc_content, - symbols: List[str], - noindex: bool, -) -> None: - env = app.env - assert isinstance(env, sphinx.environment.BuildEnvironment) - - # Collect the docutils nodes corresponding to the declarations of the - # parameters in each signature, and turn the parameter names into - # cross-links to the parameter description. - # - # In the parameter descriptions, these declarations will be copied in to - # replace the bare parameter name so that the parameter description shows - # e.g. `x : int = 10` rather than just `x`. - sig_param_nodes_for_signature = [] - for signode, symbol in zip(signodes, symbols): - sig_param_nodes_for_signature.append( - _add_parameter_links_to_signature(env, signode, symbol) - ) - - # Find all parameter descriptions in the object description body, and mark - # them as the target for cross links to that parameter. Also substitute in - # the parameter declaration for the bare parameter name, as described above. - _add_parameter_documentation_ids( - directive=directive, - env=env, - obj_content=content, - sig_param_nodes_for_signature=sig_param_nodes_for_signature, - symbols=symbols, - noindex=noindex, - ) - - -def _monkey_patch_python_domain_to_support_synopses(): - - orig_after_content = PyObject.after_content - - orig_transform_content = PyObject.transform_content - - def transform_content(self: PyObject, contentnode) -> None: - setattr(self, "contentnode", contentnode) - orig_transform_content(self, contentnode) - - PyObject.transform_content = transform_content - - def after_content(self: PyObject) -> None: - orig_after_content(self) - obj_desc = cast( - sphinx.addnodes.desc_content, getattr(self, "contentnode") - ).parent - signodes = obj_desc.children[:-1] - - py = cast(PythonDomain, self.env.get_domain("py")) - - def strip_object_entry_node_id(existing_node_id: str, object_id: str): - obj = py.objects.get(object_id) - if ( - obj is None - or obj.node_id != existing_node_id - or obj.docname != self.env.docname - ): - return - py.objects[object_id] = obj._replace(node_id="") - - nonodeid = "nonodeid" in self.options - canonical_name = self.options.get("canonical") - noindexentry = "noindexentry" in self.options - noindex = "noindex" in self.options - - symbols = [] - for signode in cast(List[docutils.nodes.Element], signodes): - modname = signode["module"] - fullname = signode["fullname"] - symbol = (modname + "." if modname else "") + fullname - symbols.append(symbol) - if nonodeid and signode["ids"]: - orig_node_id = signode["ids"][0] - signode["ids"] = [] - strip_object_entry_node_id(orig_node_id, symbol) - if canonical_name: - strip_object_entry_node_id(orig_node_id, canonical_name) - - if noindexentry: - entries = self.indexnode["entries"] - new_entries = [] - for entry in entries: - new_entry = list(entry) - if new_entry[2] == orig_node_id: - new_entry[2] = "" - new_entries.append(tuple(new_entry)) - self.indexnode["entries"] = new_entries - - if not symbols: - return - if self.objtype in ("class", "exception"): - # Any parameters are actually constructor parameters. To avoid - # symbol name conflicts, assign object names under `__init__`. - function_symbols = [f"{symbol}.__init__" for symbol in symbols] - else: - function_symbols = symbols - - _cross_link_parameters( - directive=self, - app=self.env.app, - signodes=cast(List[sphinx.addnodes.desc_signature], signodes), - content=getattr(self, "contentnode"), - symbols=function_symbols, - noindex=noindex, - ) - if noindex: - return - options = apidoc_formatting.get_object_description_options( - self.env, self.domain, self.objtype - ) - generate_synopses = options["generate_synopses"] - if generate_synopses is None: - return - synopsis = sphinx_utils.summarize_element_text( - getattr(self, "contentnode"), generate_synopses - ) - if not synopsis: - return - for symbol in symbols: - py.data["synopses"][symbol] = synopsis - - PyObject.after_content = after_content - - orig_merge_domaindata = PythonDomain.merge_domaindata - - def merge_domaindata(self, docnames: List[str], otherdata: dict) -> None: - orig_merge_domaindata(self, docnames, otherdata) - self.data["synopses"].update(otherdata["synopses"]) - - PythonDomain.merge_domaindata = merge_domaindata - - def get_object_synopses( - self: PythonDomain, - ) -> Iterator[Tuple[Tuple[str, str], str]]: - synopses = self.data["synopses"] - for refname, synopsis in synopses.items(): - obj = self.objects.get(refname) - if not obj: - continue - yield ((obj.docname, obj.node_id), synopsis) - - PythonDomain.get_object_synopses = get_object_synopses - - -sphinx.domains.python.PythonDomain.object_types["parameter"] = sphinx.domains.ObjType( - "parameter", "param" -) - - -def _maybe_strip_type_annotations( - app: sphinx.application.Sphinx, - domain: str, - objtype: str, - contentnode: sphinx.addnodes.desc_content, -) -> None: - if domain != "py": - return - obj_desc = contentnode.parent - assert isinstance(obj_desc, sphinx.addnodes.desc) - strip_self_type_annotations = app.config.python_strip_self_type_annotations - strip_return_type_annotations = app.config.python_strip_return_type_annotations - for signode in obj_desc[:-1]: - assert isinstance(signode, sphinx.addnodes.desc_signature) - if strip_self_type_annotations: - for param in signode.findall(condition=sphinx.addnodes.desc_parameter): - if param.children[0].astext() == "self": - # Remove any annotations on `self` - del param.children[1:] - break - if strip_return_type_annotations is not None: - fullname = signode.get("fullname") - if fullname is None: - # Python domain failed to parse the signature. Just ignore it. - continue - modname = signode["module"] - if modname: - fullname = modname + "." + fullname - if strip_return_type_annotations.fullmatch(fullname): - # Remove return type. - for node in signode.findall(condition=sphinx.addnodes.desc_returns): - node.parent.remove(node) - - -def _monkey_patch_python_domain_to_support_object_ids(): - for object_class in sphinx.domains.python.PythonDomain.directives.values(): - object_class.option_spec["object-ids"] = json.loads - object_class.option_spec["nonodeid"] = docutils.parsers.rst.directives.flag - - passthrough_options = ("object-ids", "nonodeid") - - orig_add_directive_header = sphinx.ext.autodoc.Documenter.add_directive_header - - def add_directive_header(self: sphinx.ext.autodoc.Documenter, sig: str) -> None: - orig_add_directive_header(self, sig) - for option_name in passthrough_options: - if option_name not in self.options: - continue - value = self.options[option_name] - self.add_line(f" :{option_name}: {value}", self.get_sourcename()) - - sphinx.ext.autodoc.Documenter.add_directive_header = add_directive_header - - orig_handle_signature = sphinx.domains.python.PyObject.handle_signature - - def handle_signature( - self: sphinx.domains.python.PyObject, - sig: str, - signode: sphinx.addnodes.desc_signature, - ) -> Tuple[str, str]: - fullname, prefix = orig_handle_signature(self, sig, signode) - object_ids = self.options.get("object-ids") - if object_ids is not None: - signature_index = getattr(self, "_signature_index", 0) - setattr(self, "_signature_index", signature_index + 1) - modname = signode["module"] - if modname: - modname += "." - else: - modname = "" - if signature_index >= len(object_ids): - logger.warning( - "Not enough object-ids %r specified for %r", - object_ids, - modname + signode["fullname"], - location=self.get_source_info(), - ) - else: - object_id = object_ids[signature_index] - if object_id.startswith(modname): - fullname = object_id[len(modname) :] - signode["fullname"] = fullname - else: - logger.warning( - "object-id %r for %r does not start with module name %r", - object_id, - signode["fullname"], - modname, - location=self.get_source_info(), - ) - return fullname, prefix - - sphinx.domains.python.PyObject.handle_signature = handle_signature - - -def _monkey_patch_python_domain_to_support_titles(): - """Enables support for titles in all Python directive types. - - Normally sphinx only supports titles in `automodule`, but the python_apigen - extension uses titles to group member summaries. - """ - - orig_before_content = PyObject.before_content - - def before_content(self: sphinx.domains.python.PyObject) -> None: - orig_before_content(self) - setattr(self, "_saved_content", self.content) - self.content = docutils.statemachine.StringList() - - orig_transform_content = sphinx.domains.python.PyObject.transform_content - - def transform_content(self: PyObject, contentnode: docutils.nodes.Node) -> None: - sphinx.util.nodes.nested_parse_with_titles( - self.state, - getattr(self, "_saved_content"), - contentnode, - ) - orig_transform_content(self, contentnode) - - sphinx.domains.python.PyObject.before_content = before_content - sphinx.domains.python.PyObject.transform_content = transform_content - - -def _config_inited( - app: sphinx.application.Sphinx, config: sphinx.config.Config -) -> None: - - if ( - config.python_strip_self_type_annotations - or config.python_strip_return_type_annotations - ): - if isinstance(config.python_strip_return_type_annotations, str): - setattr( - config, - "python_strip_return_type_annotations", - re.compile(config.python_strip_return_type_annotations), - ) - app.connect("object-description-transform", _maybe_strip_type_annotations) - - def setup(app: sphinx.application.Sphinx): - _monkey_patch_python_doc_fields() - _monkey_patch_python_parse_annotation() - _monkey_patch_python_parse_arglist() - _monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyFunction) - _monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyMethod) - _monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyProperty) - _monkey_patch_pyattribute_handle_signature(sphinx.domains.python.PyAttribute) - _monkey_patch_pyattribute_handle_signature(sphinx.domains.python.PyVariable) - _monkey_patch_parameterlist_to_support_subscript() - _monkey_patch_napoleon_admonition_classes() - _monkey_patch_python_domain_to_store_func_in_ref_context() - _monkey_patch_python_domain_to_resolve_params() - _monkey_patch_python_domain_to_deprioritize_params_in_search() - _monkey_patch_python_domain_to_add_object_synopses_to_references() - _monkey_patch_python_domain_to_support_synopses() - _monkey_patch_python_domain_to_support_object_ids() - _monkey_patch_python_domain_to_support_titles() - autodoc_property_type.apply_property_documenter_type_annotation_fix() - - sphinx.domains.python.PythonDomain.initial_data["synopses"] = {} # name -> synopsis - - app.add_role_to_domain("py", "param", PyParamXRefRole()) - app.add_config_value( - "python_strip_self_type_annotations", default=True, rebuild="env", types=(bool,) - ) - app.add_config_value( - "python_strip_return_type_annotations", - default=r".*.(__setitem__|__init__)", - rebuild="env", - types=(re.Pattern, type(None)), - ) - app.add_config_value( - "python_strip_property_prefix", default=False, rebuild="env", types=(bool,) - ) - app.connect("config-inited", _config_inited) - return { "parallel_read_safe": True, "parallel_write_safe": True, diff --git a/sphinx_immaterial/apidoc/python/napoleon_admonition_classes.py b/sphinx_immaterial/apidoc/python/napoleon_admonition_classes.py new file mode 100644 index 000000000..9b99113c2 --- /dev/null +++ b/sphinx_immaterial/apidoc/python/napoleon_admonition_classes.py @@ -0,0 +1,20 @@ +from typing import List + +from sphinx.ext.napoleon.docstring import GoogleDocstring + + +def _monkey_patch_napoleon_admonition_classes(): + def _add_admonition_class(method_name: str, class_name: str) -> None: + orig_method = getattr(GoogleDocstring, method_name) + + def wrapper(self: GoogleDocstring, section: str) -> List[str]: + result = orig_method(self, section) + result.insert(1, f" :class: {class_name}") + return result + + setattr(GoogleDocstring, method_name, wrapper) + + _add_admonition_class("_parse_examples_section", "example") + + +_monkey_patch_napoleon_admonition_classes() diff --git a/sphinx_immaterial/apidoc/python/object_ids.py b/sphinx_immaterial/apidoc/python/object_ids.py new file mode 100644 index 000000000..3398bae92 --- /dev/null +++ b/sphinx_immaterial/apidoc/python/object_ids.py @@ -0,0 +1,125 @@ +import json +from typing import Tuple, cast, List + +import docutils.nodes +import sphinx.ext.autodoc +import sphinx.addnodes +import sphinx.domains.python +from sphinx.domains.python import PyObject +from sphinx.domains.python import PythonDomain +import sphinx.util.logging + +logger = sphinx.util.logging.getLogger(__name__) + + +def _monkey_patch_python_domain_to_support_object_ids(): + for object_class in sphinx.domains.python.PythonDomain.directives.values(): + object_class.option_spec["object-ids"] = json.loads + object_class.option_spec["nonodeid"] = docutils.parsers.rst.directives.flag + + passthrough_options = ("object-ids", "nonodeid") + + orig_add_directive_header = sphinx.ext.autodoc.Documenter.add_directive_header + + def add_directive_header(self: sphinx.ext.autodoc.Documenter, sig: str) -> None: + orig_add_directive_header(self, sig) + for option_name in passthrough_options: + if option_name not in self.options: + continue + value = self.options[option_name] + self.add_line(f" :{option_name}: {value}", self.get_sourcename()) + + sphinx.ext.autodoc.Documenter.add_directive_header = add_directive_header + + orig_handle_signature = sphinx.domains.python.PyObject.handle_signature + + def handle_signature( + self: sphinx.domains.python.PyObject, + sig: str, + signode: sphinx.addnodes.desc_signature, + ) -> Tuple[str, str]: + fullname, prefix = orig_handle_signature(self, sig, signode) + object_ids = self.options.get("object-ids") + if object_ids is not None: + signature_index = getattr(self, "_signature_index", 0) + setattr(self, "_signature_index", signature_index + 1) + modname = signode["module"] + if modname: + modname += "." + else: + modname = "" + if signature_index >= len(object_ids): + logger.warning( + "Not enough object-ids %r specified for %r", + object_ids, + modname + signode["fullname"], + location=self.get_source_info(), + ) + else: + object_id = object_ids[signature_index] + if object_id.startswith(modname): + fullname = object_id[len(modname) :] + signode["fullname"] = fullname + else: + logger.warning( + "object-id %r for %r does not start with module name %r", + object_id, + signode["fullname"], + modname, + location=self.get_source_info(), + ) + return fullname, prefix + + sphinx.domains.python.PyObject.handle_signature = handle_signature + + orig_after_content = PyObject.after_content + + def after_content(self: PyObject) -> None: + orig_after_content(self) + obj_desc = cast( + sphinx.addnodes.desc_content, getattr(self, "contentnode") + ).parent + signodes = obj_desc.children[:-1] + + py = cast(PythonDomain, self.env.get_domain("py")) + + def strip_object_entry_node_id(existing_node_id: str, object_id: str): + obj = py.objects.get(object_id) + if ( + obj is None + or obj.node_id != existing_node_id + or obj.docname != self.env.docname + ): + return + py.objects[object_id] = obj._replace(node_id="") + + nonodeid = "nonodeid" in self.options + canonical_name = self.options.get("canonical") + noindexentry = "noindexentry" in self.options + noindex = "noindex" in self.options + + for signode in cast(List[docutils.nodes.Element], signodes): + modname = signode["module"] + fullname = signode["fullname"] + symbol = (modname + "." if modname else "") + fullname + if nonodeid and signode["ids"]: + orig_node_id = signode["ids"][0] + signode["ids"] = [] + strip_object_entry_node_id(orig_node_id, symbol) + if canonical_name: + strip_object_entry_node_id(orig_node_id, canonical_name) + + if noindexentry: + entries = self.indexnode["entries"] + new_entries = [] + for entry in entries: + new_entry = list(entry) + if new_entry[2] == orig_node_id: + new_entry[2] = "" + new_entries.append(tuple(new_entry)) + self.indexnode["entries"] = new_entries + + PyObject.after_content = after_content + + +_monkey_patch_python_domain_to_support_object_ids() diff --git a/sphinx_immaterial/apidoc/python/parameter_objects.py b/sphinx_immaterial/apidoc/python/parameter_objects.py new file mode 100644 index 000000000..a5feb18ba --- /dev/null +++ b/sphinx_immaterial/apidoc/python/parameter_objects.py @@ -0,0 +1,520 @@ +from typing import Optional, cast, List, Dict, Sequence, Tuple, Any, Iterator + +import docutils.nodes +from sphinx.domains.python import PyTypedField +from sphinx.domains.python import PythonDomain +from sphinx.domains.python import PyObject +import sphinx.util.logging + +from . import annotation_style +from .. import apidoc_formatting +from ... import sphinx_utils + + +logger = sphinx.util.logging.getLogger(__name__) + + +def _monkey_patch_python_doc_fields(): + def make_field( + self: PyTypedField, + types: Dict[str, List[docutils.nodes.Node]], + domain: str, + items: Sequence[Tuple[str, str]], + env: Optional[sphinx.environment.BuildEnvironment] = None, + inliner: Optional[docutils.parsers.rst.states.Inliner] = None, + location: Optional[docutils.nodes.Node] = None, + ) -> docutils.nodes.field: + bodynode = docutils.nodes.definition_list() + bodynode["classes"].append("api-field") + bodynode["classes"].append("highlight") + + def handle_item(fieldarg: str, content: Any) -> docutils.nodes.Node: + node = docutils.nodes.definition_list_item() + term_node = docutils.nodes.term() + term_node["paramname"] = fieldarg + term_node += sphinx.addnodes.desc_name(fieldarg, fieldarg) + fieldtype = types.pop(fieldarg, None) + if fieldtype: + term_node += sphinx.addnodes.desc_sig_punctuation("", " : ") + fieldtype_node = sphinx.addnodes.desc_type() + if len(fieldtype) == 1 and isinstance( + fieldtype[0], docutils.nodes.Text + ): + typename = fieldtype[0].astext() + term_node["paramtype"] = typename + fieldtype_node.extend( + annotation_style.ensure_wrapped_in_desc_type( + self.make_xrefs( + cast(str, self.typerolename), + domain, + typename, + docutils.nodes.Text, + env=env, + inliner=inliner, + location=location, + ) + ) + ) + else: + fieldtype_node += fieldtype + term_node += fieldtype_node + node += term_node + def_node = docutils.nodes.definition() + p = docutils.nodes.paragraph() + p += content + def_node += p + node += def_node + return node + + for fieldarg, content in items: + bodynode += handle_item(fieldarg, content) + fieldname = docutils.nodes.field_name("", cast(str, self.label)) + fieldbody = docutils.nodes.field_body("", bodynode) + return docutils.nodes.field("", fieldname, fieldbody) + + PyTypedField.make_field = make_field + + +class PyParamXRefRole(sphinx.domains.python.PyXRefRole): + def process_link( + self, + env: sphinx.environment.BuildEnvironment, + refnode: docutils.nodes.Element, + has_explicit_title: bool, + title: str, + target: str, + ) -> Tuple[str, str]: + refnode["py:func"] = env.ref_context.get("py:func") + return super().process_link(env, refnode, has_explicit_title, title, target) + + +def _monkey_patch_python_domain_to_store_func_in_ref_context(): + orig_before_content = PyObject.before_content + + def before_content(self: PyObject) -> None: + orig_before_content(self) + + if not isinstance( + self, (sphinx.domains.python.PyFunction, sphinx.domains.python.PyMethod) + ): + return + setattr( + self, "_prev_ref_context_py_func", self.env.ref_context.get("py:func", None) + ) + if self.names: + fullname = self.names[-1][0] + else: + fullname = None + + if fullname: + classname = self.env.ref_context.get("py:class") + if classname and fullname.startswith(classname + "."): + fullname = fullname[len(classname) + 1 :] + self.env.ref_context["py:func"] = fullname + else: + self.env.ref_context.pop("py:func", None) + + PyObject.before_content = before_content + + orig_after_content = PyObject.after_content + + def after_content(self: PyObject) -> None: + orig_after_content(self) + if not isinstance( + self, (sphinx.domains.python.PyFunction, sphinx.domains.python.PyMethod) + ): + return + + prev_py_func = getattr(self, "_prev_ref_context_py_func", None) + if prev_py_func is None: + self.env.ref_context.pop("py:func", None) + else: + self.env.ref_context["py:func"] = prev_py_func + + PyObject.after_content = after_content + + +def _monkey_patch_python_domain_to_resolve_params(): + """Adds support to the Python domain for resolving parameter references.""" + + orig_resolve_xref = PythonDomain.resolve_xref + + def resolve_xref( + self: PythonDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + typ: str, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> Optional[docutils.nodes.Element]: + if typ == "param": + func_name = node.get("py:func") + if func_name and "." not in target: + target = "%s.%s" % (func_name, target) + result = orig_resolve_xref( + self, env, fromdocname, builder, typ, target, node, contnode + ) + if ( + typ == "param" + and result is None + and node.get("implicit_sig_param_ref", False) + ): + # Suppress missing reference warnings for automatically-added + # references to parameter descriptions. + raise sphinx.errors.NoUri + return result + + PythonDomain.resolve_xref = resolve_xref + + orig_resolve_any_xref = PythonDomain.resolve_any_xref + + def resolve_any_xref( + self: PythonDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> List[Tuple[str, docutils.nodes.Element]]: + results = orig_resolve_any_xref( + self, env, fromdocname, builder, target, node, contnode + ) + # Don't resolve parameters as any refs, as they introduce too many + # ambiguities. + return [r for r in results if r[0] != "py:param"] + + PythonDomain.resolve_any_xref = resolve_any_xref + + +OBJECT_PRIORITY_DEFAULT = 1 +OBJECT_PRIORITY_IMPORTANT = 0 +OBJECT_PRIORITY_UNIMPORTANT = 2 +OBJECT_PRIORITY_EXCLUDE_FROM_SEARCH = -1 + + +def _monkey_patch_python_domain_to_deprioritize_params_in_search(): + """Ensures parameters have OBJECT_PRIORITY_UNIMPORTANT.""" + orig_get_objects = PythonDomain.get_objects + + def get_objects( + self: PythonDomain, + ) -> Iterator[Tuple[str, str, str, str, str, int]]: + for obj in orig_get_objects(self): + if obj[2] != "parameter": + yield obj + else: + yield ( + obj[0], + obj[1], + obj[2], + obj[3], + obj[4], + OBJECT_PRIORITY_UNIMPORTANT, + ) + + PythonDomain.get_objects = get_objects + + +def _add_parameter_links_to_signature( + env: sphinx.environment.BuildEnvironment, + signode: sphinx.addnodes.desc_signature, + symbol: str, +) -> Dict[str, docutils.nodes.Element]: + """Cross-links parameter names in signature to parameter objects. + + Returns: + Map of parameter name to original (not linked) parameter node. + """ + sig_param_nodes: Dict[str, docutils.nodes.Element] = {} + + replacements = [] + node_identifier_key = "sphinx_immaterial_param_name_identifier" + + def add_replacement( + name_node: docutils.nodes.Element, param_node: docutils.nodes.Element + ) -> docutils.nodes.Element: + replacements.append((name_node, param_node)) + name = name_node.astext() + # Mark `name_node` so that it can be identified after the deep copy of its + # ancestor `param_node`. + name_node[node_identifier_key] = True + param_node_copy = param_node.deepcopy() + source, line = docutils.utils.get_source_line(param_node) + param_node_copy.source = source + param_node_copy.line = line + sig_param_nodes[name] = param_node_copy + del name_node[node_identifier_key] + for name_node_copy in param_node_copy.findall(condition=type(name_node)): + if name_node_copy.get(node_identifier_key): + return name_node_copy + raise ValueError("Could not locate name node within parameter") + + for desc_param_node in signode.findall(condition=sphinx.addnodes.desc_parameter): + for sig_param_node in desc_param_node: + if not isinstance(sig_param_node, sphinx.addnodes.desc_sig_name): + continue + new_sig_param_node = add_replacement(sig_param_node, desc_param_node) + new_sig_param_node["classes"].append("sig-name") + break + + for name_node, param_node in replacements: + name = name_node.astext() + refnode = sphinx.addnodes.pending_xref( + "", + name_node.deepcopy(), + refdomain="py", + reftype="param", + reftarget=f"{symbol}.{name}", + refwarn=False, + ) + refnode["implicit_sig_param_ref"] = True + name_node.replace_self(refnode) + + return sig_param_nodes + + +def _add_parameter_documentation_ids( + directive: sphinx.domains.python.PyObject, + env: sphinx.environment.BuildEnvironment, + obj_content: sphinx.addnodes.desc_content, + sig_param_nodes_for_signature: List[Dict[str, docutils.nodes.Element]], + symbols: List[str], + noindex: bool, +) -> None: + + qualify_parameter_ids = "nonodeid" not in directive.options + + param_options = apidoc_formatting.get_object_description_options( + env, "py", "parameter" + ) + + py = cast(sphinx.domains.python.PythonDomain, env.get_domain("py")) + + def cross_link_single_parameter( + param_name: str, param_node: docutils.nodes.term + ) -> None: + # Determine the number of unique declarations of this parameter. + # + # If this single object description has multiple signatures, the same + # parameter name may be declared in more than one of those signatures. + # In the parameter description, we will replace the bare parameter name + # with the list of all of the distinct declarations of the parameter. + # Identical declarations in more than one signature will only be + # included once. + unique_decls: Dict[str, Tuple[int, docutils.nodes.Element]] = {} + for i, sig_param_nodes in enumerate(sig_param_nodes_for_signature): + desc_param_node = sig_param_nodes.get(param_name) + if desc_param_node is None: + continue + desc_param_node = cast(docutils.nodes.Element, desc_param_node) + decl_text = desc_param_node.astext().strip() + unique_decls.setdefault(decl_text, (i, desc_param_node)) + if not unique_decls: + all_params = {} + for sig_param_nodes in sig_param_nodes_for_signature: + all_params.update(sig_param_nodes) + logger.warning( + "Parameter name %r does not match any of the parameters " + "defined in the signature: %r", + param_name, + list(all_params.keys()), + location=param_node, + ) + return + + if not noindex: + synopsis: Optional[str] + generate_synopses = param_options["generate_synopses"] + if generate_synopses is not None: + synopsis = sphinx_utils.summarize_element_text( + cast(docutils.nodes.definition, param_node.parent[-1]), + generate_synopses, + ) + else: + synopsis = None + + unqualified_param_id = f"p-{param_name}" + + param_symbols = set() + + # Set ids of the parameter node. + for symbol_i, _ in unique_decls.values(): + symbol = symbols[symbol_i] + param_symbol = f"{symbol}.{param_name}" + if param_symbol in param_symbols: + continue + param_symbols.add(param_symbol) + + if synopsis: + py.data["synopses"][param_symbol] = synopsis + + if qualify_parameter_ids: + node_id = sphinx.util.nodes.make_id( + env, directive.state.document, "", param_symbol + ) + param_node["ids"].append(node_id) + else: + node_id = unqualified_param_id + + py.note_object(param_symbol, "parameter", node_id, location=param_node) + + if param_options["include_in_toc"]: + toc_title = param_name + param_node["toc_title"] = toc_title + + if not qualify_parameter_ids: + param_node["ids"].append(unqualified_param_id) + + # If a parameter type was specified explicitly, don't replace it from + # the signature. + if not param_node.get("paramtype"): + # Replace the bare parameter name with the unique parameter + # declarations. + del param_node[:] + new_param_nodes = [] + + for i, desc_param_node in unique_decls.values(): + new_param_node = param_node.deepcopy() + if i != 0: + del new_param_node["ids"][:] + source, line = docutils.utils.get_source_line(desc_param_node) + new_children = list(c.deepcopy() for c in desc_param_node.children) + new_param_node.extend(new_children) + for child in new_children: + child.source = source + child.line = line + new_param_nodes.append(new_param_node) + param_node.parent[:1] = new_param_nodes + + # Find all parameter descriptions within the object description body. Make + # sure not to find parameter descriptions within nested object descriptions. + # For example, if this is a class object description, we don't want to find + # parameter descriptions within a nested function object description. + for child in obj_content: + if not isinstance(child, docutils.nodes.field_list): + continue + for field in child: + assert isinstance(field, docutils.nodes.field) + field_body = field[-1] + assert isinstance(field_body, docutils.nodes.field_body) + for field_body_child in field_body.children: + if ( + not isinstance(field_body_child, docutils.nodes.definition_list) + or "api-field" not in field_body_child["classes"] + ): + continue + for definition in field_body_child.children: + if ( + not isinstance(definition, docutils.nodes.definition_list_item) + or len(definition.children) == 0 + ): + continue + term = definition[0] + if not isinstance(term, docutils.nodes.term): + continue + param_name = term.get("paramname") + if not param_name: + continue + cross_link_single_parameter(param_name, term) + + +def _cross_link_parameters( + directive: sphinx.domains.python.PyObject, + app: sphinx.application.Sphinx, + signodes: List[sphinx.addnodes.desc_signature], + content: sphinx.addnodes.desc_content, + symbols: List[str], + noindex: bool, +) -> None: + env = app.env + assert isinstance(env, sphinx.environment.BuildEnvironment) + + # Collect the docutils nodes corresponding to the declarations of the + # parameters in each signature, and turn the parameter names into + # cross-links to the parameter description. + # + # In the parameter descriptions, these declarations will be copied in to + # replace the bare parameter name so that the parameter description shows + # e.g. `x : int = 10` rather than just `x`. + sig_param_nodes_for_signature = [] + for signode, symbol in zip(signodes, symbols): + sig_param_nodes_for_signature.append( + _add_parameter_links_to_signature(env, signode, symbol) + ) + + # Find all parameter descriptions in the object description body, and mark + # them as the target for cross links to that parameter. Also substitute in + # the parameter declaration for the bare parameter name, as described above. + _add_parameter_documentation_ids( + directive=directive, + env=env, + obj_content=content, + sig_param_nodes_for_signature=sig_param_nodes_for_signature, + symbols=symbols, + noindex=noindex, + ) + + +def _monkey_patch_python_domain_to_cross_link_parameters(): + + orig_after_content = PyObject.after_content + + def after_content(self: PyObject) -> None: + orig_after_content(self) + obj_desc = cast( + sphinx.addnodes.desc_content, getattr(self, "contentnode") + ).parent + signodes = obj_desc.children[:-1] + + py = cast(PythonDomain, self.env.get_domain("py")) + + noindex = "noindex" in self.options + + symbols = [] + for signode in cast(List[docutils.nodes.Element], signodes): + modname = signode["module"] + fullname = signode["fullname"] + symbol = (modname + "." if modname else "") + fullname + symbols.append(symbol) + + if not symbols: + return + if self.objtype in ("class", "exception"): + # Any parameters are actually constructor parameters. To avoid + # symbol name conflicts, assign object names under `__init__`. + function_symbols = [f"{symbol}.__init__" for symbol in symbols] + else: + function_symbols = symbols + + _cross_link_parameters( + directive=self, + app=self.env.app, + signodes=cast(List[sphinx.addnodes.desc_signature], signodes), + content=getattr(self, "contentnode"), + symbols=function_symbols, + noindex=noindex, + ) + + PyObject.after_content = after_content + + +_monkey_patch_python_domain_to_cross_link_parameters() +_monkey_patch_python_doc_fields() +_monkey_patch_python_domain_to_store_func_in_ref_context() +_monkey_patch_python_domain_to_resolve_params() +_monkey_patch_python_domain_to_deprioritize_params_in_search() + +sphinx.domains.python.PythonDomain.object_types["parameter"] = sphinx.domains.ObjType( + "parameter", "param" +) + + +def setup(app: sphinx.application.Sphinx): + app.add_role_to_domain("py", "param", PyParamXRefRole()) + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/sphinx_immaterial/apidoc/python/section_titles.py b/sphinx_immaterial/apidoc/python/section_titles.py new file mode 100644 index 000000000..0fa55aad7 --- /dev/null +++ b/sphinx_immaterial/apidoc/python/section_titles.py @@ -0,0 +1,36 @@ +import docutils.nodes +import docutils.statemachine + +from sphinx.domains.python import PyObject +import sphinx.util.nodes + + +def _monkey_patch_python_domain_to_support_titles(): + """Enables support for titles in all Python directive types. + + Normally sphinx only supports titles in `automodule`, but the python_apigen + extension uses titles to group member summaries. + """ + + orig_before_content = PyObject.before_content + + def before_content(self: PyObject) -> None: + orig_before_content(self) + setattr(self, "_saved_content", self.content) + self.content = docutils.statemachine.StringList() + + orig_transform_content = PyObject.transform_content + + def transform_content(self: PyObject, contentnode: docutils.nodes.Node) -> None: + sphinx.util.nodes.nested_parse_with_titles( + self.state, + getattr(self, "_saved_content"), + contentnode, + ) + orig_transform_content(self, contentnode) + + PyObject.before_content = before_content + PyObject.transform_content = transform_content + + +_monkey_patch_python_domain_to_support_titles() diff --git a/sphinx_immaterial/apidoc/python/strip_property_prefix.py b/sphinx_immaterial/apidoc/python/strip_property_prefix.py new file mode 100644 index 000000000..c41ac0bff --- /dev/null +++ b/sphinx_immaterial/apidoc/python/strip_property_prefix.py @@ -0,0 +1,52 @@ +from typing import Type, Union, List, cast + +import docutils.nodes +import sphinx.domains.python + + +def _monkey_patch_python_get_signature_prefix( + directive_cls: Type[sphinx.domains.python.PyObject], +) -> None: + orig_get_signature_prefix = directive_cls.get_signature_prefix + + def get_signature_prefix(self, sig: str) -> Union[str, List[docutils.nodes.Node]]: + prefix = orig_get_signature_prefix(self, sig) + if not self.env.config.python_strip_property_prefix: + return prefix + if sphinx.version_info >= (4, 3): + prefix = cast(List[docutils.nodes.Node], prefix) + assert isinstance(prefix, list) + for prop_idx, node in enumerate(prefix): + if node == "property": + assert isinstance( + prefix[prop_idx + 1], sphinx.addnodes.desc_sig_space + ) + prefix = list(prefix) + del prefix[prop_idx : prop_idx + 2] + break + return prefix + prefix = cast(str, prefix) # type: ignore + assert isinstance(prefix, str) + parts = prefix.strip().split(" ") + if "property" in parts: + parts.remove("property") + if parts: + return " ".join(parts) + " " + return "" + + directive_cls.get_signature_prefix = get_signature_prefix # type: ignore + + +_monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyFunction) +_monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyMethod) +_monkey_patch_python_get_signature_prefix(sphinx.domains.python.PyProperty) + + +def setup(app: sphinx.application.Sphinx): + app.add_config_value( + "python_strip_property_prefix", default=False, rebuild="env", types=(bool,) + ) + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/sphinx_immaterial/apidoc/python/strip_self_and_return_type_annotations.py b/sphinx_immaterial/apidoc/python/strip_self_and_return_type_annotations.py new file mode 100644 index 000000000..831574c2c --- /dev/null +++ b/sphinx_immaterial/apidoc/python/strip_self_and_return_type_annotations.py @@ -0,0 +1,74 @@ +import re + +import sphinx.addnodes +import sphinx.application +import sphinx.config + + +def _maybe_strip_type_annotations( + app: sphinx.application.Sphinx, + domain: str, + objtype: str, + contentnode: sphinx.addnodes.desc_content, +) -> None: + if domain != "py": + return + obj_desc = contentnode.parent + assert isinstance(obj_desc, sphinx.addnodes.desc) + strip_self_type_annotations = app.config.python_strip_self_type_annotations + strip_return_type_annotations = app.config.python_strip_return_type_annotations + for signode in obj_desc[:-1]: + assert isinstance(signode, sphinx.addnodes.desc_signature) + if strip_self_type_annotations: + for param in signode.findall(condition=sphinx.addnodes.desc_parameter): + if param.children[0].astext() == "self": + # Remove any annotations on `self` + del param.children[1:] + break + if strip_return_type_annotations is not None: + fullname = signode.get("fullname") + if fullname is None: + # Python domain failed to parse the signature. Just ignore it. + continue + modname = signode["module"] + if modname: + fullname = modname + "." + fullname + if strip_return_type_annotations.fullmatch(fullname): + # Remove return type. + for node in signode.findall(condition=sphinx.addnodes.desc_returns): + node.parent.remove(node) + + +def _config_inited( + app: sphinx.application.Sphinx, config: sphinx.config.Config +) -> None: + + if ( + config.python_strip_self_type_annotations + or config.python_strip_return_type_annotations + ): + if isinstance(config.python_strip_return_type_annotations, str): + setattr( + config, + "python_strip_return_type_annotations", + re.compile(config.python_strip_return_type_annotations), + ) + app.connect("object-description-transform", _maybe_strip_type_annotations) + + +def setup(app: sphinx.application.Sphinx): + app.add_config_value( + "python_strip_self_type_annotations", default=True, rebuild="env", types=(bool,) + ) + app.add_config_value( + "python_strip_return_type_annotations", + default=r".*.(__setitem__|__init__)", + rebuild="env", + types=(re.Pattern, type(None)), + ) + app.connect("config-inited", _config_inited) + + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/sphinx_immaterial/apidoc/python/style_default_values_as_code.py b/sphinx_immaterial/apidoc/python/style_default_values_as_code.py new file mode 100644 index 000000000..28a851e8d --- /dev/null +++ b/sphinx_immaterial/apidoc/python/style_default_values_as_code.py @@ -0,0 +1,33 @@ +from typing import Optional + +import docutils.nodes +import sphinx.domains.python +import sphinx.environment +import sphinx.addnodes + + +def _monkey_patch_python_parse_arglist(): + """Ensures default values in signatures are styled as code.""" + + orig_parse_arglist = sphinx.domains.python._parse_arglist + + def parse_arglist( + arglist: str, env: Optional[sphinx.environment.BuildEnvironment] = None + ) -> sphinx.addnodes.desc_parameterlist: + result = orig_parse_arglist(arglist, env) + for node in result.findall(condition=docutils.nodes.inline): + if "default_value" not in node["classes"]: + continue + node.replace_self( + docutils.nodes.literal( + text=node.astext(), + classes=["code", "python", "default_value"], + language="python", + ) + ) + return result + + sphinx.domains.python._parse_arglist = parse_arglist + + +_monkey_patch_python_parse_arglist() diff --git a/sphinx_immaterial/apidoc/python/subscript_methods.py b/sphinx_immaterial/apidoc/python/subscript_methods.py new file mode 100644 index 000000000..5a58e0eb4 --- /dev/null +++ b/sphinx_immaterial/apidoc/python/subscript_methods.py @@ -0,0 +1,12 @@ +from sphinx.addnodes import desc_parameterlist + + +def _monkey_patch_parameterlist_to_support_subscript(): + def astext(self: desc_parameterlist) -> str: + open_paren, close_paren = self.get("parens", ("(", ")")) + return f"{open_paren}{super(desc_parameterlist, self).astext()}{close_paren}" + + desc_parameterlist.astext = astext # type: ignore + + +_monkey_patch_parameterlist_to_support_subscript() diff --git a/sphinx_immaterial/apidoc/python/synopses.py b/sphinx_immaterial/apidoc/python/synopses.py new file mode 100644 index 000000000..0f6b1ec5f --- /dev/null +++ b/sphinx_immaterial/apidoc/python/synopses.py @@ -0,0 +1,150 @@ +from typing import Optional, List, Tuple, cast, Iterator +import docutils.nodes + +import sphinx.addnodes +import sphinx.builders +from sphinx.domains.python import PythonDomain +from sphinx.domains.python import PyObject +import sphinx.environment + +from .. import apidoc_formatting +from ... import sphinx_utils + + +def _monkey_patch_python_domain_to_add_object_synopses_to_references(): + def _add_synopsis( + self: PythonDomain, + env: sphinx.environment.BuildEnvironment, + refnode: docutils.nodes.Element, + ) -> None: + name = refnode.get("reftitle") + obj = self.objects.get(name) + if obj is None: + return + refnode["reftitle"] = apidoc_formatting.format_object_description_tooltip( + env, + apidoc_formatting.get_object_description_options( + env, self.name, obj.objtype + ), + base_title=name, + synopsis=self.data["synopses"].get(name), + ) + + orig_resolve_xref = PythonDomain.resolve_xref + + def resolve_xref( + self: PythonDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + typ: str, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> Optional[docutils.nodes.Element]: + refnode = orig_resolve_xref( + self, env, fromdocname, builder, typ, target, node, contnode + ) + if refnode is not None: + _add_synopsis(self, env, refnode) + return refnode + + PythonDomain.resolve_xref = resolve_xref + + orig_resolve_any_xref = PythonDomain.resolve_any_xref + + def resolve_any_xref( + self: PythonDomain, + env: sphinx.environment.BuildEnvironment, + fromdocname: str, + builder: sphinx.builders.Builder, + target: str, + node: sphinx.addnodes.pending_xref, + contnode: docutils.nodes.Element, + ) -> List[Tuple[str, docutils.nodes.Element]]: + results = orig_resolve_any_xref( + self, env, fromdocname, builder, target, node, contnode + ) + for _, refnode in results: + _add_synopsis(self, env, refnode) + return results + + PythonDomain.resolve_any_xref = resolve_any_xref + + +def _monkey_patch_python_domain_to_support_synopses(): + + orig_after_content = PyObject.after_content + + orig_transform_content = PyObject.transform_content + + def transform_content(self: PyObject, contentnode) -> None: + setattr(self, "contentnode", contentnode) + orig_transform_content(self, contentnode) + + PyObject.transform_content = transform_content + + def after_content(self: PyObject) -> None: + orig_after_content(self) + noindex = "noindex" in self.options + if noindex: + return + + obj_desc = cast( + sphinx.addnodes.desc_content, getattr(self, "contentnode") + ).parent + signodes = obj_desc.children[:-1] + + py = cast(PythonDomain, self.env.get_domain("py")) + + symbols = [] + for signode in cast(List[docutils.nodes.Element], signodes): + modname = signode["module"] + fullname = signode["fullname"] + symbol = (modname + "." if modname else "") + fullname + symbols.append(symbol) + + if not symbols: + return + + options = apidoc_formatting.get_object_description_options( + self.env, self.domain, self.objtype + ) + generate_synopses = options["generate_synopses"] + if generate_synopses is None: + return + synopsis = sphinx_utils.summarize_element_text( + getattr(self, "contentnode"), generate_synopses + ) + if not synopsis: + return + for symbol in symbols: + py.data["synopses"][symbol] = synopsis + + PyObject.after_content = after_content + + orig_merge_domaindata = PythonDomain.merge_domaindata + + def merge_domaindata(self, docnames: List[str], otherdata: dict) -> None: + orig_merge_domaindata(self, docnames, otherdata) + self.data["synopses"].update(otherdata["synopses"]) + + PythonDomain.merge_domaindata = merge_domaindata + + def get_object_synopses( + self: PythonDomain, + ) -> Iterator[Tuple[Tuple[str, str], str]]: + synopses = self.data["synopses"] + for refname, synopsis in synopses.items(): + obj = self.objects.get(refname) + if not obj: + continue + yield ((obj.docname, obj.node_id), synopsis) + + PythonDomain.get_object_synopses = get_object_synopses + + +_monkey_patch_python_domain_to_add_object_synopses_to_references() +_monkey_patch_python_domain_to_support_synopses() + +sphinx.domains.python.PythonDomain.initial_data["synopses"] = {} # name -> synopsis From 7bba915e2988f188c0a27825cd7149af61c6c24b Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 12:42:45 -0700 Subject: [PATCH 3/8] Fix formatting of tests and include in CI mypy/black run --- .github/workflows/build.yml | 4 ++-- tests/python_transform_type_annotations_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 89c378bc8..eb268efbc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,11 +39,11 @@ jobs: - run: npm run check - name: Install Python packaging & linting tools run: python -m pip install -r dev-requirements.txt -r requirements.txt - - run: mypy sphinx_immaterial/**/*.py + - run: mypy sphinx_immaterial/**/*.py tests/**/*.py shell: bash - run: pylint sphinx_immaterial/**/*.py shell: bash - - run: black --diff --color sphinx_immaterial/**/*.py + - run: black --diff --color sphinx_immaterial/**/*.py tests/**/*.py shell: bash - name: Check for dirty working directory run: git diff --exit-code diff --git a/tests/python_transform_type_annotations_test.py b/tests/python_transform_type_annotations_test.py index 9e5a37c91..f211b16c8 100644 --- a/tests/python_transform_type_annotations_test.py +++ b/tests/python_transform_type_annotations_test.py @@ -34,9 +34,9 @@ def test_transform_type_annotations_pep604(theme_make_app): ) for annotation, expected_text in [ - ('Union[int, float]', 'int | float'), - ('Literal[1, 2, None]', '1 | 2 | None'), + ("Union[int, float]", "int | float"), + ("Literal[1, 2, None]", "1 | 2 | None"), ]: - parent = docutils.nodes.TextElement('','') + parent = docutils.nodes.TextElement("", "") parent.extend(sphinx.domains.python._parse_annotation(annotation, app.env)) assert parent.astext() == expected_text From 6aaa8ed9ef33b441ad477f57f553bcc8fc1f2a96 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 12:44:44 -0700 Subject: [PATCH 4/8] Fix regression in URLs in local toc for root document This fixes a regression introduced by 87d87f900901ec56aff1df09572f68497bafcf31. Fixes #138. --- sphinx_immaterial/nav_adapt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sphinx_immaterial/nav_adapt.py b/sphinx_immaterial/nav_adapt.py index c96ef8ce0..bf4a48f84 100644 --- a/sphinx_immaterial/nav_adapt.py +++ b/sphinx_immaterial/nav_adapt.py @@ -685,7 +685,10 @@ def _get_mkdocs_tocs( else: # Every page is a child of the root page. We still want a full TOC # tree, though. - local_toc_node = env.tocs[pagename] + local_toc_node = sphinx.environment.adapters.toctree.TocTree(env).get_toc_for( + pagename, + builder, + ) local_toc = _get_mkdocs_toc(local_toc_node, builder) _add_domain_info_to_toc(app, local_toc, pagename) From 9178868878011016d1eee29b446bb70585ba2000 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 12:45:50 -0700 Subject: [PATCH 5/8] Don't add extra useless tooltip to document refs in TOC Previously, regular links to documents were unintentionally given a tooltip of ` (document)`, which was not helpful. This happened because `doc` targets are listed as Sphinx "objects". --- sphinx_immaterial/nav_adapt.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sphinx_immaterial/nav_adapt.py b/sphinx_immaterial/nav_adapt.py index bf4a48f84..a9ba83484 100644 --- a/sphinx_immaterial/nav_adapt.py +++ b/sphinx_immaterial/nav_adapt.py @@ -383,6 +383,9 @@ def _make_domain_anchor_map( anchor, priority, ) in domain.get_objects(): + if domain_name == "std" and objtype == "doc": + # Don't add an extra tooltip for plain documents. + continue url = docname_to_url.get(docname) if url is None: continue From 931df4374db9be5b9fc0972d005d963c1ab15140 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 15:10:15 -0700 Subject: [PATCH 6/8] Exclude local sections of parent pages from global toc Fixes #137. --- sphinx_immaterial/nav_adapt.py | 64 +++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/sphinx_immaterial/nav_adapt.py b/sphinx_immaterial/nav_adapt.py index a9ba83484..b5aa6f866 100644 --- a/sphinx_immaterial/nav_adapt.py +++ b/sphinx_immaterial/nav_adapt.py @@ -164,10 +164,7 @@ def _strip_fragment(url: str) -> str: """Returns the url with any fragment identifier removed.""" - fragment_start = url.find("#") - if fragment_start == -1: - return url - return url[:fragment_start] + return re.sub("#.*", "", url) def _insert_wbr(text: str) -> str: @@ -503,7 +500,9 @@ def _prune_toc_by_active( TocEntryKey = Tuple[int, ...] -def _build_toc_index(toc: List[MkdocsNavEntry]) -> Dict[str, List[TocEntryKey]]: +def _build_toc_index( + toc: List[MkdocsNavEntry], +) -> Tuple[Dict[str, List[TocEntryKey]], Set[TocEntryKey]]: """Builds a map from URL to list of toc entries. This is used by `_get_global_toc` to efficiently prune the cached TOC for a @@ -511,17 +510,28 @@ def _build_toc_index(toc: List[MkdocsNavEntry]) -> Dict[str, List[TocEntryKey]]: """ url_map: Dict[str, List[TocEntryKey]] = collections.defaultdict(list) - def _traverse(entries: List[MkdocsNavEntry], parent_key: TocEntryKey): + global_toc_keys: Set[TocEntryKey] = set() + + def _traverse( + entries: List[MkdocsNavEntry], + parent_key: TocEntryKey, + parent_url: Optional[str], + ): for i, entry in enumerate(entries): child_key = parent_key + (i,) - url = entry.url - if url is not None and not entry.caption_only: - url = _strip_fragment(url) - url_map[url].append(child_key) - _traverse(entry.children, child_key) + url: Optional[str] = None + if not entry.caption_only: + url = entry.url + if url is not None: + url = _strip_fragment(url) + url_map[url].append(child_key) + _traverse(entry.children, child_key, url) + if url != parent_url or child_key in global_toc_keys: + global_toc_keys.add(child_key) + global_toc_keys.add(parent_key) - _traverse(toc, ()) - return url_map + _traverse(toc, (), None) + return url_map, global_toc_keys _FAKE_DOCNAME = "fakedoc" @@ -567,7 +577,7 @@ def __init__(self, app: sphinx.application.Sphinx): global_toc = _get_mkdocs_toc(global_toc_node, builder) _add_domain_info_to_toc(app, global_toc, _FAKE_DOCNAME) self.entries = global_toc - self.url_map = _build_toc_index(global_toc) + self.url_map, self.global_toc_keys = _build_toc_index(global_toc) def _get_cached_globaltoc_info(app: sphinx.application.Sphinx) -> CachedTocInfo: @@ -600,15 +610,26 @@ def _get_global_toc(app: sphinx.application.Sphinx, pagename: str, collapse: boo fake_page_url, builder.get_target_uri(pagename) ) keys = set(cached_data.url_map[fake_relative_url]) + global_toc_keys = cached_data.global_toc_keys ancestors = _get_ancestor_keys(keys) real_page_url = builder.get_target_uri(pagename) def _make_toc_for_page(key: TocEntryKey, children: List[MkdocsNavEntry]): page_is_current = key in keys - children = list(children) + new_children: List[MkdocsNavEntry] = [] for i, child in enumerate(children): child_key = key + (i,) - child = children[i] = copy.copy(child) + in_ancestors = child_key in ancestors + child_active = in_ancestors + child_current = in_ancestors and child_key in keys + if ( + not child_active + and not page_is_current + and child_key not in global_toc_keys + ): + continue + child = copy.copy(child) + new_children.append(child) if child.url is not None: root_relative_url = urllib.parse.urljoin(fake_page_url, child.url) uri = urllib.parse.urlparse(root_relative_url) @@ -618,21 +639,14 @@ def _make_toc_for_page(key: TocEntryKey, children: List[MkdocsNavEntry]): ) if uri.fragment or child.url == "": child.url += f"#{uri.fragment}" - in_ancestors = child_key in ancestors - child_active = False - child_current = False - if in_ancestors: - child_active = True - if child_key in keys: - child_current = True child.active = child_active and not page_is_current child.current = child_current and not page_is_current child.active_or_section_within_active = child_active - if in_ancestors or child.caption_only: + if in_ancestors or child.caption_only or not collapse: child.children = _make_toc_for_page(child_key, child.children) else: child.children = [] - return children + return new_children return _make_toc_for_page((), cached_data.entries) From 0f12699d3eeecb5127762b74e4c511f19c4bd16d Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 16:31:59 -0700 Subject: [PATCH 7/8] Support `only` directives that impact the TOC This addresses one issue noted in #132 although due to Sphinx bug https://github.com/sphinx-doc/sphinx/issues/9819 it is still not possible to use only directives to selectively include `genindex` and `modindex` in the global TOC. --- sphinx_immaterial/nav_adapt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sphinx_immaterial/nav_adapt.py b/sphinx_immaterial/nav_adapt.py index b5aa6f866..c3b9baef5 100644 --- a/sphinx_immaterial/nav_adapt.py +++ b/sphinx_immaterial/nav_adapt.py @@ -248,6 +248,10 @@ def visit_reference(self, node: docutils.nodes.reference): self._url = node.get("refuri") raise docutils.nodes.SkipChildren + # `only` directives can result in `comment` nodes. + def visit_comment(self, node: docutils.nodes.comment): + raise docutils.nodes.SkipChildren + def visit_compact_paragraph(self, node: docutils.nodes.Element): pass From f889aad208e24888ce339f710559188d6ce3df02 Mon Sep 17 00:00:00 2001 From: Jeremy Maitin-Shepard Date: Tue, 26 Jul 2022 12:47:36 -0700 Subject: [PATCH 8/8] Add unit tests for nav_adapt and Python domain fixes --- dev-requirements.txt | 1 + tests/conftest.py | 29 ++++ tests/nav_adapt_test.py | 131 ++++++++++++++++++ tests/python_object_ids_test.py | 28 ++++ tests/python_synopsis_test.py | 28 ++++ .../False/index.yml | 19 +++ .../True/index.yml | 26 ++++ .../collapse_False-includeonly_False/a.yml | 99 +++++++++++++ .../collapse_False-includeonly_False/b.yml | 43 ++++++ .../collapse_False-includeonly_False/c.yml | 43 ++++++ .../index.yml | 64 +++++++++ .../collapse_False-includeonly_True/a.yml | 117 ++++++++++++++++ .../collapse_False-includeonly_True/b.yml | 43 ++++++ .../collapse_False-includeonly_True/c.yml | 43 ++++++ .../collapse_False-includeonly_True/index.yml | 72 ++++++++++ .../collapse_True-includeonly_False/a.yml | 99 +++++++++++++ .../collapse_True-includeonly_False/b.yml | 43 ++++++ .../collapse_True-includeonly_False/c.yml | 27 ++++ .../collapse_True-includeonly_False/index.yml | 48 +++++++ .../collapse_True-includeonly_True/a.yml | 117 ++++++++++++++++ .../collapse_True-includeonly_True/b.yml | 43 ++++++ .../collapse_True-includeonly_True/c.yml | 27 ++++ .../collapse_True-includeonly_True/index.yml | 56 ++++++++ .../test_python_class_synopsis/reftitle.txt | 1 + 24 files changed, 1247 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/nav_adapt_test.py create mode 100644 tests/python_object_ids_test.py create mode 100644 tests/python_synopsis_test.py create mode 100644 tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/False/index.yml create mode 100644 tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/True/index.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/a.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/b.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/c.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/index.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/a.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/b.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/c.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/index.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/a.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/b.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/c.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/index.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/a.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/b.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/c.yml create mode 100644 tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/index.yml create mode 100644 tests/snapshots/python_synopsis_test/test_python_class_synopsis/reftitle.txt diff --git a/dev-requirements.txt b/dev-requirements.txt index caae34e5d..823617361 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,3 +6,4 @@ types-PyYAML docutils-stubs types-jsonschema pytest +pytest-snapshot diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..bf49d83dd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,29 @@ +import pathlib +from typing import Dict + +import pytest +from sphinx.testing.path import path as SphinxPath + + +pytest_plugins = ("sphinx.testing.fixtures",) + + +@pytest.fixture +def immaterial_make_app(tmp_path: pathlib.Path, make_app): + + conf = """ +extensions = [ + "sphinx_immaterial", +] +html_theme = "sphinx_immaterial" +""" + + def make(files: Dict[str, str], extra_conf: str = "", **kwargs): + (tmp_path / "conf.py").write_text(conf + extra_conf, encoding="utf-8") + for filename, content in files.items(): + (tmp_path / filename).write_text(content, encoding="utf-8") + app = make_app(srcdir=SphinxPath(str(tmp_path)), **kwargs) + app.pdb = True + return app + + yield make diff --git a/tests/nav_adapt_test.py b/tests/nav_adapt_test.py new file mode 100644 index 000000000..8765310c2 --- /dev/null +++ b/tests/nav_adapt_test.py @@ -0,0 +1,131 @@ +import json + +import pytest +import yaml +import sphinx.application + +import sphinx_immaterial.nav_adapt + + +def _default_json_encode(obj): + return obj.__dict__ + + +def get_nav_info(app: sphinx.application.Sphinx, pagename: str) -> str: + ( + global_toc, + local_toc, + integrated_local_toc, + ) = sphinx_immaterial.nav_adapt._get_mkdocs_tocs( + app, pagename, duplicate_local_toc=False, toc_integrate=False + ) + obj = { + "global_toc": global_toc, + "local_toc": local_toc, + "integrated_local_toc": integrated_local_toc, + } + return yaml.safe_dump(json.loads(json.dumps(obj, default=_default_json_encode))) + + +@pytest.mark.parametrize("includeonly", [True, False], ids=lambda x: f"includeonly_{x}") +@pytest.mark.parametrize("collapse", [True, False], ids=lambda x: f"collapse_{x}") +def test_nav(immaterial_make_app, snapshot, collapse, includeonly): + + # Note: `includeonly` tests selectively including sections (due to objects) + # but does not test selectively including `toctree` directives themselves, + # as that is not supported by Sphinx: + # https://github.com/sphinx-doc/sphinx/issues/9819 + + app = immaterial_make_app( + confoverrides=dict(html_theme_options=dict(globaltoc_collapse=collapse)), + tags=["xyz"] if includeonly else [], + files={ + "index.rst": """ +Overall title +============= + +Getting started +--------------- + +.. only:: xyz + + .. py:class:: Foo + +Another section +--------------- + +.. toctree:: + :hidden: + + a + +.. toctree:: + :hidden: + :caption: TOC caption + + c +""", + "a.rst": """ +A page +====== + +Section 1 +--------- + +.. toctree:: + :hidden: + + b + +Section 2 +--------- + +.. only:: xyz + + .. py:class:: Bar + +""", + "b.rst": """ +B page +====== +""", + "c.rst": """ +C page +====== +""", + }, + ) + app.build() + + assert not app._warning.getvalue() + for pagename in ["index", "a", "b", "c"]: + snapshot.assert_match(get_nav_info(app, pagename), f"{pagename}.yml") + + +@pytest.mark.parametrize("include", [True, False]) +def test_include_rubrics_in_toc(immaterial_make_app, snapshot, include): + app = immaterial_make_app( + extra_conf=f""" +object_description_options=[('py:class', dict(include_rubrics_in_toc={include}))] +""", + files={ + "index.rst": """ +Overall title +============= + +.. py:class:: Foo + + Some text. + + .. rubric:: Examples + +""", + }, + ) + app.build() + + assert not app._warning.getvalue() + print(app._status.getvalue()) + + for pagename in ["index"]: + snapshot.assert_match(get_nav_info(app, pagename), f"{pagename}.yml") diff --git a/tests/python_object_ids_test.py b/tests/python_object_ids_test.py new file mode 100644 index 000000000..5b40df4e0 --- /dev/null +++ b/tests/python_object_ids_test.py @@ -0,0 +1,28 @@ +import sphinx.addnodes + + +def test_nonodeid(immaterial_make_app): + + app = immaterial_make_app( + files={ + "index.rst": """ +.. py:class:: Bar + +.. py:class:: Foo + :nonodeid: +""" + }, + ) + + app.build() + + assert not app._warning.getvalue() + + doc = app.env.get_and_resolve_doctree("index", app.builder) + + nodes = list(doc.findall(condition=sphinx.addnodes.desc_signature)) + + assert len(nodes) == 2 + + assert nodes[0]["ids"] == ["Bar"] + assert nodes[1]["ids"] == [] diff --git a/tests/python_synopsis_test.py b/tests/python_synopsis_test.py new file mode 100644 index 000000000..33b879f8d --- /dev/null +++ b/tests/python_synopsis_test.py @@ -0,0 +1,28 @@ +import docutils.nodes + + +def test_python_class_synopsis(immaterial_make_app, snapshot): + + app = immaterial_make_app( + files={ + "index.rst": """ +.. py:class:: Bar + + Synopsis goes here. + + Rest of description goes here. + +:py:obj:`Bar` +""" + }, + ) + + app.build() + + assert not app._warning.getvalue() + + doc = app.env.get_and_resolve_doctree("index", app.builder) + + node = list(doc.findall(condition=docutils.nodes.reference))[-1] + + snapshot.assert_match(node["reftitle"], "reftitle.txt") diff --git a/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/False/index.yml b/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/False/index.yml new file mode 100644 index 000000000..75606ddad --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/False/index.yml @@ -0,0 +1,19 @@ +global_toc: [] +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Foo + caption_only: false + children: [] + current: false + title: "CFoo" + url: '#Foo' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/True/index.yml b/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/True/index.yml new file mode 100644 index 000000000..653535b36 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_include_rubrics_in_toc/True/index.yml @@ -0,0 +1,26 @@ +global_toc: [] +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Foo + caption_only: false + children: + - active: false + aria_label: Examples + caption_only: false + children: [] + current: false + title: Examples + url: '#examples' + current: false + title: "CFoo" + url: '#Foo' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/a.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/a.yml new file mode 100644 index 000000000..45f9d285e --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/a.yml @@ -0,0 +1,99 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + current: true + title: A page + url: '#' +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: [] + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' +local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: [] + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: [] + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/b.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/b.yml new file mode 100644 index 000000000..d17d9a536 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/b.yml @@ -0,0 +1,43 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: B page + caption_only: false + children: [] + current: true + title: B page + url: '#' + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/c.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/c.yml new file mode 100644 index 000000000..5cda7b5bd --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/c.yml @@ -0,0 +1,43 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: true + active_or_section_within_active: true + aria_label: TOC caption + caption_only: true + children: + - active: true + active_or_section_within_active: true + aria_label: C page + caption_only: false + children: [] + current: true + title: C page + url: '#' + current: false + title: TOC caption + url: '#' +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/index.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/index.yml new file mode 100644 index 000000000..27e138366 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_False/index.yml @@ -0,0 +1,64 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Getting started + caption_only: false + children: [] + current: false + title: Getting started + url: '#getting-started' + - active: false + aria_label: Another section + caption_only: false + children: [] + current: false + title: Another section + url: '#another-section' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/a.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/a.yml new file mode 100644 index 000000000..d3ec6ac47 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/a.yml @@ -0,0 +1,117 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + current: true + title: A page + url: '#' +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Bar + caption_only: false + children: [] + current: false + title: CBar + url: '#Bar' + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' +local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: [] + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Bar + caption_only: false + children: [] + current: false + title: CBar + url: '#Bar' + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/b.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/b.yml new file mode 100644 index 000000000..d17d9a536 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/b.yml @@ -0,0 +1,43 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: B page + caption_only: false + children: [] + current: true + title: B page + url: '#' + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/c.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/c.yml new file mode 100644 index 000000000..5cda7b5bd --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/c.yml @@ -0,0 +1,43 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: true + active_or_section_within_active: true + aria_label: TOC caption + caption_only: true + children: + - active: true + active_or_section_within_active: true + aria_label: C page + caption_only: false + children: [] + current: true + title: C page + url: '#' + current: false + title: TOC caption + url: '#' +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/index.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/index.yml new file mode 100644 index 000000000..4f1142114 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_False-includeonly_True/index.yml @@ -0,0 +1,72 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Getting started + caption_only: false + children: + - active: false + aria_label: Foo + caption_only: false + children: [] + current: false + title: CFoo + url: '#Foo' + current: false + title: Getting started + url: '#getting-started' + - active: false + aria_label: Another section + caption_only: false + children: [] + current: false + title: Another section + url: '#another-section' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/a.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/a.yml new file mode 100644 index 000000000..45f9d285e --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/a.yml @@ -0,0 +1,99 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + current: true + title: A page + url: '#' +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: [] + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' +local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: [] + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: [] + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/b.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/b.yml new file mode 100644 index 000000000..d17d9a536 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/b.yml @@ -0,0 +1,43 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: B page + caption_only: false + children: [] + current: true + title: B page + url: '#' + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/c.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/c.yml new file mode 100644 index 000000000..89e57752f --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/c.yml @@ -0,0 +1,27 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: [] + current: false + title: A page + url: a.html +- active: true + active_or_section_within_active: true + aria_label: TOC caption + caption_only: true + children: + - active: true + active_or_section_within_active: true + aria_label: C page + caption_only: false + children: [] + current: true + title: C page + url: '#' + current: false + title: TOC caption + url: '#' +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/index.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/index.yml new file mode 100644 index 000000000..e05527c17 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_False/index.yml @@ -0,0 +1,48 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: [] + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Getting started + caption_only: false + children: [] + current: false + title: Getting started + url: '#getting-started' + - active: false + aria_label: Another section + caption_only: false + children: [] + current: false + title: Another section + url: '#another-section' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/a.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/a.yml new file mode 100644 index 000000000..d3ec6ac47 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/a.yml @@ -0,0 +1,117 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + current: true + title: A page + url: '#' +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: false + active_or_section_within_active: false + aria_label: B page + caption_only: false + children: [] + current: false + title: B page + url: b.html + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Bar + caption_only: false + children: [] + current: false + title: CBar + url: '#Bar' + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' +local_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: [] + current: false + title: Section 1 + url: '#section-1' + - active: false + active_or_section_within_active: true + aria_label: Section 2 + caption_only: false + children: + - active: false + active_or_section_within_active: true + aria_label: Bar + caption_only: false + children: [] + current: false + title: CBar + url: '#Bar' + current: false + title: Section 2 + url: '#section-2' + current: true + title: A page + url: '#' diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/b.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/b.yml new file mode 100644 index 000000000..d17d9a536 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/b.yml @@ -0,0 +1,43 @@ +global_toc: +- active: true + active_or_section_within_active: true + aria_label: A page + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: Section 1 + caption_only: false + children: + - active: true + active_or_section_within_active: true + aria_label: B page + caption_only: false + children: [] + current: true + title: B page + url: '#' + current: false + title: Section 1 + url: a.html#section-1 + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/c.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/c.yml new file mode 100644 index 000000000..89e57752f --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/c.yml @@ -0,0 +1,27 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: [] + current: false + title: A page + url: a.html +- active: true + active_or_section_within_active: true + aria_label: TOC caption + caption_only: true + children: + - active: true + active_or_section_within_active: true + aria_label: C page + caption_only: false + children: [] + current: true + title: C page + url: '#' + current: false + title: TOC caption + url: '#' +integrated_local_toc: [] +local_toc: [] diff --git a/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/index.yml b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/index.yml new file mode 100644 index 000000000..c80bf6129 --- /dev/null +++ b/tests/snapshots/nav_adapt_test/test_nav/collapse_True-includeonly_True/index.yml @@ -0,0 +1,56 @@ +global_toc: +- active: false + active_or_section_within_active: false + aria_label: A page + caption_only: false + children: [] + current: false + title: A page + url: a.html +- active: false + active_or_section_within_active: false + aria_label: TOC caption + caption_only: true + children: + - active: false + active_or_section_within_active: false + aria_label: C page + caption_only: false + children: [] + current: false + title: C page + url: c.html + current: false + title: TOC caption + url: c.html +integrated_local_toc: [] +local_toc: +- active: false + aria_label: Overall title + caption_only: false + children: + - active: false + aria_label: Getting started + caption_only: false + children: + - active: false + aria_label: Foo + caption_only: false + children: [] + current: false + title: CFoo + url: '#Foo' + current: false + title: Getting started + url: '#getting-started' + - active: false + aria_label: Another section + caption_only: false + children: [] + current: false + title: Another section + url: '#another-section' + current: false + title: Overall title + url: '#' diff --git a/tests/snapshots/python_synopsis_test/test_python_class_synopsis/reftitle.txt b/tests/snapshots/python_synopsis_test/test_python_class_synopsis/reftitle.txt new file mode 100644 index 000000000..b154a73f6 --- /dev/null +++ b/tests/snapshots/python_synopsis_test/test_python_class_synopsis/reftitle.txt @@ -0,0 +1 @@ +Bar (Python class) — Synopsis goes here. \ No newline at end of file