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

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 

36 

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 

45 

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 

51 

52 

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]]] 

58 

59 

60@export 

61class CodeCoverageBase(BaseDirective): 

62 

63 option_spec = { 

64 "class": strip, 

65 "reportid": stripAndNormalize 

66 } 

67 

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 } 

85 

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. 

91 

92 _coverageLevelDefinitions: ClassVar[Dict[str, Dict[Union[int, str], Dict[str, str]]]] = {} 

93 _packageConfigurations: ClassVar[Dict[str, package_DictType]] = {} 

94 

95 _cssClasses: List[str] 

96 _reportID: str 

97 _levels: Dict[Union[int, str], Dict[str, str]] 

98 

99 def _CheckOptions(self) -> None: 

100 """ 

101 Parse all directive options or use default values. 

102 """ 

103 cssClasses = self._ParseStringOption("class", "", r"(\w+)?( +\w+)*") 

104 

105 self._reportID = self._ParseStringOption("reportid") 

106 self._cssClasses = [] if cssClasses == "" else cssClasses.split(" ") 

107 

108 @classmethod 

109 def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None: 

110 """ 

111 Check configuration fields and load necessary values. 

112 

113 :param sphinxApplication: Sphinx application instance. 

114 :param sphinxConfiguration: Sphinx configuration instance. 

115 """ 

116 cls._CheckLevelsConfiguration(sphinxConfiguration) 

117 cls._CheckPackagesConfiguration(sphinxConfiguration) 

118 

119 @classmethod 

120 def ReadReports(cls, sphinxApplication: Sphinx) -> None: 

121 """ 

122 Read code coverage report files. 

123 

124 :param sphinxApplication: Sphinx application instance. 

125 """ 

126 print(f"[REPORT] Reading code coverage reports ...") 

127 

128 @classmethod 

129 def _CheckLevelsConfiguration(cls, sphinxConfiguration: Config) -> None: 

130 from sphinx_reports import ReportDomain 

131 

132 variableName = f"{ReportDomain.name}_{cls.configPrefix}_levels" 

133 

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 

138 

139 if "default" not in coverageLevelDefinitions: 

140 cls._coverageLevelDefinitions["default"] = cls.defaultCoverageDefinitions["default"] 

141 

142 for key, coverageLevelDefinition in coverageLevelDefinitions.items(): 

143 configurationName = f"conf.py: {variableName}:[{key}]" 

144 

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.") 

149 

150 cls._coverageLevelDefinitions[key] = {} 

151 

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 

161 

162 try: 

163 cssClass = levelConfig["class"] 

164 except KeyError as ex: 

165 raise ReportExtensionError(f"{configurationName}[{level}].class: CSS class is missing.") from ex 

166 

167 try: 

168 description = levelConfig["desc"] 

169 except KeyError as ex: 

170 raise ReportExtensionError(f"{configurationName}[{level}].desc: Description is missing.") from ex 

171 

172 cls._coverageLevelDefinitions[key][level] = { 

173 "class": cssClass, 

174 "desc": description 

175 } 

176 

177 @classmethod 

178 def _CheckPackagesConfiguration(cls, sphinxConfiguration: Config) -> None: 

179 from sphinx_reports import ReportDomain 

180 

181 variableName = f"{ReportDomain.name}_{cls.configPrefix}_packages" 

182 

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 

187 

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 

192 

193 for reportID, packageConfiguration in allPackages.items(): 

194 configurationName = f"conf.py: {variableName}:[{reportID}]" 

195 

196 try: 

197 packageName = packageConfiguration["name"] 

198 except KeyError as ex: 

199 raise ReportExtensionError(f"{configurationName}.name: Configuration is missing.") from ex 

200 

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 

205 

206 if not jsonReport.exists(): 

207 raise ReportExtensionError(f"{configurationName}.json_report: Coverage report file '{jsonReport}' doesn't exist.") from FileNotFoundError(jsonReport) 

208 

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 

215 

216 if not (0.0 <= failBelow <= 100.0): 

217 raise ReportExtensionError(f"{configurationName}.fail_below: Is out of range 0..100.") 

218 

219 try: 

220 levels = packageConfiguration["levels"] 

221 except KeyError as ex: 

222 raise ReportExtensionError(f"{configurationName}.levels: Configuration is missing.") from ex 

223 

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.") 

235 

236 levelDefinition = {} 

237 for x, y in packageConfiguration["levels"].items(): 

238 pass 

239 else: 

240 raise ReportExtensionError(f"") 

241 

242 cls._packageConfigurations[reportID] = { 

243 "name": packageName, 

244 "json_report": jsonReport, 

245 "fail_below": failBelow, 

246 "levels": levelDefinition 

247 } 

248 

249 def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: 

250 if currentLevel < 0.0: 

251 return self._levels["error"][configKey] 

252 

253 for levelLimit, levelConfig in self._levels.items(): 

254 if isinstance(levelLimit, int) and (currentLevel * 100) < levelLimit: 

255 return levelConfig[configKey] 

256 

257 return self._levels[100][configKey] 

258 

259 

260@export 

261class CodeCoverage(CodeCoverageBase): 

262 """ 

263 This directive will be replaced by a table representing code coverage. 

264 """ 

265 directiveName: str = "code-coverage" 

266 

267 has_content = False 

268 required_arguments = 0 

269 optional_arguments = CodeCoverageBase.optional_arguments + 1 

270 

271 option_spec = CodeCoverageBase.option_spec | { 

272 "no-branch-coverage": flag 

273 } 

274 

275 _noBranchCoverage: bool 

276 _packageName: str 

277 _jsonReport: Path 

278 _failBelow: float 

279 _coverage: PackageCoverage 

280 

281 def _CheckOptions(self) -> None: 

282 """ 

283 Parse all directive options or use default values. 

284 """ 

285 super()._CheckOptions() 

286 

287 self._noBranchCoverage = "no-branch-coverage" in self.options 

288 

289 try: 

290 packageConfiguration = self._packageConfigurations[self._reportID] 

291 except KeyError as ex: 

292 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex 

293 

294 self._packageName = packageConfiguration["name"] 

295 self._jsonReport = packageConfiguration["json_report"] 

296 self._failBelow = packageConfiguration["fail_below"] 

297 self._levels = packageConfiguration["levels"] 

298 

299 def _GenerateCoverageTable(self) -> nodes.table: 

300 cssClasses = ["report-codecov-table", f"report-codecov-{self._reportID}"] 

301 cssClasses.extend(self._cssClasses) 

302 

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 ] 

310 

311 if self._noBranchCoverage: 

312 columns.pop(2) 

313 

314 tableGroup = self._CreateDoubleRowTableHeader( 

315 identifier=self._reportID, 

316 columns=columns, 

317 classes=cssClasses 

318 ) 

319 tableBody = nodes.tbody() 

320 tableGroup += tableBody 

321 

322 self.renderlevel(tableBody, self._coverage) 

323 

324 # Add a summary row 

325 tableRow = nodes.row("", classes=[ 

326 "report-summary", 

327 self._ConvertToColor(self._coverage.AggregatedStatementCoverage, "class") 

328 ]) 

329 tableBody += tableRow 

330 

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%}")) 

343 

344 return tableGroup.parent 

345 

346 def sortedValues(self, d: Mapping[str, Coverage]) -> Generator[Coverage, None, None]: 

347 for key in sorted(d.keys()): 

348 yield d[key] 

349 

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 

357 

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%}")) 

370 

371 for package in self.sortedValues(packageCoverage._packages): 

372 self.renderlevel(tableBody, package, level + 1) 

373 

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 

380 

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%}")) 

393 

394 def _CreatePages(self) -> None: 

395 def handlePackage(package: PackageCoverage) -> None: 

396 for pack in package._packages.values(): 

397 if handlePackage(pack): 

398 return 

399 

400 for module in package._modules.values(): 

401 if handleModule(module): 

402 return 

403 

404 def handleModule(module: ModuleCoverage) -> None: 

405 doc = new_document("dummy") 

406 

407 rootSection = nodes.section(ids=["foo"]) 

408 doc += rootSection 

409 

410 title = nodes.title(text=f"{module.Name}") 

411 rootSection += title 

412 rootSection += nodes.paragraph(text="some text") 

413 

414 docname = f"coverage/{module.Name}" 

415 self.env.titles[docname] = title 

416 self.env.longtitles[docname] = title 

417 

418 return 

419 

420 handlePackage(self._coverage) 

421 

422 def run(self) -> List[nodes.Node]: 

423 container = Landscape() 

424 

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) 

430 

431 # Assemble a list of Python source files 

432 analyzer = Analyzer(self._packageName, self._jsonReport) 

433 self._coverage = analyzer.Convert() 

434 # self._coverage.Aggregate() 

435 

436 self._CreatePages() 

437 

438 container += self._GenerateCoverageTable() 

439 

440 def foo(): 

441 docName = self.env.docname 

442 docParent = docName[:docName.rindex("/")] 

443 

444 subnode = toctree() 

445 subnode['parent'] = docName 

446 

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) 

459 

460 wrappernode = nodes.compound(classes=['toctree-wrapper']) 

461 wrappernode.append(subnode) 

462 self.add_name(wrappernode) 

463 

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 

483 

484 subnode["entries"].append((moduleDocumentTitle, moduleDocumentName)) 

485 subnode["includefiles"].append(moduleDocumentName) 

486 

487 return [container] #, wrappernode] 

488 

489 

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 

498 

499 option_spec = CodeCoverageBase.option_spec | { 

500 "style": stripAndNormalize 

501 } 

502 

503 directiveName: str = "code-coverage-legend" 

504 

505 _style: LegendStyle 

506 

507 def _CheckOptions(self) -> None: 

508 # Parse all directive options or use default values 

509 super()._CheckOptions() 

510 

511 self._style = self._ParseLegendStyle("style", LegendStyle.horizontal_table) 

512 

513 try: 

514 packageConfiguration = self._packageConfigurations[self._reportID] 

515 except KeyError as ex: 

516 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex 

517 

518 self._levels = packageConfiguration["levels"] 

519 

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)) 

525 

526 tableGroup = self._CreateSingleTableHeader(columns, identifier=identifier, classes=classes) 

527 tableBody = nodes.tbody() 

528 tableGroup += tableBody 

529 

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")]) 

536 

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")]) 

543 

544 return table 

545 

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 ) 

554 

555 tableBody = nodes.tbody() 

556 tableGroup += tableBody 

557 

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 ) 

566 

567 return tableGroup.parent 

568 

569 def run(self) -> List[nodes.Node]: 

570 container = nodes.container() 

571 

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) 

577 

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.") 

587 

588 return [container] 

589 

590 

591 

592@export 

593class ModuleCoverage(CodeCoverageBase): 

594 """ 

595 This directive will be replaced by highlighted source code. 

596 """ 

597 directiveName: str = "module-coverage" 

598 

599 has_content = False 

600 required_arguments = 0 

601 optional_arguments = 2 

602 

603 option_spec = CodeCoverageBase.option_spec | { 

604 "module": stripAndNormalize 

605 } 

606 

607 _packageName: str 

608 _moduleName: str 

609 _jsonReport: Path 

610 

611 def _CheckOptions(self) -> None: 

612 """ 

613 Parse all directive options or use default values. 

614 """ 

615 super()._CheckOptions() 

616 

617 self._moduleName = self._ParseStringOption("module") 

618 

619 try: 

620 packageConfiguration = self._packageConfigurations[self._reportID] 

621 except KeyError as ex: 

622 raise ReportExtensionError(f"No configuration for '{self._reportID}'") from ex 

623 

624 self._packageName = packageConfiguration["name"] 

625 self._jsonReport = packageConfiguration["json_report"] 

626 

627 def run(self) -> List[nodes.Node]: 

628 container = nodes.container() 

629 

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) 

635 

636 # Assemble a list of Python source files 

637 analyzer = Analyzer(self._packageName, self._jsonReport) 

638 self._coverage = analyzer.Convert() 

639 

640 sourceFile = "../../sphinx_reports/__init__.py" 

641 

642 container += nodes.paragraph(text=f"Code coverage of {self._moduleName}") 

643 

644 # lexer = get_lexer_by_name("python", tabsize=2) 

645 # tokens = lex(code, lexer) 

646 

647 # htmlFormatter = HtmlFormatter(linenos=True, cssclass="source") 

648 # highlight() 

649 

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) 

653 

654 reader = LiteralIncludeReader(filename, {"tab-width": 2}, self.config) 

655 text, lines = reader.read(location=location) 

656 

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) 

662 

663 container += literalBlock 

664 

665 return [container]