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

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.Coverage, "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 tableRow = nodes.row("", classes=[ 

351 "report-package", 

352 self._ConvertToColor(packageCoverage.Coverage, "class") 

353 ]) 

354 tableBody += tableRow 

355 

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

368 

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

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

371 

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 

378 

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

391 

392 def _CreatePages(self) -> None: 

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

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

395 if handlePackage(pack): 

396 return 

397 

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

399 if handleModule(module): 

400 return 

401 

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

403 doc = new_document("dummy") 

404 

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

406 doc += rootSection 

407 

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

409 rootSection += title 

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

411 

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

413 self.env.titles[docname] = title 

414 self.env.longtitles[docname] = title 

415 

416 return 

417 

418 handlePackage(self._coverage) 

419 

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

421 container = nodes.container() 

422 

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) 

428 

429 # Assemble a list of Python source files 

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

431 self._coverage = analyzer.Convert() 

432 # self._coverage.Aggregate() 

433 

434 self._CreatePages() 

435 

436 container += self._GenerateCoverageTable() 

437 

438 def foo(): 

439 docName = self.env.docname 

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

441 

442 subnode = toctree() 

443 subnode['parent'] = docName 

444 

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) 

457 

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

459 wrappernode.append(subnode) 

460 self.add_name(wrappernode) 

461 

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 

481 

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

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

484 

485 return [container] #, wrappernode] 

486 

487 

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 

496 

497 option_spec = CodeCoverageBase.option_spec | { 

498 "style": strip 

499 } 

500 

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

502 

503 _style: LegendStyle 

504 

505 def _CheckOptions(self) -> None: 

506 # Parse all directive options or use default values 

507 super()._CheckOptions() 

508 

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

510 

511 try: 

512 packageConfiguration = self._packageConfigurations[self._reportID] 

513 except KeyError as ex: 

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

515 

516 self._levels = packageConfiguration["levels"] 

517 

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

523 

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

525 tableBody = nodes.tbody() 

526 tableGroup += tableBody 

527 

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

534 

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

541 

542 return table 

543 

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 ) 

552 

553 tableBody = nodes.tbody() 

554 tableGroup += tableBody 

555 

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 ) 

564 

565 return table 

566 

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

568 container = nodes.container() 

569 

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) 

575 

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

585 

586 return [container] 

587 

588 

589 

590@export 

591class ModuleCoverage(CodeCoverageBase): 

592 """ 

593 This directive will be replaced by highlighted source code. 

594 """ 

595 directiveName: str = "module-coverage" 

596 

597 has_content = False 

598 required_arguments = 0 

599 optional_arguments = 2 

600 

601 option_spec = CodeCoverageBase.option_spec | { 

602 "module": strip 

603 } 

604 

605 _packageName: str 

606 _moduleName: str 

607 _jsonReport: Path 

608 

609 def _CheckOptions(self) -> None: 

610 """ 

611 Parse all directive options or use default values. 

612 """ 

613 super()._CheckOptions() 

614 

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

616 

617 try: 

618 packageConfiguration = self._packageConfigurations[self._reportID] 

619 except KeyError as ex: 

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

621 

622 self._packageName = packageConfiguration["name"] 

623 self._jsonReport = packageConfiguration["json_report"] 

624 

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

626 container = nodes.container() 

627 

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) 

633 

634 # Assemble a list of Python source files 

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

636 self._coverage = analyzer.Convert() 

637 

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

639 

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

641 

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

643 # tokens = lex(code, lexer) 

644 

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

646 # highlight() 

647 

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) 

651 

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

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

654 

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) 

660 

661 container += literalBlock 

662 

663 return [container]