Pages.py
Summary
- Lines:
206 of 213 covered (96.7%)
- Branches:
31 of 34 taken (91.2%), 3 lines partially
- Coverage:
96.0%
- Directory:
- Missed lines:
157-160, 167-168, 277, 591, 612
Source
pyTooling/Sphinx/Pages.pycoveredpartialuncoveredexcludednot executable
1# ==================================================================================================================== #
2# _____ _ _ ____ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ / ___| _ __ | |__ (_)_ __ __ __ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | \___ \| '_ \| '_ \| | '_ \\ \/ / #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_ ___) | |_) | | | | | | | |> < #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)____/| .__/|_| |_|_|_| |_/_/\_\ #
7# |_| |___/ |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2026-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Pages generated from a model: a document per object of the model - a testsuite, a testcase, a source file, a VHDL
33entity, ... - built without a source file, in the navigation below the directive showing the model's summary.
34
35:class:`GeneratedPages` holds what doesn't depend on the model or the domain: the document names, the registration and
36generation of the documents, and the tables of contents. :class:`ReportPages` adds the objects of domain ``report``:
37an entry per page, which a role of :data:`~pyTooling.Sphinx.REPORT_ROLES` refers to. Another domain - e.g. a VHDL
38domain - derives from :class:`GeneratedPages`, names the type of its objects as the type argument
39(``GeneratedPages[Library | DesignUnit]``), names its domain in :attr:`GeneratedPages._DOMAIN`, and registers its
40objects in :meth:`GeneratedPages._Registered`; a page may also be written as reStructuredText, see
41:meth:`GeneratedPages._Fill`.
42
43A report declared with the key ``pages`` gets a document per object, named below that key's document name by the
44names of the objects from the report's root down, e.g. ``<pages>/<testsuite>/<testcase>``. A name's characters other
45than letters, digits, ``_``, ``-`` and ``.`` - e.g. the brackets of a parametrized pytest testcase - are replaced by
46``_``, and a name colliding with a sibling's (also when differing only in case) gets the suffix ``-2``, ``-3``, ...
47
48The pages are built as docutils node trees and handed to Sphinx as if they were read from a file: their names are
49registered when the builder is initialized and again before the documents are read -
50:meth:`sphinx.project.Project.discover` forgets them in between -, and the file Sphinx sees as their source is the
51report - or a file a derived class names per page, e.g. a module's source -, so a changed report rebuilds them.
52A page carries the metadata ``:orphan:``. A page with children lists them in a hidden table of contents, as the
53directive showing the report's summary lists the top-level pages, so the pages are in the navigation below that
54directive's document; a page's own sections aren't listed there. Every object with a page is an object of domain
55``report``, referred to by a role of :data:`~pyTooling.Sphinx.REPORT_ROLES`.
56
57.. seealso::
58
59 :mod:`pyTooling.Sphinx.UnittestPages`
60 |rarr| The pages of a unit test report: a page per testsuite and testcase.
61"""
62from __future__ import annotations
63
64from pathlib import Path
65from re import compile as re_compile
66from time import time_ns
67from typing import Any, ClassVar, Generic, Iterable, Optional as Nullable, TypeVar
68
69from docutils import nodes
70from docutils.frontend import get_default_settings
71from docutils.readers.standalone import Reader
72from docutils.transforms.references import DanglingReferences
73from docutils.utils import DependencyList
74from sphinx import addnodes
75from sphinx.addnodes import pending_xref
76from sphinx.application import Sphinx
77from sphinx.domains import Domain
78from sphinx.environment import BuildEnvironment
79from sphinx.transforms import DoctreeReadEvent, SphinxTransformer
80from sphinx.util.docutils import new_document, sphinx_domains
81from sphinx.util.logging import getLogger
82
83from pyTooling.Common import getFullyQualifiedName
84from pyTooling.Decorators import export, readonly
85from pyTooling.MetaClasses import ExtendedType, abstractmethod
86
87from pyTooling.Sphinx import ReportDomain, ReportEntry, ReportExtensionError
88
89
90__all__ = ["UNSAFE_CHARACTERS", "EntityType"]
91
92UNSAFE_CHARACTERS = re_compile(r"[^A-Za-z0-9_.\-]+")
93"""Characters of an object's name, which are replaced in its document name."""
94
95EntityType = TypeVar("EntityType")
96"""The type of the objects a model's pages are generated for."""
97
98
99@export
100class GeneratedPages(Generic[EntityType], metaclass=ExtendedType, slots=True):
101 """
102 Base-class of the pages generated for one model - a report, a design, ...: a document name per object.
103
104 The document names are computed once, when the pages are created, so a summary, the roles and the pages agree on
105 them. A derived class names the type of the model's objects as the type argument :data:`EntityType`, names an
106 object, lists its children, builds its page, and names the domain of the pages' objects in :attr:`_DOMAIN`; it
107 declares its own class variable :attr:`_pages`, the registry of its models' pages.
108 """
109
110 _DOMAIN: ClassVar[str] #: Name of the domain, which the pages' objects belong to.
111 _pages: ClassVar[dict[str, GeneratedPages[Any]]] = {} #: The pages of every model of this kind, by pages ID.
112 _separator: ClassVar[str] = "." #: Separator of the names in an object's qualified name.
113
114 _pagesID: str #: Identifier of the pages, e.g. a report's identifier.
115 _source: Path #: The file Sphinx regards as the pages' source, e.g. the report file.
116 _paths: dict[int, tuple[str, ...]] #: Names from the model's root down to an object, by its ``id()``.
117 _docNames: dict[int, str] #: Document names, by the ``id()`` of their object.
118 _documents: dict[str, tuple[str, EntityType]] #: Role name and object, by document name.
119 _topLevel: list[str] #: Document names of the top-level objects' pages.
120
121 def __init__(self, pagesID: str, prefix: str, source: Path, topLevel: Iterable[tuple[str, EntityType]]) -> None:
122 """
123 Compute the document names of a model's objects.
124
125 An object whose qualified name is already taken by another of the same kind gets no page; this is logged as a
126 warning.
127
128 :param pagesID: Identifier of the pages, e.g. a report's identifier.
129 :param prefix: Document name the pages are generated below.
130 :param source: The file Sphinx regards as the pages' source, e.g. the report file.
131 :param topLevel: The top-level objects as ``(role name, object)`` pairs, in the order of their pages.
132 """
133 self._pagesID = pagesID
134 self._source = source.resolve()
135 self._paths = {}
136 self._docNames = {}
137 self._documents = {}
138
139 qualifiedNames: dict[str, set[str]] = {}
140
141 def addPages(children: Iterable[tuple[str, EntityType]], path: tuple[str, ...], directory: str) -> list[str]:
142 """
143 Nested function adding a document name and entry per object, recursing into the objects' children.
144
145 :param children: The objects of one parent as ``(role name, object)`` pairs.
146 :param path: The parent's path below the report's root.
147 :param directory: The parent's document name, the directory of its children's documents.
148 :returns: The document names of the objects, which got a page.
149 """
150 siblings: set[str] = set()
151 docNames: list[str] = []
152 for roleName, child in children:
153 name = self._Name(child)
154 childPath = (*path, *self._PathElements(name))
155 qualifiedName = self._separator.join(childPath)
156 if qualifiedName in (names := qualifiedNames.setdefault(roleName, set())):
157 getLogger(__name__).warning(
158 f"Pages '{pagesID}': the qualified name '{qualifiedName}' is not unique, so the second one gets no page."
159 )
160 continue
161
162 names.add(qualifiedName)
163
164 baseName = UNSAFE_CHARACTERS.sub("_", name).strip("_.") or "_"
165 fileName, counter = baseName, 1
166 while fileName.lower() in siblings:
167 counter += 1
168 fileName = f"{baseName}-{counter}"
169 siblings.add(fileName.lower())
170
171 docName = f"{directory}/{fileName}"
172 self._paths[id(child)] = childPath
173 self._docNames[id(child)] = docName
174 self._documents[docName] = (roleName, child)
175 docNames.append(docName)
176
177 addPages(self._Children(roleName, child), childPath, docName)
178
179 return docNames
180
181 self._topLevel = addPages(topLevel, (), prefix)
182
183 @staticmethod
184 def CheckPagesConfiguration(configurationName: str, pages: Any) -> str:
185 """
186 Check the document name a report's pages are generated below - the value of a report's key ``pages`` -, and
187 normalize it.
188
189 :param configurationName: Name of the report's configuration, as an error message names it.
190 :param pages: The value of key ``pages``.
191 :returns: The document name without leading and trailing ``/``.
192 :raises ReportExtensionError: If ``pages`` isn't a string.
193 :raises ReportExtensionError: If ``pages`` isn't a relative document name. |br|
194 Use a name like 'unittests/src', separated by '/', without '.' or '..'.
195 """
196 if not isinstance(pages, str):
197 ex = ReportExtensionError(f"{configurationName}.pages: Document name is not a string.")
198 ex.add_note(f"Got type '{getFullyQualifiedName(pages)}'.")
199 raise ex
200
201 parts = pages.strip("/").split("/")
202 if "\\" in pages or any(part in ("", ".", "..") for part in parts):
203 ex = ReportExtensionError(f"{configurationName}.pages: '{pages}' is not a relative document name.")
204 ex.add_note("Use a name like 'unittests/src', separated by '/', without '.' or '..'.")
205 raise ex
206
207 return "/".join(parts)
208
209 @classmethod
210 def HasPages(cls, pagesID: str) -> bool:
211 """
212 Check if a model has pages.
213
214 :param pagesID: Identifier of the pages, e.g. a report's identifier.
215 :returns: ``True``, if the model was declared with pages, e.g. a report with the key ``pages``.
216 """
217 return pagesID in cls._pages
218
219 @classmethod
220 def GetPages(cls, pagesID: str) -> GeneratedPages[Any]:
221 """
222 Return the pages of a model.
223
224 :param pagesID: Identifier of the pages, e.g. a report's identifier.
225 :returns: The model's pages.
226 :raises KeyError: If the model has no pages.
227 """
228 try:
229 return cls._pages[pagesID]
230 except KeyError as ex:
231 raise KeyError(f"'{pagesID}' has no pages.") from ex
232
233 @classmethod
234 def GenerateAll(cls, sphinxApplication: Sphinx, env: BuildEnvironment, docnames: list[str]) -> None:
235 """
236 Call-back for Sphinx' ``env-before-read-docs`` event: generate the pages of every model of this kind.
237
238 :param sphinxApplication: Sphinx application instance.
239 :param env: The build environment.
240 :param docnames: The documents to read, from which the generated pages are removed.
241 """
242 for pages in cls._pages.values():
243 pages.Generate(sphinxApplication)
244
245 docnames[:] = [docname for docname in docnames if docname not in pages._documents]
246
247 @classmethod
248 def HideSource(
249 cls,
250 sphinxApplication: Sphinx,
251 pagename: str,
252 templatename: str,
253 context: dict[str, object],
254 doctree: Nullable[nodes.document]
255 ) -> None:
256 """
257 Call-back for Sphinx' ``html-page-context`` event: a generated page has no source to copy or link.
258
259 Without it, the HTML builder would copy the pages' source - e.g. the report file - per page.
260
261 :param sphinxApplication: Sphinx application instance.
262 :param pagename: Name of the page.
263 :param templatename: Name of the page's template.
264 :param context: The template's context.
265 :param doctree: The page's doctree.
266 """
267 if any(pagename in pages._documents for pages in cls._pages.values()):
268 context["sourcename"] = ""
269
270 @readonly
271 def PagesID(self) -> str:
272 """
273 Read-only property to access the pages' identifier (:attr:`_pagesID`).
274
275 :returns: The identifier of the pages, e.g. a report's identifier.
276 """
277 return self._pagesID
278
279 @readonly
280 def DocNames(self) -> tuple[str, ...]:
281 """
282 Read-only property to return the names of the generated documents.
283
284 :returns: The document names, an object before its children.
285 """
286 return tuple(self._documents)
287
288 def DocName(self, entity: EntityType) -> Nullable[str]:
289 """
290 Return the name of an object's document.
291
292 :param entity: The object.
293 :returns: The document name, or ``None`` if the object has no page.
294 """
295 return self._docNames.get(id(entity), None)
296
297 def Register(self, env: BuildEnvironment) -> None:
298 """
299 Register the document names, each with its source (see :meth:`_Source`).
300
301 :param env: The build environment.
302 """
303 for docName, (_, entity) in self._documents.items():
304 env.project.docnames.add(docName)
305 env.project._docname_to_path[docName] = self._Source(entity)
306
307 def Generate(self, sphinxApplication: Sphinx) -> None:
308 """
309 Build the doctree of every page, have the environment collect it, and store it as if it was read.
310
311 The page's content is added by :meth:`_Fill`, while the page is the environment's current document - so a
312 directive of a page written as reStructuredText registers its objects for that page. The event ``doctree-read``
313 is emitted per page, so the environment's collectors record its title, table of contents and metadata.
314 Afterwards, the page's table of contents keeps the pages it lists, and loses its sections, so the navigation shows
315 the model's objects only. Each object is then registered in its domain, see :meth:`_Registered`.
316
317 :param sphinxApplication: Sphinx application instance.
318 """
319 env = sphinxApplication.env
320 domain = env.domains[self._DOMAIN]
321
322 self.Register(env)
323 for docName, (roleName, entity) in self._documents.items():
324 document = new_document(str(self._Source(entity)))
325 document.settings.env = env
326 document.settings.record_dependencies = DependencyList()
327 document += nodes.docinfo("", nodes.field("", nodes.field_name("", "orphan"), nodes.field_body()))
328
329 env.prepare_settings(docName)
330 try:
331 self._Fill(sphinxApplication, document, docName, roleName, entity)
332 sphinxApplication.events.emit("doctree-read", document)
333 finally:
334 env.prepare_settings("")
335 env.ref_context.clear()
336
337 pageEntry = env.tocs[docName][0]
338 for entries in pageEntry[1:]:
339 entries[:] = [entry for entry in entries if isinstance(entry, addnodes.toctree)]
340 if len(entries) == 0:
341 pageEntry.remove(entries)
342
343 env.all_docs[docName] = time_ns() // 1_000
344 self._Registered(domain, docName, roleName, entity)
345 sphinxApplication.builder.write_doctree(docName, document)
346
347 def TableOfContents(self, docName: str) -> nodes.compound:
348 """
349 Create the hidden table of contents of the model's top-level pages, for the document showing its summary.
350
351 The pages are then below that document in the navigation.
352
353 :param docName: Name of the document showing the report's summary.
354 :returns: The table of contents.
355 """
356 return self._TableOfContents(docName, self._topLevel)
357
358 @staticmethod
359 def _TableOfContents(docName: str, children: list[str]) -> nodes.compound:
360 """
361 Create a hidden table of contents listing pages.
362
363 :param docName: Name of the document holding the table of contents.
364 :param children: Names of the listed pages' documents.
365 :returns: The table of contents.
366 """
367 toctree = addnodes.toctree(
368 parent=docName, entries=[(None, child) for child in children], includefiles=children, maxdepth=1,
369 caption=None, glob=False, hidden=True, includehidden=False, titlesonly=True, numbered=0
370 )
371 return nodes.compound("", toctree, classes=["toctree-wrapper"])
372
373 @staticmethod
374 def _Field(fieldList: nodes.field_list, name: str, *content: nodes.Node) -> None:
375 """
376 Add a field to a page's summary.
377
378 :param fieldList: The summary's field list.
379 :param name: The field's name.
380 :param content: The field's value.
381 """
382 fieldList += nodes.field("", nodes.field_name(name, name), nodes.field_body("", nodes.paragraph("", "", *content)))
383
384 @staticmethod
385 def _Table(columns: list[tuple[str, int]], classes: list[str]) -> tuple[nodes.table, nodes.tbody]:
386 """
387 Create a table with a header row.
388
389 :param columns: One ``(title, width)`` pair per column.
390 :param classes: CSS classes of the table.
391 :returns: The table, and its body the rows are added to.
392 """
393 table = nodes.table("", classes=classes)
394 table += (tableGroup := nodes.tgroup(cols=len(columns)))
395 for _, width in columns:
396 tableGroup += nodes.colspec(colwidth=width)
397
398 tableGroup += (tableHeader := nodes.thead())
399 tableHeader += (headerRow := nodes.row())
400 for columnTitle, _ in columns:
401 headerRow += nodes.entry("", nodes.paragraph(columnTitle, columnTitle))
402
403 tableGroup += (tableBody := nodes.tbody())
404 return table, tableBody
405
406 @abstractmethod
407 def _Name(self, entity: EntityType) -> str:
408 """
409 Return an object's name, the last element of its path.
410
411 :param entity: The object.
412 :returns: The name.
413 """
414
415 def _PathElements(self, name: str) -> tuple[str, ...]:
416 """
417 Return the elements an object's name adds to its path: the name itself, unless a derived class splits it.
418
419 :param name: The object's name.
420 :returns: The path elements.
421 """
422 return (name, )
423
424 def _Source(self, entity: EntityType) -> Path:
425 """
426 Return the file Sphinx regards as the source of an object's page: the pages' source, unless a derived class names
427 another one.
428
429 :param entity: The object.
430 :returns: The page's source file.
431 """
432 return self._source
433
434 @abstractmethod
435 def _Children(self, roleName: str, entity: EntityType) -> Iterable[tuple[str, EntityType]]:
436 """
437 Return an object's children, in the order of their pages.
438
439 :param roleName: The role referring to the object.
440 :param entity: The object.
441 :returns: The children as ``(role name, object)`` pairs.
442 """
443
444 def _Fill(
445 self,
446 sphinxApplication: Sphinx,
447 document: nodes.document,
448 docName: str,
449 roleName: str,
450 entity: EntityType
451 ) -> None:
452 """
453 Add an object's page to its document: the top section built by :meth:`_Page`.
454
455 A derived class writing its pages as reStructuredText overrides it, and parses the text into the document by
456 :meth:`_ParseReST` - the page is the environment's current document then, so its directives register their
457 objects for the page.
458
459 :param sphinxApplication: Sphinx application instance.
460 :param document: The page's document, holding the metadata ``:orphan:``.
461 :param docName: Name of the page's document.
462 :param roleName: The role referring to the object.
463 :param entity: The object.
464 """
465 document += self._Page(docName, roleName, entity)
466
467 @staticmethod
468 def _ParseReST(sphinxApplication: Sphinx, document: nodes.document, text: str) -> None:
469 """
470 Parse reStructuredText into a page's document, as Sphinx reads a source file: with the directives and roles of
471 every domain.
472
473 The document gets the parser's default settings it lacks, and the settings Sphinx reads a source file with. The
474 transforms of Sphinx' reading are applied - e.g. a label is attached to its section -, except the one emitting
475 ``doctree-read``, which :meth:`Generate` emits.
476
477 :param sphinxApplication: Sphinx application instance.
478 :param document: The page's document, which the parsed nodes are appended to.
479 :param text: The reStructuredText.
480 """
481 parser = sphinxApplication.registry.create_source_parser(
482 "restructuredtext", config=sphinxApplication.config, env=sphinxApplication.env
483 )
484
485 settings = document.settings
486 for name, value in vars(get_default_settings(parser)).items():
487 if not hasattr(settings, name):
488 setattr(settings, name, value)
489
490 for name, value in sphinxApplication.env.settings.items():
491 setattr(settings, name, value)
492
493 transformer = SphinxTransformer(document)
494 transformer.set_environment(sphinxApplication.env)
495 transformer.add_transforms(
496 [transform for transform in Reader().get_transforms() if transform is not DanglingReferences]
497 )
498 transformer.add_transforms(parser.get_transforms())
499 transformer.add_transforms(
500 [transform for transform in sphinxApplication.registry.get_transforms() if transform is not DoctreeReadEvent]
501 )
502
503 with sphinx_domains(sphinxApplication.env):
504 parser.parse(text, document)
505 transformer.apply_transforms()
506
507 @abstractmethod
508 def _Page(self, docName: str, roleName: str, entity: EntityType) -> nodes.section:
509 """
510 Build an object's page.
511
512 :param docName: Name of the page's document.
513 :param roleName: The role referring to the object.
514 :param entity: The object.
515 :returns: The page's top section.
516 """
517
518 def _Registered(self, domain: Domain, docName: str, roleName: str, entity: EntityType) -> None:
519 """
520 Register an object's page in the pages' domain, after the page was generated.
521
522 Nothing by default: a page written as reStructuredText registers its objects by its directives.
523
524 :param domain: The domain named by :attr:`_DOMAIN`.
525 :param docName: Name of the page's document.
526 :param roleName: The role referring to the object.
527 :param entity: The object.
528 """
529
530
531@export
532class ReportPages(GeneratedPages[EntityType]):
533 """
534 Base-class of the pages of one report: a document name and an entry of domain ``report`` per object.
535
536 A derived class names an object, writes its title, lists its children, and builds its page; it declares its own
537 class variable :attr:`~GeneratedPages._pages`, the registry of its reports' pages.
538 """
539
540 _DOMAIN: ClassVar[str] = ReportDomain.name #: The pages' objects belong to domain ``report``.
541
542 _entries: dict[int, ReportEntry] #: The entries, by the ``id()`` of their object.
543
544 def __init__(self, reportID: str, prefix: str, reportFile: Path, topLevel: Iterable[tuple[str, EntityType]]) -> None:
545 """
546 Compute the document names and entries of a report's objects.
547
548 An object whose qualified name is already taken by another of the same kind gets no page; this is logged as a
549 warning.
550
551 :param reportID: Identifier of the report.
552 :param prefix: Document name the pages are generated below.
553 :param reportFile: The report file, the pages' source for Sphinx.
554 :param topLevel: The top-level objects as ``(role name, object)`` pairs, in the order of their pages.
555 """
556 super().__init__(reportID, prefix, reportFile, topLevel)
557
558 self._entries = {
559 id(entity): ReportEntry(docName, reportID, self._paths[id(entity)], self._separator, self._Title(entity))
560 for docName, (_, entity) in self._documents.items()
561 }
562
563 @classmethod
564 def CreateReference(cls, roleName: str, entry: ReportEntry, docname: str, text: Nullable[str] = None) -> pending_xref:
565 """
566 Create a reference to an object's page, showing its name or a text.
567
568 :param roleName: The role referring to the object, e.g. ``tc`` for a testcase.
569 :param entry: The object.
570 :param docname: Name of the document holding the reference.
571 :param text: Optional, the text shown instead of the object's name. Default: ``None``.
572 :returns: The reference, resolved by domain ``report``.
573 """
574 reference = pending_xref(
575 "", refdomain=cls._DOMAIN, reftype=roleName, reftarget=f"{entry.reportID}:{entry.QualifiedName}",
576 refexplicit=text is not None, refwarn=True, refdoc=docname
577 )
578 text = entry.Name if text is None else text
579 classes = ["xref", cls._DOMAIN, f"{cls._DOMAIN}-{roleName}"]
580 reference += nodes.inline(text, text, classes=classes)
581
582 return reference
583
584 @readonly
585 def ReportID(self) -> str:
586 """
587 Read-only property to access the report's identifier (:attr:`~GeneratedPages._pagesID`).
588
589 :returns: The identifier of the report.
590 """
591 return self._pagesID
592
593 def Entry(self, entity: EntityType) -> Nullable[ReportEntry]:
594 """
595 Return the entry of an object of this report.
596
597 :param entity: The object.
598 :returns: Its entry, or ``None`` if it has no page.
599 """
600 return self._entries.get(id(entity), None)
601
602 def _Reference(self, roleName: str, entity: EntityType, docName: str) -> nodes.Node:
603 """
604 Create a reference to an object's page, or its name if it has no page.
605
606 :param roleName: The role referring to the object.
607 :param entity: The object.
608 :param docName: Name of the document holding the reference.
609 :returns: The reference, or the name as text.
610 """
611 if (entry := self.Entry(entity)) is None:
612 return nodes.Text(self._Name(entity))
613
614 return self.CreateReference(roleName, entry, docName)
615
616 @abstractmethod
617 def _Title(self, entity: EntityType) -> Nullable[str]:
618 """
619 Return an object's title written for a reader, if it differs from its name.
620
621 :param entity: The object.
622 :returns: The title, or ``None``.
623 """
624
625 def _Registered(self, domain: Domain, docName: str, roleName: str, entity: EntityType) -> None:
626 """
627 Register an object's entry in domain ``report``, after its page was generated.
628
629 :param domain: Domain ``report``.
630 :param docName: Name of the page's document.
631 :param roleName: The role referring to the object, e.g. ``tc`` for a testcase.
632 :param entity: The object.
633 """
634 domain.AddEntry(roleName, self._entries[id(entity)]) # type: ignore[attr-defined]