pyTooling.Sphinx 0.1.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. pyTooling/Sphinx/Abbreviation.py +452 -0
  2. pyTooling/Sphinx/CodeCoverage.py +878 -0
  3. pyTooling/Sphinx/CodeCoveragePages.py +443 -0
  4. pyTooling/Sphinx/CondensedClass.py +471 -0
  5. pyTooling/Sphinx/CondensedSource.py +441 -0
  6. pyTooling/Sphinx/DependencyTable.py +1437 -0
  7. pyTooling/Sphinx/DocCoverage.py +1120 -0
  8. pyTooling/Sphinx/ExternalLink.py +654 -0
  9. pyTooling/Sphinx/GitHubMarkdown.py +517 -0
  10. pyTooling/Sphinx/HTML.py +614 -0
  11. pyTooling/Sphinx/LaTeX.py +322 -0
  12. pyTooling/Sphinx/Markdown.py +691 -0
  13. pyTooling/Sphinx/Node.py +251 -0
  14. pyTooling/Sphinx/Pages.py +475 -0
  15. pyTooling/Sphinx/Resources/GitHubEmoji.txt +1901 -0
  16. pyTooling/Sphinx/Resources/__init__.py +53 -0
  17. pyTooling/Sphinx/Resources/pyTooling.Sphinx.sty +24 -0
  18. pyTooling/Sphinx/Resources/pyTooling.css +1116 -0
  19. pyTooling/Sphinx/Resources/pyTooling.js +53 -0
  20. pyTooling/Sphinx/Roles.py +168 -0
  21. pyTooling/Sphinx/SchemaGraph.py +228 -0
  22. pyTooling/Sphinx/Shields.py +600 -0
  23. pyTooling/Sphinx/Tree.py +596 -0
  24. pyTooling/Sphinx/UnicodeTable.py +519 -0
  25. pyTooling/Sphinx/Unittest.py +644 -0
  26. pyTooling/Sphinx/UnittestPages.py +325 -0
  27. pyTooling/Sphinx/Workaround.py +73 -0
  28. pyTooling/Sphinx/XMLSchemaGraph.py +194 -0
  29. pyTooling/Sphinx/__init__.py +1125 -0
  30. pyTooling/Sphinx/py.typed +0 -0
  31. pytooling_sphinx-0.1.0rc2.dist-info/METADATA +213 -0
  32. pytooling_sphinx-0.1.0rc2.dist-info/RECORD +36 -0
  33. pytooling_sphinx-0.1.0rc2.dist-info/WHEEL +5 -0
  34. pytooling_sphinx-0.1.0rc2.dist-info/licenses/LICENSE.md +82 -0
  35. pytooling_sphinx-0.1.0rc2.dist-info/licenses/NOTICE.md +24 -0
  36. pytooling_sphinx-0.1.0rc2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,452 @@
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
+ """
32
+ A list of abbreviations, and roles referring to an abbreviation in the form they name.
33
+
34
+ The directive ``abbreviations`` holds the list - a definition list of abbreviations, each with its long form and,
35
+ optionally, a description like a glossary's:
36
+
37
+ .. code-block:: ReST
38
+
39
+ .. abbreviations::
40
+
41
+ FSM
42
+ :long: finite-state machine
43
+ :long-plural: finite-state machines
44
+
45
+ A machine in exactly one of a finite number of states at a time.
46
+
47
+ HDL
48
+ :long: hardware description language
49
+
50
+ A role refers to an abbreviation from any document, in the form it names, as LaTeX' ``acro`` package does:
51
+ ``:acs:`FSM``` writes *FSM*, ``:acl:`FSM``` *finite-state machine*, ``:acf:`FSM``` *finite-state machine (FSM)*,
52
+ and ``:acsp:``, ``:aclp:`` and ``:acfp:`` the plurals. ``:ac:`` and ``:acp:`` are ``:acs:`` and ``:acsp:``: a reader
53
+ doesn't read a page linearly, so there is no first use spelling an abbreviation out. A text written as the role's
54
+ title, ``:acs:`FSM's <FSM>```, replaces the form. Every reference links to the abbreviation in the list; in HTML, a
55
+ short form shows its long form in a box on hover.
56
+
57
+ .. seealso::
58
+
59
+ :ref:`DIR/Abbreviations`
60
+ |rarr| The directive and the roles, with rendered examples.
61
+ :mod:`pyTooling.Sphinx`
62
+ |rarr| The extension this belongs to, and what else it brings.
63
+ """
64
+ from typing import Any, ClassVar, Iterable, Iterator, Literal, NamedTuple, Optional as Nullable
65
+
66
+ from docutils import nodes
67
+ from sphinx.addnodes import pending_xref
68
+ from sphinx.builders import Builder
69
+ from sphinx.domains import Domain, ObjType
70
+ from sphinx.environment import BuildEnvironment
71
+ from sphinx.roles import XRefRole
72
+ from sphinx.util.logging import getLogger
73
+ from sphinx.util.nodes import make_id, make_refnode
74
+
75
+ from pyTooling.Decorators import export, readonly
76
+ from pyTooling.Documentation import splitDocString
77
+ from pyTooling.Sphinx import BaseDirective, SphinxExtensionError, strip, stripAndNormalize
78
+ from pyTooling.Sphinx.Node import Abbreviation
79
+
80
+
81
+ __all__ = ["DOMAIN_NAME", "CONFIG_PREFIX", "ROLES", "FIELDS"]
82
+
83
+ #: Name of the domain collecting the abbreviations of all documents.
84
+ DOMAIN_NAME = "abbreviation"
85
+
86
+ #: Prefix of the configuration values the directive adds to :file:`conf.py`.
87
+ CONFIG_PREFIX = "pyTooling_Abbreviation"
88
+
89
+ #: The roles, mapping a role's name to the form it writes and whether it is the plural.
90
+ ROLES = {
91
+ "ac": ("short", False),
92
+ "acs": ("short", False),
93
+ "acl": ("long", False),
94
+ "acf": ("full", False),
95
+ "acp": ("short", True),
96
+ "acsp": ("short", True),
97
+ "aclp": ("long", True),
98
+ "acfp": ("full", True),
99
+ }
100
+
101
+ #: The fields an abbreviation states its forms in, and whether one is required.
102
+ FIELDS = {
103
+ "long": True,
104
+ "plural": False,
105
+ "long-plural": False,
106
+ }
107
+
108
+
109
+ @export
110
+ class AbbreviationEntry(NamedTuple):
111
+ """An abbreviation of the list: where it is written, and its forms as plain text."""
112
+
113
+ docName: str #: Name of the document holding the list.
114
+ anchor: str #: Identifier of the abbreviation's entry in that document.
115
+ short: str #: The abbreviation itself, e.g. ``FSM``.
116
+ long: str #: The long form, e.g. ``finite-state machine``.
117
+ plural: str #: The plural of the short form, e.g. ``FSMs``.
118
+ longPlural: str #: The plural of the long form, e.g. ``finite-state machines``.
119
+ summary: str = "" #: The description's first paragraph as plain text; empty without a description.
120
+
121
+ def Form(self, form: str, plural: bool) -> str:
122
+ """
123
+ Return one form of the abbreviation as plain text.
124
+
125
+ :param form: ``short``, ``long`` or ``full``, the long form followed by the short one in parentheses.
126
+ :param plural: Whether the plural is asked for.
127
+ :returns: The form's text.
128
+ """
129
+ short = self.plural if plural else self.short
130
+ long = self.longPlural if plural else self.long
131
+ if form == "short":
132
+ return short
133
+ elif form == "long":
134
+ return long
135
+
136
+ return f"{long} ({short})"
137
+
138
+
139
+ @export
140
+ class AbbreviationRole(XRefRole):
141
+ """
142
+ A role referring to an abbreviation in the domain :data:`DOMAIN_NAME`, whatever name it is registered by.
143
+
144
+ Sphinx registers a role outside a domain without one, so the reference would be resolved by nobody; this role puts
145
+ its own domain on the reference, and uses its name as the reference's type.
146
+ """
147
+
148
+ def run(self) -> tuple[list[nodes.Node], list[nodes.system_message]]:
149
+ """
150
+ Create the reference, or only the text if the role is written with ``!``.
151
+
152
+ :returns: The nodes and messages a role returns.
153
+ """
154
+ self.refdomain = DOMAIN_NAME
155
+ self.reftype = self.name.rpartition(":")[2]
156
+ self.classes = ["xref", DOMAIN_NAME, f"{DOMAIN_NAME}-{self.reftype}"]
157
+
158
+ if self.disabled:
159
+ return self.create_non_xref_node()
160
+
161
+ return self.create_xref_node()
162
+
163
+
164
+ @export
165
+ class AbbreviationDomain(Domain):
166
+ """
167
+ The domain collecting the abbreviations of all documents, and resolving the references to them.
168
+
169
+ An abbreviation is registered once in the whole documentation; it gets no entry in the general index.
170
+ """
171
+
172
+ name = DOMAIN_NAME #: Name of the domain.
173
+ label = "Abbreviation" #: Name of the domain shown to a reader.
174
+
175
+ object_types = {"abbreviation": ObjType("abbreviation", *ROLES)} #: The one kind of object, referred to by every role.
176
+ initial_data: dict[str, Any] = {"abbreviations": {}} #: The data of an empty documentation: no abbreviation.
177
+ roles = {
178
+ roleName: AbbreviationRole(innernodeclass=nodes.inline, warn_dangling=True) for roleName in ROLES
179
+ } #: The roles, also as ``:abbreviation:<role>:``.
180
+ dangling_warnings = {
181
+ roleName: "abbreviation '%(target)s' isn't in a list of abbreviations" for roleName in ROLES
182
+ } #: The warning for a reference to an unknown abbreviation, per role.
183
+
184
+ @readonly
185
+ def Abbreviations(self) -> dict[str, AbbreviationEntry]:
186
+ """
187
+ Read-only property to return the abbreviations of all documents, from the domain's data.
188
+
189
+ :returns: The abbreviations, keyed by their short form.
190
+ """
191
+ return self.data["abbreviations"]
192
+
193
+ def Register(self, entry: AbbreviationEntry, location: nodes.Node) -> None:
194
+ """
195
+ Register an abbreviation, or warn that it is registered in a list already.
196
+
197
+ :param entry: The abbreviation.
198
+ :param location: Node the warning points to.
199
+ """
200
+ if (registered := self.Abbreviations.get(entry.short)) is not None:
201
+ getLogger(__name__).warning(
202
+ f"abbreviation '{entry.short}' is in the list of document '{registered.docName}' already",
203
+ location=location
204
+ )
205
+ return
206
+
207
+ self.Abbreviations[entry.short] = entry
208
+
209
+ def clear_doc(self, docname: str) -> None:
210
+ """
211
+ Forget the abbreviations of a document, before it is read again.
212
+
213
+ :param docname: Name of the document.
214
+ """
215
+ for short in [short for short, entry in self.Abbreviations.items() if entry.docName == docname]:
216
+ del self.Abbreviations[short]
217
+
218
+ def merge_domaindata(self, docnames: Iterable[str], otherdata: dict[str, Any]) -> None:
219
+ """
220
+ Take over the abbreviations a parallel reader collected for its documents.
221
+
222
+ :param docnames: Names of the documents the other reader read.
223
+ :param otherdata: The other reader's domain data.
224
+ """
225
+ documents = set(docnames)
226
+ for short, entry in otherdata["abbreviations"].items():
227
+ if entry.docName in documents:
228
+ self.Abbreviations[short] = entry
229
+
230
+ def resolve_xref(
231
+ self,
232
+ env: BuildEnvironment,
233
+ fromdocname: str,
234
+ builder: Builder,
235
+ typ: str,
236
+ target: str,
237
+ node: pending_xref,
238
+ contnode: nodes.Element
239
+ ) -> Nullable[nodes.reference]:
240
+ """
241
+ Resolve a reference to an abbreviation into a link to its entry, showing the form the role names.
242
+
243
+ A short form is an :class:`~pyTooling.Sphinx.Node.Abbreviation`, so HTML explains it on hover; a full form holds
244
+ one. A title written in the role replaces the form's text; the box still explains the abbreviation. With
245
+ ``pyTooling_Abbreviation_ShowSummary`` set, the box also shows the summary of the abbreviation's description.
246
+
247
+ :param env: The build environment.
248
+ :param fromdocname: Name of the document holding the reference.
249
+ :param builder: The builder writing the documents.
250
+ :param typ: The role's name.
251
+ :param target: The abbreviation referred to.
252
+ :param node: The reference.
253
+ :param contnode: The reference's text.
254
+ :returns: The link, or ``None`` if the abbreviation isn't in a list.
255
+ """
256
+ if (entry := self.Abbreviations.get(target)) is None:
257
+ return None
258
+
259
+ form, plural = ROLES[typ]
260
+ short, long = entry.Form("short", plural), entry.Form("long", plural)
261
+ summary = entry.summary if env.config[f"{CONFIG_PREFIX}_ShowSummary"] else ""
262
+ if node.get("refexplicit", False):
263
+ text = contnode.astext()
264
+ content: list[nodes.Node] = [
265
+ Abbreviation(text, text, short=short, long=long, summary=summary) if form == "short" else nodes.Text(text)
266
+ ]
267
+ elif form == "short":
268
+ content = [Abbreviation(short, short, short=short, long=long, summary=summary)]
269
+ elif form == "long":
270
+ content = [nodes.Text(long)]
271
+ else:
272
+ content = [
273
+ nodes.Text(f"{long} ("), Abbreviation(short, short, short=short, long=long, summary=summary), nodes.Text(")")
274
+ ]
275
+
276
+ inline = nodes.inline("", "", *content, classes=contnode["classes"])
277
+ return make_refnode(builder, fromdocname, entry.docName, entry.anchor, inline)
278
+
279
+ def resolve_any_xref(
280
+ self,
281
+ env: BuildEnvironment,
282
+ fromdocname: str,
283
+ builder: Builder,
284
+ target: str,
285
+ node: pending_xref,
286
+ contnode: nodes.Element
287
+ ) -> list[tuple[str, nodes.reference]]:
288
+ """
289
+ Resolve a reference of role ``:any:`` to an abbreviation, which shows its short form.
290
+
291
+ :param env: The build environment.
292
+ :param fromdocname: Name of the document holding the reference.
293
+ :param builder: The builder writing the documents.
294
+ :param target: The text referred to.
295
+ :param node: The reference.
296
+ :param contnode: The reference's text.
297
+ :returns: The role and link, if the text is an abbreviation; otherwise nothing.
298
+ """
299
+ if (reference := self.resolve_xref(env, fromdocname, builder, "acs", target, node, contnode)) is None:
300
+ return []
301
+
302
+ return [(f"{DOMAIN_NAME}:acs", reference)]
303
+
304
+ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
305
+ """
306
+ Name the abbreviations, for the search and for other documentations referring to them.
307
+
308
+ :returns: A tuple per abbreviation: name, display name, type, document, anchor and search priority.
309
+ """
310
+ for short, entry in self.Abbreviations.items():
311
+ yield short, short, "abbreviation", entry.docName, entry.anchor, 1
312
+
313
+
314
+ @export
315
+ class Abbreviations(BaseDirective):
316
+ """
317
+ The ``abbreviations`` directive: a list of abbreviations, which the roles in :data:`ROLES` refer to.
318
+
319
+ The content is a definition list. A term is an abbreviation; its definition starts with a field list stating the
320
+ forms - :data:`FIELDS` - and may continue with a description. The list is shown in alphabetical order, unless
321
+ ``:ordered:`` is ``no``; ``:class:`` puts additional CSS classes on the list.
322
+ """
323
+
324
+ directiveName: str = "abbreviations" #: Name the directive is invoked by.
325
+
326
+ #: The configuration values this directive adds to :file:`conf.py`, as ``name: (default, rebuild, types)``. Each is
327
+ #: registered with :data:`CONFIG_PREFIX` as prefix, e.g. ``pyTooling_Abbreviation_ShowSummary``.
328
+ #:
329
+ #: ``ShowSummary`` adds the summary of an abbreviation's description - its first paragraph - to the box HTML shows
330
+ #: on hover.
331
+ configValues: ClassVar[dict[str, tuple[Any, Literal["env"], Any]]] = {
332
+ "ShowSummary": (False, "env", bool),
333
+ }
334
+
335
+ has_content = True #: A boolean; ``True`` if content is allowed.
336
+ required_arguments = 0 #: Number of required directive arguments.
337
+ optional_arguments = 0 #: Number of optional arguments after the required ones.
338
+ final_argument_whitespace = False #: A boolean; ``True`` if the last argument may contain spaces.
339
+ # docutils declares 'option_spec' on 'Directive' and 'BaseDirective' assigns it, so mypy calls every
340
+ # spelling of this override a conflict with one of them
341
+ #: Mapping of option names to validator functions.
342
+ option_spec: dict[str, Any] = { # type: ignore[misc]
343
+ "class": strip,
344
+ "ordered": stripAndNormalize,
345
+ }
346
+
347
+ def run(self) -> list[nodes.Node]:
348
+ """
349
+ Render the list, and register its abbreviations in the domain.
350
+
351
+ :returns: The list as a definition list, or the message of whatever the content got wrong.
352
+ """
353
+ container = nodes.container()
354
+ self.state.nested_parse(self.content, self.content_offset, container)
355
+
356
+ try:
357
+ ordered = self._ParseBooleanOption("ordered", True)
358
+ except SphinxExtensionError as ex:
359
+ return [self.state.document.reporter.error(str(ex), line=self.lineno)]
360
+
361
+ try:
362
+ definitionList = self._DefinitionList(container)
363
+ entries = [self._ReadItem(item) for item in definitionList.children]
364
+ except SphinxExtensionError as ex:
365
+ return [self.state.document.reporter.error(f"{self.directiveName}: {ex}", line=self.lineno)]
366
+
367
+ if ordered:
368
+ items = sorted(zip(definitionList.children, entries), key=lambda pair: pair[1][0].casefold())
369
+ definitionList[:] = [item for item, _ in items]
370
+ entries = [entry for _, entry in items]
371
+
372
+ domain: AbbreviationDomain = self.env.get_domain(DOMAIN_NAME) # type: ignore[assignment]
373
+ for item, (short, forms, description) in zip(definitionList.children, entries):
374
+ anchor = make_id(self.env, self.state.document, "abbreviation", short)
375
+ term = item[0]
376
+ term["ids"].append(anchor)
377
+ self.state.document.note_explicit_target(term)
378
+
379
+ long, plural, longPlural = forms["long"], forms.get("plural"), forms.get("long-plural")
380
+ entry = AbbreviationEntry(
381
+ docName=self.env.docname,
382
+ anchor=anchor,
383
+ short=short,
384
+ long=long.astext(),
385
+ plural=f"{short}s" if plural is None else plural.astext(),
386
+ longPlural=f"{long.astext()}s" if longPlural is None else longPlural.astext(),
387
+ summary=splitDocString("\n\n".join(node.astext() for node in description), maxSummaryLength=0)[0]
388
+ )
389
+ domain.Register(entry, item)
390
+
391
+ definition = nodes.definition()
392
+ definition += nodes.paragraph("", "", *long.children, classes=["abbreviation-long"])
393
+ definition += description
394
+ item.replace(item[1], definition)
395
+
396
+ definitionList["classes"] += ["pytooling-abbreviations"] + self.options.get("class", "").split()
397
+
398
+ return [definitionList]
399
+
400
+ @staticmethod
401
+ def _DefinitionList(container: nodes.Element) -> nodes.definition_list:
402
+ """
403
+ Return the definition list the content has to consist of.
404
+
405
+ :param container: The parsed content.
406
+ :returns: The definition list.
407
+ :raises SphinxExtensionError: If the content is anything but one definition list.
408
+ """
409
+ if len(container.children) != 1 or not isinstance(container[0], nodes.definition_list):
410
+ raise SphinxExtensionError("The directive's content isn't a definition list of abbreviations.")
411
+
412
+ return container[0]
413
+
414
+ @staticmethod
415
+ def _ReadItem(item: nodes.definition_list_item) -> tuple[str, dict[str, nodes.paragraph], list[nodes.Node]]:
416
+ """
417
+ Read an abbreviation: its term, the fields stating its forms, and the description following them.
418
+
419
+ :param item: An item of the definition list.
420
+ :returns: The abbreviation, its forms keyed by field name, and the description.
421
+ :raises SphinxExtensionError: If the term has a classifier.
422
+ :raises SphinxExtensionError: If the definition doesn't start with a field list.
423
+ :raises SphinxExtensionError: If a field is unknown, given twice, or not a single paragraph.
424
+ :raises SphinxExtensionError: If a required field is missing.
425
+ """
426
+ term, definition = item[0], item[-1]
427
+ short = term.astext().strip()
428
+ if len(item.children) != 2:
429
+ raise SphinxExtensionError(f"Abbreviation '{short}' has a classifier, which isn't supported.")
430
+
431
+ if len(definition.children) == 0 or not isinstance(definition[0], nodes.field_list):
432
+ raise SphinxExtensionError(f"Abbreviation '{short}' doesn't start with a field list, e.g. ':long:'.")
433
+
434
+ forms: dict[str, nodes.paragraph] = {}
435
+ for field in definition[0].children:
436
+ name = field[0].astext().strip()
437
+ body = field[1]
438
+ if name not in FIELDS:
439
+ ex = SphinxExtensionError(f"Abbreviation '{short}' has an unknown field ':{name}:'.")
440
+ ex.add_note(f"Known fields: {', '.join(f':{field}:' for field in FIELDS)}")
441
+ raise ex
442
+ elif name in forms:
443
+ raise SphinxExtensionError(f"Abbreviation '{short}' states field ':{name}:' twice.")
444
+ elif len(body.children) != 1 or not isinstance(body[0], nodes.paragraph):
445
+ raise SphinxExtensionError(f"Field ':{name}:' of abbreviation '{short}' isn't a single paragraph.")
446
+
447
+ forms[name] = body[0]
448
+
449
+ for name in (name for name, required in FIELDS.items() if required and name not in forms):
450
+ raise SphinxExtensionError(f"Abbreviation '{short}' has no field ':{name}:'.")
451
+
452
+ return short, forms, list(definition.children[1:])