Coverage for sphinx_reports/CodeCoverage.py: 18%
371 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 18:49 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-02 18:49 +0000
1# ==================================================================================================================== #
2# _ _ _ #
3# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ #
4# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| #
5# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ #
6# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ #
7# |_| |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2023-2025 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, BaseDirective
48from sphinx_reports.DataModel.CodeCoverage import PackageCoverage, Coverage, ModuleCoverage
49from sphinx_reports.Adapter.Coverage import Analyzer
52class package_DictType(TypedDict):
53 name: str
54 json_report: Path
55 fail_below: int
56 levels: Union[str, Dict[Union[int, str], Dict[str, str]]]
59@export
60class CodeCoverageBase(BaseDirective):
62 option_spec = {
63 "class": strip,
64 "reportid": strip
65 }
67 defaultCoverageDefinitions = {
68 "default": {
69 10: {"class": "report-cov-below10", "desc": "almost unused"},
70 20: {"class": "report-cov-below20", "desc": "almost unused"},
71 30: {"class": "report-cov-below30", "desc": "almost unused"},
72 40: {"class": "report-cov-below40", "desc": "poorly used"},
73 50: {"class": "report-cov-below50", "desc": "poorly used"},
74 60: {"class": "report-cov-below60", "desc": "somehow used"},
75 70: {"class": "report-cov-below70", "desc": "somehow used"},
76 80: {"class": "report-cov-below80", "desc": "somehow used"},
77 85: {"class": "report-cov-below85", "desc": "well used"},
78 90: {"class": "report-cov-below90", "desc": "well used"},
79 95: {"class": "report-cov-below95", "desc": "well used"},
80 100: {"class": "report-cov-below100", "desc": "excellently used"},
81 "error": {"class": "report-cov-error", "desc": "internal error"},
82 }
83 }
85 configPrefix: str = "codecov"
86 configValues: Dict[str, Tuple[Any, str, Any]] = {
87 f"{configPrefix}_packages": ({}, "env", Dict),
88 f"{configPrefix}_levels": (defaultCoverageDefinitions, "env", Dict),
89 } #: A dictionary of all configuration values used by code coverage directives.
91 _coverageLevelDefinitions: ClassVar[Dict[str, Dict[Union[int, str], Dict[str, str]]]] = {}
92 _packageConfigurations: ClassVar[Dict[str, package_DictType]] = {}
94 _cssClasses: List[str]
95 _reportID: str
96 _levels: Dict[Union[int, str], Dict[str, str]]
98 def _CheckOptions(self) -> None:
99 """
100 Parse all directive options or use default values.
101 """
102 cssClasses = self._ParseStringOption("class", "", r"(\w+)?( +\w+)*")
104 self._reportID = self._ParseStringOption("reportid")
105 self._cssClasses = [] if cssClasses == "" else cssClasses.split(" ")
107 @classmethod
108 def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None:
109 """
110 Check configuration fields and load necessary values.
112 :param sphinxApplication: Sphinx application instance.
113 :param sphinxConfiguration: Sphinx configuration instance.
114 """
115 cls._CheckLevelsConfiguration(sphinxConfiguration)
116 cls._CheckPackagesConfiguration(sphinxConfiguration)
118 @classmethod
119 def ReadReports(cls, sphinxApplication: Sphinx) -> None:
120 """
121 Read code coverage report files.
123 :param sphinxApplication: Sphinx application instance.
124 """
125 print(f"[REPORT] Reading code coverage reports ...")
127 @classmethod
128 def _CheckLevelsConfiguration(cls, sphinxConfiguration: Config) -> None:
129 from sphinx_reports import ReportDomain
131 variableName = f"{ReportDomain.name}_{cls.configPrefix}_levels"
133 try:
134 coverageLevelDefinitions: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_levels"]
135 except (KeyError, AttributeError) as ex:
136 raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex
138 if "default" not in coverageLevelDefinitions:
139 cls._coverageLevelDefinitions["default"] = cls.defaultCoverageDefinitions["default"]
141 for key, coverageLevelDefinition in coverageLevelDefinitions.items():
142 configurationName = f"conf.py: {variableName}:[{key}]"
144 if 100 not in coverageLevelDefinition:
145 raise ReportExtensionError(f"{configurationName}[100]: Configuration is missing.")
146 elif "error" not in coverageLevelDefinition:
147 raise ReportExtensionError(f"{configurationName}[error]: Configuration is missing.")
149 cls._coverageLevelDefinitions[key] = {}
151 for level, levelConfig in coverageLevelDefinition.items():
152 try:
153 if isinstance(level, str):
154 if level != "error":
155 raise ReportExtensionError(f"{configurationName}[{level}]: Level is a keyword, but not 'error'.")
156 elif not (0.0 <= int(level) <= 100.0):
157 raise ReportExtensionError(f"{configurationName}[{level}]: Level is out of range 0..100.")
158 except ValueError as ex:
159 raise ReportExtensionError(f"{configurationName}[{level}]: Level is not a keyword or an integer in range 0..100.") from ex
161 try:
162 cssClass = levelConfig["class"]
163 except KeyError as ex:
164 raise ReportExtensionError(f"{configurationName}[{level}].class: CSS class is missing.") from ex
166 try:
167 description = levelConfig["desc"]
168 except KeyError as ex:
169 raise ReportExtensionError(f"{configurationName}[{level}].desc: Description is missing.") from ex
171 cls._coverageLevelDefinitions[key][level] = {
172 "class": cssClass,
173 "desc": description
174 }
176 @classmethod
177 def _CheckPackagesConfiguration(cls, sphinxConfiguration: Config) -> None:
178 from sphinx_reports import ReportDomain
180 variableName = f"{ReportDomain.name}_{cls.configPrefix}_packages"
182 try:
183 allPackages: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_packages"]
184 except (KeyError, AttributeError) as ex:
185 raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex
187 # try:
188 # packageConfiguration = allPackages[self._reportID]
189 # except KeyError as ex:
190 # raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{cls.configPrefix}_packages: No configuration found for '{self._reportID}'.") from ex
192 for reportID, packageConfiguration in allPackages.items():
193 configurationName = f"conf.py: {variableName}:[{reportID}]"
195 try:
196 packageName = packageConfiguration["name"]
197 except KeyError as ex:
198 raise ReportExtensionError(f"{configurationName}.name: Configuration is missing.") from ex
200 try:
201 jsonReport = Path(packageConfiguration["json_report"])
202 except KeyError as ex:
203 raise ReportExtensionError(f"{configurationName}.json_report: Configuration is missing.") from ex
205 if not jsonReport.exists():
206 raise ReportExtensionError(f"{configurationName}.json_report: Coverage report file '{jsonReport}' doesn't exist.") from FileNotFoundError(jsonReport)
208 try:
209 failBelow = int(packageConfiguration["fail_below"]) / 100
210 except KeyError as ex:
211 raise ReportExtensionError(f"{configurationName}.fail_below: Configuration is missing.") from ex
212 except ValueError as ex:
213 raise ReportExtensionError(f"{configurationName}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex
215 if not (0.0 <= failBelow <= 100.0):
216 raise ReportExtensionError(f"{configurationName}.fail_below: Is out of range 0..100.")
218 try:
219 levels = packageConfiguration["levels"]
220 except KeyError as ex:
221 raise ReportExtensionError(f"{configurationName}.levels: Configuration is missing.") from ex
223 if isinstance(levels, str):
224 try:
225 levelDefinition = cls._coverageLevelDefinitions[levels]
226 except KeyError as ex:
227 raise ReportExtensionError(
228 f"{configurationName}.levels: Referenced coverage levels '{levels}' are not defined in conf.py variable '{variableName}'.") from ex
229 elif isinstance(levels, dict):
230 if 100 not in packageConfiguration["levels"]:
231 raise ReportExtensionError(f"{configurationName}.levels[100]: Configuration is missing.")
232 elif "error" not in packageConfiguration["levels"]:
233 raise ReportExtensionError(f"{configurationName}.levels[error]: Configuration is missing.")
235 levelDefinition = {}
236 for x, y in packageConfiguration["levels"].items():
237 pass
238 else:
239 raise ReportExtensionError(f"")
241 cls._packageConfigurations[reportID] = {
242 "name": packageName,
243 "json_report": jsonReport,
244 "fail_below": failBelow,
245 "levels": levelDefinition
246 }
248 def _ConvertToColor(self, currentLevel: float, configKey: str) -> str:
249 if currentLevel < 0.0:
250 return self._levels["error"][configKey]
252 for levelLimit, levelConfig in self._levels.items():
253 if isinstance(levelLimit, int) and (currentLevel * 100) < levelLimit:
254 return levelConfig[configKey]
256 return self._levels[100][configKey]
259@export
260class CodeCoverage(CodeCoverageBase):
261 """
262 This directive will be replaced by a table representing code coverage.
263 """
264 directiveName: str = "code-coverage"
266 has_content = False
267 required_arguments = 0
268 optional_arguments = CodeCoverageBase.optional_arguments + 1
270 option_spec = CodeCoverageBase.option_spec | {
271 "no-branch-coverage": flag
272 }
274 _noBranchCoverage: bool
275 _packageName: str
276 _jsonReport: Path
277 _failBelow: float
278 _coverage: PackageCoverage
280 def _CheckOptions(self) -> None:
281 """
282 Parse all directive options or use default values.
283 """
284 super()._CheckOptions()
286 self._noBranchCoverage = "no-branch-coverage" in self.options
288 try:
289 packageConfiguration = self._packageConfigurations[self._reportID]
290 except KeyError as ex:
291 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
293 self._packageName = packageConfiguration["name"]
294 self._jsonReport = packageConfiguration["json_report"]
295 self._failBelow = packageConfiguration["fail_below"]
296 self._levels = packageConfiguration["levels"]
298 def _GenerateCoverageTable(self) -> nodes.table:
299 cssClasses = ["report-codecov-table", f"report-codecov-{self._reportID}"]
300 cssClasses.extend(self._cssClasses)
302 # Create a table and table header with 10 columns
303 columns = [
304 ("Package", [(" Module", 500)], None),
305 ("Statments", [("Total", 100), ("Excluded", 100), ("Covered", 100), ("Missing", 100), ("Coverage", 100)], None),
306 ("Branches" , [("Total", 100), ("Covered", 100), ("Partial", 100), ("Missing", 100), ("Coverage", 100)], None),
307 # ("Coverage", [("in %", 100)], None)
308 ]
310 if self._noBranchCoverage:
311 columns.pop(2)
313 table, tableGroup = self._CreateTableHeader(
314 identifier=self._reportID,
315 columns=columns,
316 classes=cssClasses
317 )
318 tableBody = nodes.tbody()
319 tableGroup += tableBody
321 self.renderlevel(tableBody, self._coverage)
323 # Add a summary row
324 tableRow = nodes.row("", classes=[
325 "report-summary",
326 self._ConvertToColor(self._coverage.AggregatedStatementCoverage, "class")
327 ])
328 tableBody += tableRow
330 tableRow += nodes.entry("", nodes.paragraph(text=f"Overall ({self._coverage.FileCount} files):"))
331 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedTotalStatements}"))
332 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedExcludedStatements}"))
333 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedCoveredStatements}"))
334 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedMissingStatements}"))
335 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedStatementCoverage:.1%}"))
336 if not self._noBranchCoverage:
337 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedTotalBranches}"))
338 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedCoveredBranches}"))
339 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedPartialBranches}"))
340 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedMissingBranches}"))
341 tableRow += nodes.entry("", nodes.paragraph(text=f"{self._coverage.AggregatedBranchCoverage:.1%}"))
343 return table
345 def sortedValues(self, d: Mapping[str, Coverage]) -> Generator[Coverage, None, None]:
346 for key in sorted(d.keys()):
347 yield d[key]
349 def renderlevel(self, tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: int = 0) -> None:
350 coverage = 1 if packageCoverage.Coverage < 0.0 else packageCoverage.Coverage
351 tableRow = nodes.row("", classes=[
352 "report-package",
353 self._ConvertToColor(coverage, "class")
354 ])
355 tableBody += tableRow
357 tableRow += nodes.entry("", nodes.paragraph(text=f"{' ' * level}📦{packageCoverage.Name}"))
358 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.TotalStatements}"))
359 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.ExcludedStatements}"))
360 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.CoveredStatements}"))
361 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.MissingStatements}"))
362 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.StatementCoverage:.1%}"))
363 if not self._noBranchCoverage:
364 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.TotalBranches}"))
365 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.CoveredBranches}"))
366 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.PartialBranches}"))
367 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.MissingBranches}"))
368 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.BranchCoverage:.1%}"))
370 for package in self.sortedValues(packageCoverage._packages):
371 self.renderlevel(tableBody, package, level + 1)
373 for module in self.sortedValues(packageCoverage._modules):
374 tableRow = nodes.row("", classes=[
375 "report-module",
376 self._ConvertToColor(module.Coverage, "class")
377 ])
378 tableBody += tableRow
380 tableRow += nodes.entry("", nodes.paragraph(text=f"{' ' * (level + 1)} 📓{module.Name}"))
381 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.TotalStatements}"))
382 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.ExcludedStatements}"))
383 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.CoveredStatements}"))
384 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.MissingStatements}"))
385 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.StatementCoverage:.1%}"))
386 if not self._noBranchCoverage:
387 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.TotalBranches}"))
388 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.CoveredBranches}"))
389 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.PartialBranches}"))
390 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.MissingBranches}"))
391 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.BranchCoverage:.1%}"))
393 def _CreatePages(self) -> None:
394 def handlePackage(package: PackageCoverage) -> None:
395 for pack in package._packages.values():
396 if handlePackage(pack):
397 return
399 for module in package._modules.values():
400 if handleModule(module):
401 return
403 def handleModule(module: ModuleCoverage) -> None:
404 doc = new_document("dummy")
406 rootSection = nodes.section(ids=["foo"])
407 doc += rootSection
409 title = nodes.title(text=f"{module.Name}")
410 rootSection += title
411 rootSection += nodes.paragraph(text="some text")
413 docname = f"coverage/{module.Name}"
414 self.env.titles[docname] = title
415 self.env.longtitles[docname] = title
417 return
419 handlePackage(self._coverage)
421 def run(self) -> List[nodes.Node]:
422 container = nodes.container()
424 try:
425 self._CheckOptions()
426 except ReportExtensionError as ex:
427 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
428 return self._internalError(container, __name__, message, ex)
430 # Assemble a list of Python source files
431 analyzer = Analyzer(self._packageName, self._jsonReport)
432 self._coverage = analyzer.Convert()
433 # self._coverage.Aggregate()
435 self._CreatePages()
437 container += self._GenerateCoverageTable()
439 def foo():
440 docName = self.env.docname
441 docParent = docName[:docName.rindex("/")]
443 subnode = toctree()
444 subnode['parent'] = docName
446 # (title, ref) pairs, where ref may be a document, or an external link,
447 # and title may be None if the document's title is to be used
448 subnode['entries'] = []
449 subnode['includefiles'] = []
450 subnode['maxdepth'] = -1 # self.options.get('maxdepth', -1)
451 subnode['caption'] = None # self.options.get('caption')
452 subnode['glob'] = None # 'glob' in self.options
453 subnode['hidden'] = True # 'hidden' in self.options
454 subnode['includehidden'] = False # 'includehidden' in self.options
455 subnode['numbered'] = 0 # self.options.get('numbered', 0)
456 subnode['titlesonly'] = False # 'titlesonly' in self.options
457 self.set_source_info(subnode)
459 wrappernode = nodes.compound(classes=['toctree-wrapper'])
460 wrappernode.append(subnode)
461 self.add_name(wrappernode)
463 for entry in (
464 "sphinx_reports",
465 "sphinx_reports.Adapter",
466 "sphinx_reports.Adapter.Coverage",
467 "sphinx_reports.Adapter.DocStrCoverage",
468 "sphinx_reports.Adapter.JUnit",
469 "sphinx_reports.DataModel",
470 "sphinx_reports.DataModel.CodeCoverage",
471 "sphinx_reports.DataModel.DocumentationCoverage",
472 "sphinx_reports.DataModel.Unittest",
473 "sphinx_reports.static",
474 "sphinx_reports.CodeCoverage",
475 "sphinx_reports.Common",
476 "sphinx_reports.DocCoverage",
477 "sphinx_reports.Sphinx",
478 "sphinx_reports.Unittest",
479 ):
480 moduleDocumentName = f"{docParent}/{entry}"
481 moduleDocumentTitle = entry
483 subnode["entries"].append((moduleDocumentTitle, moduleDocumentName))
484 subnode["includefiles"].append(moduleDocumentName)
486 return [container] #, wrappernode]
489@export
490class CodeCoverageLegend(CodeCoverageBase):
491 """
492 This directive will be replaced by a legend table representing coverage levels.
493 """
494 has_content = False
495 required_arguments = 0
496 optional_arguments = CodeCoverageBase.optional_arguments + 1
498 option_spec = CodeCoverageBase.option_spec | {
499 "style": strip
500 }
502 directiveName: str = "code-coverage-legend"
504 _style: LegendStyle
506 def _CheckOptions(self) -> None:
507 # Parse all directive options or use default values
508 super()._CheckOptions()
510 self._style = self._ParseLegendStyle("style", LegendStyle.horizontal_table)
512 try:
513 packageConfiguration = self._packageConfigurations[self._reportID]
514 except KeyError as ex:
515 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
517 self._levels = packageConfiguration["levels"]
519 def _CreateHorizontalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
520 columns = [("Code Coverage:", None, 300)]
521 for level in self._levels:
522 if isinstance(level, int):
523 columns.append((f"≤{level} %", None, 200))
525 table, tableGroup = self._CreateTableHeader(columns, identifier=identifier, classes=classes)
526 tableBody = nodes.tbody()
527 tableGroup += tableBody
529 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
530 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
531 tableBody += legendRow
532 for level, config in self._levels.items():
533 if isinstance(level, int):
534 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
536 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
537 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
538 tableBody += legendRow
539 for level, config in self._levels.items():
540 if isinstance(level, int):
541 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
543 return table
545 def _CreateVerticalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
546 table, tableGroup = self._CreateTableHeader([
547 ("Code Coverage", None, 300),
548 ("Coverage Level", None, 300)
549 ],
550 identifier=identifier,
551 classes=classes
552 )
554 tableBody = nodes.tbody()
555 tableGroup += tableBody
557 for level, config in self._levels.items():
558 if isinstance(level, int):
559 tableBody += nodes.row(
560 "",
561 nodes.entry("", nodes.paragraph(text=f"≤{level} %")),
562 nodes.entry("", nodes.paragraph(text=config["desc"])),
563 classes=["report-codecov-legend-row", self._ConvertToColor((level - 1) / 100, "class")]
564 )
566 return table
568 def run(self) -> List[nodes.Node]:
569 container = nodes.container()
571 try:
572 self._CheckOptions()
573 except ReportExtensionError as ex:
574 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
575 return self._internalError(container, __name__, message, ex)
577 if LegendStyle.Table in self._style:
578 if LegendStyle.Horizontal in self._style:
579 container += self._CreateHorizontalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
580 elif LegendStyle.Vertical in self._style:
581 container += self._CreateVerticalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
582 else:
583 container += nodes.paragraph(text=f"Unsupported legend style.")
584 else:
585 container += nodes.paragraph(text=f"Unsupported legend style.")
587 return [container]
591@export
592class ModuleCoverage(CodeCoverageBase):
593 """
594 This directive will be replaced by highlighted source code.
595 """
596 directiveName: str = "module-coverage"
598 has_content = False
599 required_arguments = 0
600 optional_arguments = 2
602 option_spec = CodeCoverageBase.option_spec | {
603 "module": strip
604 }
606 _packageName: str
607 _moduleName: str
608 _jsonReport: Path
610 def _CheckOptions(self) -> None:
611 """
612 Parse all directive options or use default values.
613 """
614 super()._CheckOptions()
616 self._moduleName = self._ParseStringOption("module")
618 try:
619 packageConfiguration = self._packageConfigurations[self._reportID]
620 except KeyError as ex:
621 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
623 self._packageName = packageConfiguration["name"]
624 self._jsonReport = packageConfiguration["json_report"]
626 def run(self) -> List[nodes.Node]:
627 container = nodes.container()
629 try:
630 self._CheckOptions()
631 except ReportExtensionError as ex:
632 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
633 return self._internalError(container, __name__, message, ex)
635 # Assemble a list of Python source files
636 analyzer = Analyzer(self._packageName, self._jsonReport)
637 self._coverage = analyzer.Convert()
639 sourceFile = "../../sphinx_reports/__init__.py"
641 container += nodes.paragraph(text=f"Code coverage of {self._moduleName}")
643 # lexer = get_lexer_by_name("python", tabsize=2)
644 # tokens = lex(code, lexer)
646 # htmlFormatter = HtmlFormatter(linenos=True, cssclass="source")
647 # highlight()
649 location = self.state_machine.get_source_and_line(self.lineno)
650 rel_filename, filename = self.env.relfn2path(sourceFile)
651 self.env.note_dependency(rel_filename)
653 reader = LiteralIncludeReader(filename, {"tab-width": 2}, self.config)
654 text, lines = reader.read(location=location)
656 literalBlock: nodes.Element = nodes.literal_block(text, text, source=filename)
657 literalBlock["language"] = "codecov"
658 literalBlock['highlight_args'] = extra_args = {}
659 extra_args['hl_lines'] = [i for i in range(10, 20)]
660 self.set_source_info(literalBlock)
662 container += literalBlock
664 return [container]