Coverage for sphinx_reports/CodeCoverage.py: 18%
370 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 22:12 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 22:12 +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.Coverage, "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 tableRow = nodes.row("", classes=[
351 "report-package",
352 self._ConvertToColor(packageCoverage.Coverage, "class")
353 ])
354 tableBody += tableRow
356 tableRow += nodes.entry("", nodes.paragraph(text=f"{' ' * level}📦{packageCoverage.Name}"))
357 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.TotalStatements}"))
358 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.ExcludedStatements}"))
359 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.CoveredStatements}"))
360 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.MissingStatements}"))
361 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.StatementCoverage:.1%}"))
362 if not self._noBranchCoverage:
363 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.TotalBranches}"))
364 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.CoveredBranches}"))
365 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.PartialBranches}"))
366 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.MissingBranches}"))
367 tableRow += nodes.entry("", nodes.paragraph(text=f"{packageCoverage.BranchCoverage:.1%}"))
369 for package in self.sortedValues(packageCoverage._packages):
370 self.renderlevel(tableBody, package, level + 1)
372 for module in self.sortedValues(packageCoverage._modules):
373 tableRow = nodes.row("", classes=[
374 "report-module",
375 self._ConvertToColor(module.Coverage, "class")
376 ])
377 tableBody += tableRow
379 tableRow += nodes.entry("", nodes.paragraph(text=f"{' ' * (level + 1)} {module.Name}"))
380 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.TotalStatements}"))
381 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.ExcludedStatements}"))
382 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.CoveredStatements}"))
383 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.MissingStatements}"))
384 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.StatementCoverage:.1%}"))
385 if not self._noBranchCoverage:
386 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.TotalBranches}"))
387 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.CoveredBranches}"))
388 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.PartialBranches}"))
389 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.MissingBranches}"))
390 tableRow += nodes.entry("", nodes.paragraph(text=f"{module.BranchCoverage:.1%}"))
392 def _CreatePages(self) -> None:
393 def handlePackage(package: PackageCoverage) -> None:
394 for pack in package._packages.values():
395 if handlePackage(pack):
396 return
398 for module in package._modules.values():
399 if handleModule(module):
400 return
402 def handleModule(module: ModuleCoverage) -> None:
403 doc = new_document("dummy")
405 rootSection = nodes.section(ids=["foo"])
406 doc += rootSection
408 title = nodes.title(text=f"{module.Name}")
409 rootSection += title
410 rootSection += nodes.paragraph(text="some text")
412 docname = f"coverage/{module.Name}"
413 self.env.titles[docname] = title
414 self.env.longtitles[docname] = title
416 return
418 handlePackage(self._coverage)
420 def run(self) -> List[nodes.Node]:
421 container = nodes.container()
423 try:
424 self._CheckOptions()
425 except ReportExtensionError as ex:
426 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
427 return self._internalError(container, __name__, message, ex)
429 # Assemble a list of Python source files
430 analyzer = Analyzer(self._packageName, self._jsonReport)
431 self._coverage = analyzer.Convert()
432 # self._coverage.Aggregate()
434 self._CreatePages()
436 container += self._GenerateCoverageTable()
438 def foo():
439 docName = self.env.docname
440 docParent = docName[:docName.rindex("/")]
442 subnode = toctree()
443 subnode['parent'] = docName
445 # (title, ref) pairs, where ref may be a document, or an external link,
446 # and title may be None if the document's title is to be used
447 subnode['entries'] = []
448 subnode['includefiles'] = []
449 subnode['maxdepth'] = -1 # self.options.get('maxdepth', -1)
450 subnode['caption'] = None # self.options.get('caption')
451 subnode['glob'] = None # 'glob' in self.options
452 subnode['hidden'] = True # 'hidden' in self.options
453 subnode['includehidden'] = False # 'includehidden' in self.options
454 subnode['numbered'] = 0 # self.options.get('numbered', 0)
455 subnode['titlesonly'] = False # 'titlesonly' in self.options
456 self.set_source_info(subnode)
458 wrappernode = nodes.compound(classes=['toctree-wrapper'])
459 wrappernode.append(subnode)
460 self.add_name(wrappernode)
462 for entry in (
463 "sphinx_reports",
464 "sphinx_reports.Adapter",
465 "sphinx_reports.Adapter.Coverage",
466 "sphinx_reports.Adapter.DocStrCoverage",
467 "sphinx_reports.Adapter.JUnit",
468 "sphinx_reports.DataModel",
469 "sphinx_reports.DataModel.CodeCoverage",
470 "sphinx_reports.DataModel.DocumentationCoverage",
471 "sphinx_reports.DataModel.Unittest",
472 "sphinx_reports.static",
473 "sphinx_reports.CodeCoverage",
474 "sphinx_reports.Common",
475 "sphinx_reports.DocCoverage",
476 "sphinx_reports.Sphinx",
477 "sphinx_reports.Unittest",
478 ):
479 moduleDocumentName = f"{docParent}/{entry}"
480 moduleDocumentTitle = entry
482 subnode["entries"].append((moduleDocumentTitle, moduleDocumentName))
483 subnode["includefiles"].append(moduleDocumentName)
485 return [container] #, wrappernode]
488@export
489class CodeCoverageLegend(CodeCoverageBase):
490 """
491 This directive will be replaced by a legend table representing coverage levels.
492 """
493 has_content = False
494 required_arguments = 0
495 optional_arguments = CodeCoverageBase.optional_arguments + 1
497 option_spec = CodeCoverageBase.option_spec | {
498 "style": strip
499 }
501 directiveName: str = "code-coverage-legend"
503 _style: LegendStyle
505 def _CheckOptions(self) -> None:
506 # Parse all directive options or use default values
507 super()._CheckOptions()
509 self._style = self._ParseLegendStyle("style", LegendStyle.horizontal_table)
511 try:
512 packageConfiguration = self._packageConfigurations[self._reportID]
513 except KeyError as ex:
514 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
516 self._levels = packageConfiguration["levels"]
518 def _CreateHorizontalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
519 columns = [("Code Coverage:", None, 300)]
520 for level in self._levels:
521 if isinstance(level, int):
522 columns.append((f"≤{level} %", None, 200))
524 table, tableGroup = self._CreateTableHeader(columns, identifier=identifier, classes=classes)
525 tableBody = nodes.tbody()
526 tableGroup += tableBody
528 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
529 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
530 tableBody += legendRow
531 for level, config in self._levels.items():
532 if isinstance(level, int):
533 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
535 legendRow = nodes.row("", classes=["report-codecov-legend-row"])
536 legendRow += nodes.entry("", nodes.paragraph(text="Coverage Level:"))
537 tableBody += legendRow
538 for level, config in self._levels.items():
539 if isinstance(level, int):
540 legendRow += nodes.entry("", nodes.paragraph(text=config["desc"]), classes=[self._ConvertToColor((level - 1) / 100, "class")])
542 return table
544 def _CreateVerticalLegendTable(self, identifier: str, classes: List[str]) -> nodes.table:
545 table, tableGroup = self._CreateTableHeader([
546 ("Code Coverage", None, 300),
547 ("Coverage Level", None, 300)
548 ],
549 identifier=identifier,
550 classes=classes
551 )
553 tableBody = nodes.tbody()
554 tableGroup += tableBody
556 for level, config in self._levels.items():
557 if isinstance(level, int):
558 tableBody += nodes.row(
559 "",
560 nodes.entry("", nodes.paragraph(text=f"≤{level} %")),
561 nodes.entry("", nodes.paragraph(text=config["desc"])),
562 classes=["report-codecov-legend-row", self._ConvertToColor((level - 1) / 100, "class")]
563 )
565 return table
567 def run(self) -> List[nodes.Node]:
568 container = nodes.container()
570 try:
571 self._CheckOptions()
572 except ReportExtensionError as ex:
573 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
574 return self._internalError(container, __name__, message, ex)
576 if LegendStyle.Table in self._style:
577 if LegendStyle.Horizontal in self._style:
578 container += self._CreateHorizontalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
579 elif LegendStyle.Vertical in self._style:
580 container += self._CreateVerticalLegendTable(identifier=f"{self._reportID}-legend", classes=["report-codecov-legend"])
581 else:
582 container += nodes.paragraph(text=f"Unsupported legend style.")
583 else:
584 container += nodes.paragraph(text=f"Unsupported legend style.")
586 return [container]
590@export
591class ModuleCoverage(CodeCoverageBase):
592 """
593 This directive will be replaced by highlighted source code.
594 """
595 directiveName: str = "module-coverage"
597 has_content = False
598 required_arguments = 0
599 optional_arguments = 2
601 option_spec = CodeCoverageBase.option_spec | {
602 "module": strip
603 }
605 _packageName: str
606 _moduleName: str
607 _jsonReport: Path
609 def _CheckOptions(self) -> None:
610 """
611 Parse all directive options or use default values.
612 """
613 super()._CheckOptions()
615 self._moduleName = self._ParseStringOption("module")
617 try:
618 packageConfiguration = self._packageConfigurations[self._reportID]
619 except KeyError as ex:
620 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex
622 self._packageName = packageConfiguration["name"]
623 self._jsonReport = packageConfiguration["json_report"]
625 def run(self) -> List[nodes.Node]:
626 container = nodes.container()
628 try:
629 self._CheckOptions()
630 except ReportExtensionError as ex:
631 message = f"Caught {ex.__class__.__name__} when checking options for directive '{self.directiveName}'."
632 return self._internalError(container, __name__, message, ex)
634 # Assemble a list of Python source files
635 analyzer = Analyzer(self._packageName, self._jsonReport)
636 self._coverage = analyzer.Convert()
638 sourceFile = "../../sphinx_reports/__init__.py"
640 container += nodes.paragraph(text=f"Code coverage of {self._moduleName}")
642 # lexer = get_lexer_by_name("python", tabsize=2)
643 # tokens = lex(code, lexer)
645 # htmlFormatter = HtmlFormatter(linenos=True, cssclass="source")
646 # highlight()
648 location = self.state_machine.get_source_and_line(self.lineno)
649 rel_filename, filename = self.env.relfn2path(sourceFile)
650 self.env.note_dependency(rel_filename)
652 reader = LiteralIncludeReader(filename, {"tab-width": 2}, self.config)
653 text, lines = reader.read(location=location)
655 literalBlock: nodes.Element = nodes.literal_block(text, text, source=filename)
656 literalBlock["language"] = "codecov"
657 literalBlock['highlight_args'] = extra_args = {}
658 extra_args['hl_lines'] = [i for i in range(10, 20)]
659 self.set_source_info(literalBlock)
661 container += literalBlock
663 return [container]