Coverage for sphinx_reports / CodeCoverage.py: 18%
372 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-16 00:05 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-16 00:05 +0000
1# ==================================================================================================================== #
2# _ _ _ #
3# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ #
4# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| #
5# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ #
6# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ #
7# |_| |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2023-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**Report code coverage as Sphinx documentation page(s).**
33"""
34from pathlib import Path
35from typing import Dict, Tuple, Any, List, Mapping, Generator, TypedDict, Union, Optional as Nullable, ClassVar
37from docutils import nodes
38from docutils.parsers.rst.directives import flag
39from sphinx.addnodes import toctree
40from sphinx.application import Sphinx
41from sphinx.config import Config
42from sphinx.directives.code import LiteralIncludeReader
43from sphinx.util.docutils import new_document
44from pyTooling.Decorators import export
46from sphinx_reports.Common import ReportExtensionError, LegendStyle
47from sphinx_reports.Sphinx import strip, stripAndNormalize, BaseDirective
48from sphinx_reports.Node import Landscape
49from sphinx_reports.DataModel.CodeCoverage import PackageCoverage, Coverage, ModuleCoverage
50from sphinx_reports.Adapter.Coverage import Analyzer
53class package_DictType(TypedDict):
54 name: str
55 json_report: Path
56 fail_below: int
57 levels: Union[str, Dict[Union[int, str], Dict[str, str]]]
60@export
61class CodeCoverageBase(BaseDirective):
63 option_spec = {
64 "class": strip,
65 "reportid": stripAndNormalize
66 }
68 defaultCoverageDefinitions = {
69 "default": {
70 10: {"class": "report-cov-below10", "desc": "almost unused"},
71 20: {"class": "report-cov-below20", "desc": "almost unused"},
72 30: {"class": "report-cov-below30", "desc": "almost unused"},
73 40: {"class": "report-cov-below40", "desc": "poorly used"},
74 50: {"class": "report-cov-below50", "desc": "poorly used"},
75 60: {"class": "report-cov-below60", "desc": "somehow used"},
76 70: {"class": "report-cov-below70", "desc": "somehow used"},
77 80: {"class": "report-cov-below80", "desc": "somehow used"},
78 85: {"class": "report-cov-below85", "desc": "well used"},
79 90: {"class": "report-cov-below90", "desc": "well used"},
80 95: {"class": "report-cov-below95", "desc": "well used"},
81 100: {"class": "report-cov-below100", "desc": "excellently used"},
82 "error": {"class": "report-cov-error", "desc": "internal error"},
83 }
84 }
86 configPrefix: str = "codecov"
87 configValues: Dict[str, Tuple[Any, str, Any]] = {
88 f"{configPrefix}_packages": ({}, "env", Dict),
89 f"{configPrefix}_levels": (defaultCoverageDefinitions, "env", Dict),
90 } #: A dictionary of all configuration values used by code coverage directives.
92 _coverageLevelDefinitions: ClassVar[Dict[str, Dict[Union[int, str], Dict[str, str]]]] = {}
93 _packageConfigurations: ClassVar[Dict[str, package_DictType]] = {}
95 _cssClasses: List[str]
96 _reportID: str
97 _levels: Dict[Union[int, str], Dict[str, str]]
99 def _CheckOptions(self) -> None:
100 """
101 Parse all directive options or use default values.
102 """
103 cssClasses = self._ParseStringOption("class", "", r"(\w+)?( +\w+)*")
105 self._reportID = self._ParseStringOption("reportid")
106 self._cssClasses = [] if cssClasses == "" else cssClasses.split(" ")
108 @classmethod
109 def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None:
110 """
111 Check configuration fields and load necessary values.
113 :param sphinxApplication: Sphinx application instance.
114 :param sphinxConfiguration: Sphinx configuration instance.
115 """
116 cls._CheckLevelsConfiguration(sphinxConfiguration)
117 cls._CheckPackagesConfiguration(sphinxConfiguration)
119 @classmethod
120 def ReadReports(cls, sphinxApplication: Sphinx) -> None:
121 """
122 Read code coverage report files.
124 :param sphinxApplication: Sphinx application instance.
125 """
126 print(f"[REPORT] Reading code coverage reports ...")
128 @classmethod
129 def _CheckLevelsConfiguration(cls, sphinxConfiguration: Config) -> None:
130 from sphinx_reports import ReportDomain
132 variableName = f"{ReportDomain.name}_{cls.configPrefix}_levels"
134 try:
135 coverageLevelDefinitions: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_levels"]
136 except (KeyError, AttributeError) as ex:
137 raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex
139 if "default" not in coverageLevelDefinitions:
140 cls._coverageLevelDefinitions["default"] = cls.defaultCoverageDefinitions["default"]
142 for key, coverageLevelDefinition in coverageLevelDefinitions.items():
143 configurationName = f"conf.py: {variableName}:[{key}]"
145 if 100 not in coverageLevelDefinition:
146 raise ReportExtensionError(f"{configurationName}[100]: Configuration is missing.")
147 elif "error" not in coverageLevelDefinition:
148 raise ReportExtensionError(f"{configurationName}[error]: Configuration is missing.")
150 cls._coverageLevelDefinitions[key] = {}
152 for level, levelConfig in coverageLevelDefinition.items():
153 try:
154 if isinstance(level, str):
155 if level != "error":
156 raise ReportExtensionError(f"{configurationName}[{level}]: Level is a keyword, but not 'error'.")
157 elif not (0.0 <= int(level) <= 100.0):
158 raise ReportExtensionError(f"{configurationName}[{level}]: Level is out of range 0..100.")
159 except ValueError as ex:
160 raise ReportExtensionError(f"{configurationName}[{level}]: Level is not a keyword or an integer in range 0..100.") from ex
162 try:
163 cssClass = levelConfig["class"]
164 except KeyError as ex:
165 raise ReportExtensionError(f"{configurationName}[{level}].class: CSS class is missing.") from ex
167 try:
168 description = levelConfig["desc"]
169 except KeyError as ex:
170 raise ReportExtensionError(f"{configurationName}[{level}].desc: Description is missing.") from ex
172 cls._coverageLevelDefinitions[key][level] = {
173 "class": cssClass,
174 "desc": description
175 }
177 @classmethod
178 def _CheckPackagesConfiguration(cls, sphinxConfiguration: Config) -> None:
179 from sphinx_reports import ReportDomain
181 variableName = f"{ReportDomain.name}_{cls.configPrefix}_packages"
183 try:
184 allPackages: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_packages"]
185 except (KeyError, AttributeError) as ex:
186 raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex
188 # try:
189 # packageConfiguration = allPackages[self._reportID]
190 # except KeyError as ex:
191 # raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{cls.configPrefix}_packages: No configuration found for '{self._reportID}'.") from ex
193 for reportID, packageConfiguration in allPackages.items():
194 configurationName = f"conf.py: {variableName}:[{reportID}]"
196 try:
197 packageName = packageConfiguration["name"]
198 except KeyError as ex:
199 raise ReportExtensionError(f"{configurationName}.name: Configuration is missing.") from ex
201 try:
202 jsonReport = Path(packageConfiguration["json_report"])
203 except KeyError as ex:
204 raise ReportExtensionError(f"{configurationName}.json_report: Configuration is missing.") from ex
206 if not jsonReport.exists():
207 raise ReportExtensionError(f"{configurationName}.json_report: Coverage report file '{jsonReport}' doesn't exist.") from FileNotFoundError(jsonReport)
209 try:
210 failBelow = int(packageConfiguration["fail_below"]) / 100
211 except KeyError as ex:
212 raise ReportExtensionError(f"{configurationName}.fail_below: Configuration is missing.") from ex
213 except ValueError as ex:
214 raise ReportExtensionError(f"{configurationName}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex
216 if not (0.0 <= failBelow <= 100.0):
217 raise ReportExtensionError(f"{configurationName}.fail_below: Is out of range 0..100.")
219 try:
220 levels = packageConfiguration["levels"]
221 except KeyError as ex:
222 raise ReportExtensionError(f"{configurationName}.levels: Configuration is missing.") from ex
224 if isinstance(levels, str):
225 try:
226 levelDefinition = cls._coverageLevelDefinitions[levels]
227 except KeyError as ex:
228 raise ReportExtensionError(
229 f"{configurationName}.levels: Referenced coverage levels '{levels}' are not defined in conf.py variable '{variableName}'.") from ex
230 elif isinstance(levels, dict):
231 if 100 not in packageConfiguration["levels"]:
232 raise ReportExtensionError(f"{configurationName}.levels[100]: Configuration is missing.")
233 elif "error" not in packageConfiguration["levels"]:
234 raise ReportExtensionError(f"{configurationName}.levels[error]: Configuration is missing.")
236 levelDefinition = {}
237 for x, y in packageConfiguration["levels"].items():
238 pass
239 else:
240 raise ReportExtensionError(f"")
242 cls._packageConfigurations[reportID] = {
243 "name": packageName,
244 "json_report": jsonReport,
245 "fail_below": failBelow,
246 "levels": levelDefinition
247 }
249 def _ConvertToColor(self, currentLevel: float, configKey: str) -> str:
250 if currentLevel < 0.0:
251 return self._levels["error"][configKey]
253 for levelLimit, levelConfig in self._levels.items():
254 if isinstance(levelLimit, int) and (currentLevel * 100) < levelLimit:
255 return levelConfig[configKey]
257 return self._levels[100][configKey]
260@export
261class CodeCoverage(CodeCoverageBase):
262 """
263 This directive will be replaced by a table representing code coverage.
264 """
265 directiveName: str = "code-coverage"
267 has_content = False
268 required_arguments = 0
269 optional_arguments = CodeCoverageBase.optional_arguments + 1
271 option_spec = CodeCoverageBase.option_spec | {
272 "no-branch-coverage": flag
273 }
275 _noBranchCoverage: bool
276 _packageName: str
277 _jsonReport: Path
278 _failBelow: float
279 _coverage: PackageCoverage
281 def _CheckOptions(self) -> None:
282 """
283 Parse all directive options or use default values.
284 """
285 super()._CheckOptions()
287 self._noBranchCoverage = "no-branch-coverage" in self.options
289 try:
290 packageConfiguration = self._packageConfigurations[self._reportID]
291 except KeyError as ex:
292 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
294 self._packageName = packageConfiguration["name"]
295 self._jsonReport = packageConfiguration["json_report"]
296 self._failBelow = packageConfiguration["fail_below"]
297 self._levels = packageConfiguration["levels"]
299 def _GenerateCoverageTable(self) -> nodes.table:
300 cssClasses = ["report-codecov-table", f"report-codecov-{self._reportID}"]
301 cssClasses.extend(self._cssClasses)
303 # Create a table and table header with 10 columns
304 columns = [
305 ("Package", [(" Module", 5)], None),
306 ("Statments", [("Total", 1), ("Excluded", 1), ("Covered", 1), ("Missing", 1), ("Coverage", 1)], None),
307 ("Branches" , [("Total", 1), ("Covered", 1), ("Partial", 1), ("Missing", 1), ("Coverage", 1)], None),
308 # ("Coverage", [("in %", 1)], None)
309 ]
311 if self._noBranchCoverage:
312 columns.pop(2)
314 tableGroup = self._CreateDoubleRowTableHeader(
315 identifier=self._reportID,
316 columns=columns,
317 classes=cssClasses
318 )
319 tableBody = nodes.tbody()
320 tableGroup += tableBody
322 self.renderlevel(tableBody, self._coverage)
324 # Add a summary row
325 tableRow = nodes.row("", classes=[
326 "report-summary",
327 self._ConvertToColor(self._coverage.AggregatedStatementCoverage, "class")
328 ])
329 tableBody += tableRow
331 tableRow += nodes.entry("", nodes.Text(f"Overall ({self._coverage.FileCount} files):"))
332 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedTotalStatements}"))
333 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedExcludedStatements}"))
334 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedCoveredStatements}"))
335 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedMissingStatements}"))
336 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedStatementCoverage:.1%}"))
337 if not self._noBranchCoverage:
338 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedTotalBranches}"))
339 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedCoveredBranches}"))
340 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedPartialBranches}"))
341 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedMissingBranches}"))
342 tableRow += nodes.entry("", nodes.Text(f"{self._coverage.AggregatedBranchCoverage:.1%}"))
344 return tableGroup.parent
346 def sortedValues(self, d: Mapping[str, Coverage]) -> Generator[Coverage, None, None]:
347 for key in sorted(d.keys()):
348 yield d[key]
350 def renderlevel(self, tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: int = 0) -> None:
351 coverage = 1 if packageCoverage.Coverage < 0.0 else packageCoverage.Coverage
352 tableRow = nodes.row("", classes=[
353 "report-package",
354 self._ConvertToColor(coverage, "class")
355 ])
356 tableBody += tableRow
358 tableRow += nodes.entry("", nodes.Text(f"{' ' * level}📦{packageCoverage.Name}"))
359 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.TotalStatements}"))
360 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.ExcludedStatements}"))
361 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.CoveredStatements}"))
362 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.MissingStatements}"))
363 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.StatementCoverage:.1%}"))
364 if not self._noBranchCoverage:
365 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.TotalBranches}"))
366 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.CoveredBranches}"))
367 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.PartialBranches}"))
368 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.MissingBranches}"))
369 tableRow += nodes.entry("", nodes.Text(f"{packageCoverage.BranchCoverage:.1%}"))
371 for package in self.sortedValues(packageCoverage._packages):
372 self.renderlevel(tableBody, package, level + 1)
374 for module in self.sortedValues(packageCoverage._modules):
375 tableRow = nodes.row("", classes=[
376 "report-module",
377 self._ConvertToColor(module.Coverage, "class")
378 ])
379 tableBody += tableRow
381 tableRow += nodes.entry("", nodes.Text(f"{' ' * (level + 1)} ⚙️{module.Name}"))
382 tableRow += nodes.entry("", nodes.Text(f"{module.TotalStatements}"))
383 tableRow += nodes.entry("", nodes.Text(f"{module.ExcludedStatements}"))
384 tableRow += nodes.entry("", nodes.Text(f"{module.CoveredStatements}"))
385 tableRow += nodes.entry("", nodes.Text(f"{module.MissingStatements}"))
386 tableRow += nodes.entry("", nodes.Text(f"{module.StatementCoverage:.1%}"))
387 if not self._noBranchCoverage:
388 tableRow += nodes.entry("", nodes.Text(f"{module.TotalBranches}"))
389 tableRow += nodes.entry("", nodes.Text(f"{module.CoveredBranches}"))
390 tableRow += nodes.entry("", nodes.Text(f"{module.PartialBranches}"))
391 tableRow += nodes.entry("", nodes.Text(f"{module.MissingBranches}"))
392 tableRow += nodes.entry("", nodes.Text(f"{module.BranchCoverage:.1%}"))
394 def _CreatePages(self) -> None:
395 def handlePackage(package: PackageCoverage) -> None:
396 for pack in package._packages.values():
397 if handlePackage(pack):
398 return
400 for module in package._modules.values():
401 if handleModule(module):
402 return
404 def handleModule(module: ModuleCoverage) -> None:
405 doc = new_document("dummy")
407 rootSection = nodes.section(ids=["foo"])
408 doc += rootSection
410 title = nodes.title(text=f"{module.Name}")
411 rootSection += title
412 rootSection += nodes.paragraph(text="some text")
414 docname = f"coverage/{module.Name}"
415 self.env.titles[docname] = title
416 self.env.longtitles[docname] = title
418 return
420 handlePackage(self._coverage)
422 def run(self) -> List[nodes.Node]:
423 container = Landscape()
425 try:
426 self._CheckOptions()
427 except ReportExtensionError as ex:
428 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
429 return self._internalError(container, __name__, message, ex)
431 # Assemble a list of Python source files
432 analyzer = Analyzer(self._packageName, self._jsonReport)
433 self._coverage = analyzer.Convert()
434 # self._coverage.Aggregate()
436 self._CreatePages()
438 container += self._GenerateCoverageTable()
440 def foo():
441 docName = self.env.docname
442 docParent = docName[:docName.rindex("/")]
444 subnode = toctree()
445 subnode['parent'] = docName
447 # (title, ref) pairs, where ref may be a document, or an external link,
448 # and title may be None if the document's title is to be used
449 subnode['entries'] = []
450 subnode['includefiles'] = []
451 subnode['maxdepth'] = -1 # self.options.get('maxdepth', -1)
452 subnode['caption'] = None # self.options.get('caption')
453 subnode['glob'] = None # 'glob' in self.options
454 subnode['hidden'] = True # 'hidden' in self.options
455 subnode['includehidden'] = False # 'includehidden' in self.options
456 subnode['numbered'] = 0 # self.options.get('numbered', 0)
457 subnode['titlesonly'] = False # 'titlesonly' in self.options
458 self.set_source_info(subnode)
460 wrappernode = nodes.compound(classes=['toctree-wrapper'])
461 wrappernode.append(subnode)
462 self.add_name(wrappernode)
464 for entry in (
465 "sphinx_reports",
466 "sphinx_reports.Adapter",
467 "sphinx_reports.Adapter.Coverage",
468 "sphinx_reports.Adapter.DocStrCoverage",
469 "sphinx_reports.Adapter.JUnit",
470 "sphinx_reports.DataModel",
471 "sphinx_reports.DataModel.CodeCoverage",
472 "sphinx_reports.DataModel.DocumentationCoverage",
473 "sphinx_reports.DataModel.Unittest",
474 "sphinx_reports.static",
475 "sphinx_reports.CodeCoverage",
476 "sphinx_reports.Common",
477 "sphinx_reports.DocCoverage",
478 "sphinx_reports.Sphinx",
479 "sphinx_reports.Unittest",
480 ):
481 moduleDocumentName = f"{docParent}/{entry}"
482 moduleDocumentTitle = entry
484 subnode["entries"].append((moduleDocumentTitle, moduleDocumentName))
485 subnode["includefiles"].append(moduleDocumentName)
487 return [container] #, wrappernode]
490@export
491class CodeCoverageLegend(CodeCoverageBase):
492 """
493 This directive will be replaced by a legend table representing coverage levels.
494 """
495 has_content = False
496 required_arguments = 0
497 optional_arguments = CodeCoverageBase.optional_arguments + 1
499 option_spec = CodeCoverageBase.option_spec | {
500 "style": stripAndNormalize
501 }
503 directiveName: str = "code-coverage-legend"
505 _style: LegendStyle
507 def _CheckOptions(self) -> None:
508 # Parse all directive options or use default values
509 super()._CheckOptions()
511 self._style = self._ParseLegendStyle("style", LegendStyle.horizontal_table)
513 try:
514 packageConfiguration = self._packageConfigurations[self._reportID]
515 except KeyError as ex:
516 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
518 self._levels = packageConfiguration["levels"]
520 def _CreateHorizontalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
521 columns = [("Code Coverage:", 3)]
522 for level in self._levels:
523 if isinstance(level, int):
524 columns.append((f"≤{level} %", 2))
526 tableGroup = self._CreateSingleTableHeader(columns, identifier=identifier, classes=classes)
527 tableBody = nodes.tbody()
528 tableGroup += tableBody
530 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
531 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
532 tableBody += legendRow
533 for level, config in self._levels.items():
534 if isinstance(level, int):
535 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
537 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
538 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
539 tableBody += legendRow
540 for level, config in self._levels.items():
541 if isinstance(level, int):
542 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
544 return table
546 def _CreateVerticalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
547 tableGroup = self._CreateSingleTableHeader([
548 ("Code Coverage", 3),
549 ("Coverage Level", 3)
550 ],
551 identifier=identifier,
552 classes=classes
553 )
555 tableBody = nodes.tbody()
556 tableGroup += tableBody
558 for level, config in self._levels.items():
559 if isinstance(level, int):
560 tableBody += nodes.row(
561 "",
562 nodes.entry("", nodes.Text(f"≤{level} %")),
563 nodes.entry("", nodes.paragraph(text=config["desc"])),
564 classes=["report-codecov-legend-row", self._ConvertToColor((level - 1) / 100, "class")]
565 )
567 return tableGroup.parent
569 def run(self) -> List[nodes.Node]:
570 container = nodes.container()
572 try:
573 self._CheckOptions()
574 except ReportExtensionError as ex:
575 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
576 return self._internalError(container, __name__, message, ex)
578 if LegendStyle.Table in self._style:
579 if LegendStyle.Horizontal in self._style:
580 container += self._CreateHorizontalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
581 elif LegendStyle.Vertical in self._style:
582 container += self._CreateVerticalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
583 else:
584 container += nodes.paragraph(text=f"Unsupported legend style.")
585 else:
586 container += nodes.paragraph(text=f"Unsupported legend style.")
588 return [container]
592@export
593class ModuleCoverage(CodeCoverageBase):
594 """
595 This directive will be replaced by highlighted source code.
596 """
597 directiveName: str = "module-coverage"
599 has_content = False
600 required_arguments = 0
601 optional_arguments = 2
603 option_spec = CodeCoverageBase.option_spec | {
604 "module": stripAndNormalize
605 }
607 _packageName: str
608 _moduleName: str
609 _jsonReport: Path
611 def _CheckOptions(self) -> None:
612 """
613 Parse all directive options or use default values.
614 """
615 super()._CheckOptions()
617 self._moduleName = self._ParseStringOption("module")
619 try:
620 packageConfiguration = self._packageConfigurations[self._reportID]
621 except KeyError as ex:
622 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
624 self._packageName = packageConfiguration["name"]
625 self._jsonReport = packageConfiguration["json_report"]
627 def run(self) -> List[nodes.Node]:
628 container = nodes.container()
630 try:
631 self._CheckOptions()
632 except ReportExtensionError as ex:
633 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
634 return self._internalError(container, __name__, message, ex)
636 # Assemble a list of Python source files
637 analyzer = Analyzer(self._packageName, self._jsonReport)
638 self._coverage = analyzer.Convert()
640 sourceFile = "../../sphinx_reports/__init__.py"
642 container += nodes.paragraph(text=f"Code coverage of {self._moduleName}")
644 # lexer = get_lexer_by_name("python", tabsize=2)
645 # tokens = lex(code, lexer)
647 # htmlFormatter = HtmlFormatter(linenos=True, cssclass="source")
648 # highlight()
650 location = self.state_machine.get_source_and_line(self.lineno)
651 rel_filename, filename = self.env.relfn2path(sourceFile)
652 self.env.note_dependency(rel_filename)
654 reader = LiteralIncludeReader(filename, {"tab-width": 2}, self.config)
655 text, lines = reader.read(location=location)
657 literalBlock: nodes.Element = nodes.literal_block(text, text, source=filename)
658 literalBlock["language"] = "codecov"
659 literalBlock['highlight_args'] = extra_args = {}
660 extra_args['hl_lines'] = [i for i in range(10, 20)]
661 self.set_source_info(literalBlock)
663 container += literalBlock
665 return [container]