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

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 

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, BaseDirective 

48from sphinx_reports.DataModel.CodeCoverage import PackageCoverage, Coverage, ModuleCoverage 

49from sphinx_reports.Adapter.Coverage import Analyzer 

50 

51 

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

57 

58 

59@export 

60class CodeCoverageBase(BaseDirective): 

61 

62 option_spec = { 

63 "class": strip, 

64 "reportid": strip 

65 } 

66 

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 } 

84 

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. 

90 

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

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

93 

94 _cssClasses: List[str] 

95 _reportID: str 

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

97 

98 def _CheckOptions(self) -> None: 

99 """ 

100 Parse all directive options or use default values. 

101 """ 

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

103 

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

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

106 

107 @classmethod 

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

109 """ 

110 Check configuration fields and load necessary values. 

111 

112 :param sphinxApplication: Sphinx application instance. 

113 :param sphinxConfiguration: Sphinx configuration instance. 

114 """ 

115 cls._CheckLevelsConfiguration(sphinxConfiguration) 

116 cls._CheckPackagesConfiguration(sphinxConfiguration) 

117 

118 @classmethod 

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

120 """ 

121 Read code coverage report files. 

122 

123 :param sphinxApplication: Sphinx application instance. 

124 """ 

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

126 

127 @classmethod 

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

129 from sphinx_reports import ReportDomain 

130 

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

132 

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 

137 

138 if "default" not in coverageLevelDefinitions: 

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

140 

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

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

143 

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

148 

149 cls._coverageLevelDefinitions[key] = {} 

150 

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 

160 

161 try: 

162 cssClass = levelConfig["class"] 

163 except KeyError as ex: 

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

165 

166 try: 

167 description = levelConfig["desc"] 

168 except KeyError as ex: 

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

170 

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

172 "class": cssClass, 

173 "desc": description 

174 } 

175 

176 @classmethod 

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

178 from sphinx_reports import ReportDomain 

179 

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

181 

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 

186 

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 

191 

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

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

194 

195 try: 

196 packageName = packageConfiguration["name"] 

197 except KeyError as ex: 

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

199 

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 

204 

205 if not jsonReport.exists(): 

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

207 

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 

214 

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

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

217 

218 try: 

219 levels = packageConfiguration["levels"] 

220 except KeyError as ex: 

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

222 

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

234 

235 levelDefinition = {} 

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

237 pass 

238 else: 

239 raise ReportExtensionError(f"") 

240 

241 cls._packageConfigurations[reportID] = { 

242 "name": packageName, 

243 "json_report": jsonReport, 

244 "fail_below": failBelow, 

245 "levels": levelDefinition 

246 } 

247 

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

249 if currentLevel < 0.0: 

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

251 

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

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

254 return levelConfig[configKey] 

255 

256 return self._levels[100][configKey] 

257 

258 

259@export 

260class CodeCoverage(CodeCoverageBase): 

261 """ 

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

263 """ 

264 directiveName: str = "code-coverage" 

265 

266 has_content = False 

267 required_arguments = 0 

268 optional_arguments = CodeCoverageBase.optional_arguments + 1 

269 

270 option_spec = CodeCoverageBase.option_spec | { 

271 "no-branch-coverage": flag 

272 } 

273 

274 _noBranchCoverage: bool 

275 _packageName: str 

276 _jsonReport: Path 

277 _failBelow: float 

278 _coverage: PackageCoverage 

279 

280 def _CheckOptions(self) -> None: 

281 """ 

282 Parse all directive options or use default values. 

283 """ 

284 super()._CheckOptions() 

285 

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

287 

288 try: 

289 packageConfiguration = self._packageConfigurations[self._reportID] 

290 except KeyError as ex: 

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

292 

293 self._packageName = packageConfiguration["name"] 

294 self._jsonReport = packageConfiguration["json_report"] 

295 self._failBelow = packageConfiguration["fail_below"] 

296 self._levels = packageConfiguration["levels"] 

297 

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

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

300 cssClasses.extend(self._cssClasses) 

301 

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 ] 

309 

310 if self._noBranchCoverage: 

311 columns.pop(2) 

312 

313 table, tableGroup = self._CreateTableHeader( 

314 identifier=self._reportID, 

315 columns=columns, 

316 classes=cssClasses 

317 ) 

318 tableBody = nodes.tbody() 

319 tableGroup += tableBody 

320 

321 self.renderlevel(tableBody, self._coverage) 

322 

323 # Add a summary row 

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

325 "report-summary", 

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

327 ]) 

328 tableBody += tableRow 

329 

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

342 

343 return table 

344 

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

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

347 yield d[key] 

348 

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 

356 

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

369 

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

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

372 

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 

379 

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

392 

393 def _CreatePages(self) -> None: 

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

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

396 if handlePackage(pack): 

397 return 

398 

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

400 if handleModule(module): 

401 return 

402 

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

404 doc = new_document("dummy") 

405 

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

407 doc += rootSection 

408 

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

410 rootSection += title 

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

412 

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

414 self.env.titles[docname] = title 

415 self.env.longtitles[docname] = title 

416 

417 return 

418 

419 handlePackage(self._coverage) 

420 

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

422 container = nodes.container() 

423 

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) 

429 

430 # Assemble a list of Python source files 

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

432 self._coverage = analyzer.Convert() 

433 # self._coverage.Aggregate() 

434 

435 self._CreatePages() 

436 

437 container += self._GenerateCoverageTable() 

438 

439 def foo(): 

440 docName = self.env.docname 

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

442 

443 subnode = toctree() 

444 subnode['parent'] = docName 

445 

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) 

458 

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

460 wrappernode.append(subnode) 

461 self.add_name(wrappernode) 

462 

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 

482 

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

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

485 

486 return [container] #, wrappernode] 

487 

488 

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 

497 

498 option_spec = CodeCoverageBase.option_spec | { 

499 "style": strip 

500 } 

501 

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

503 

504 _style: LegendStyle 

505 

506 def _CheckOptions(self) -> None: 

507 # Parse all directive options or use default values 

508 super()._CheckOptions() 

509 

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

511 

512 try: 

513 packageConfiguration = self._packageConfigurations[self._reportID] 

514 except KeyError as ex: 

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

516 

517 self._levels = packageConfiguration["levels"] 

518 

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

524 

525 table, tableGroup = self._CreateTableHeader(columns, identifier=identifier, classes=classes) 

526 tableBody = nodes.tbody() 

527 tableGroup += tableBody 

528 

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

535 

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

542 

543 return table 

544 

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 ) 

553 

554 tableBody = nodes.tbody() 

555 tableGroup += tableBody 

556 

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 ) 

565 

566 return table 

567 

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

569 container = nodes.container() 

570 

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) 

576 

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

586 

587 return [container] 

588 

589 

590 

591@export 

592class ModuleCoverage(CodeCoverageBase): 

593 """ 

594 This directive will be replaced by highlighted source code. 

595 """ 

596 directiveName: str = "module-coverage" 

597 

598 has_content = False 

599 required_arguments = 0 

600 optional_arguments = 2 

601 

602 option_spec = CodeCoverageBase.option_spec | { 

603 "module": strip 

604 } 

605 

606 _packageName: str 

607 _moduleName: str 

608 _jsonReport: Path 

609 

610 def _CheckOptions(self) -> None: 

611 """ 

612 Parse all directive options or use default values. 

613 """ 

614 super()._CheckOptions() 

615 

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

617 

618 try: 

619 packageConfiguration = self._packageConfigurations[self._reportID] 

620 except KeyError as ex: 

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

622 

623 self._packageName = packageConfiguration["name"] 

624 self._jsonReport = packageConfiguration["json_report"] 

625 

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

627 container = nodes.container() 

628 

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) 

634 

635 # Assemble a list of Python source files 

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

637 self._coverage = analyzer.Convert() 

638 

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

640 

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

642 

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

644 # tokens = lex(code, lexer) 

645 

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

647 # highlight() 

648 

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) 

652 

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

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

655 

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) 

661 

662 container += literalBlock 

663 

664 return [container]