Coverage for pyEDAA/Reports/CodeCoverage/CoveragePy.py: 90%

489 statements  

« prev     ^ index     » next       coverage.py v7.17.3, created at 2026-11-14 02:26 +0000

1# ==================================================================================================================== # 

2# _____ ____ _ _ ____ _ # 

3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # 

4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # 

5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # 

6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # 

7# |_| |___/ |_| # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2021-2026 Electronic Design Automation Abstraction (EDA²) # 

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

32coverage.py's JSON code coverage format: a model of the format, read from a report and converted to the common model. 

33 

34coverage.py writes the format with ``coverage json``. A report is validated against the JSON Schema of the format 

35version it states (:class:`FormatVersion`), reverse-engineered from coverage.py: :file:`CoveragePy-2.schema.json` for 

36format 2 (coverage.py 7.4.1 to 7.5), :file:`CoveragePy-3.schema.json` for format 3 (coverage.py 7.6 and later). The 

37format's model keeps what the report states: a :class:`Document` holds :class:`File` records, a file - in format 3 - 

38:class:`Region` records of its functions and classes, and each its lines, branches and :class:`Summary`. Each 

39record's constructor takes typed values, so the model can be built by hand: a file or a region names its parent with 

40the keyword parameter ``parent`` and is added to it. Its class method ``Parse`` reads the record's JSON object. 

41 

42:meth:`Document.ToCoverageSummary` converts the model to the common model of :mod:`pyEDAA.Reports.CodeCoverage`: 

43 

44* A file's path is relative to the directory coverage.py ran in. 

45* Executed lines are covered - partially covered, if one of their branches wasn't taken -, missing lines uncovered, 

46 excluded lines excluded. The format has no counts. 

47* A branch is a pair of source and destination line; it becomes a branch of its source line, naming its target line - 

48 none for an exit of a function, which coverage.py states as a negative number. 

49* A file's directories become packages, the file a module spanning the whole file; its classes and functions - named by 

50 qualified names like ``Circle.Area`` - become classes, methods and functions, each spanning its ``class`` or ``def`` 

51 line to its last line. coverage.py's own summary of a function counts the ``def`` line for the enclosing scope. 

52 

53.. rubric:: Example 

54 

55.. code-block:: Python 

56 

57 from pathlib import Path 

58 from pyEDAA.Reports.CodeCoverage.CoveragePy import Document 

59 

60 report = Document(Path("coverage.json"), analyzeAndConvert=True) 

61 summary = report.ToCoverageSummary() 

62 print(f"{summary.FileCount} files: {summary.Coverage:.1%}") 

63""" 

64from __future__ import annotations 

65 

66from datetime import datetime 

67from enum import Enum, IntEnum 

68from json import JSONDecodeError, loads 

69from pathlib import Path 

70from typing import Any, Iterable, Optional as Nullable, Self 

71 

72from pyTooling.Common import getFullyQualifiedName, readResourceFile 

73from pyTooling.Decorators import export, readonly 

74from pyTooling.Exceptions import ToolingException 

75from pyTooling.MetaClasses import ExtendedType, abstractclass 

76from pyTooling.Stopwatch import Stopwatch 

77from pyTooling.Versioning import SemanticVersion 

78 

79from pyEDAA.Reports import Resources 

80from pyEDAA.Reports.CodeCoverage import Branch as cc_Branch, Class as cc_Class, CodeCoverageError, CoverageSummary, \ 

81 Document as cc_Document, File as cc_File, Function as cc_Function, \ 

82 Line as cc_Line, LineCoverageStatus, Method as cc_Method, Module as cc_Module, \ 

83 Package as cc_Package, Unit as cc_Unit 

84 

85 

86__all__ = ["SCHEMAS"] 

87 

88# A class with a property named like a class - ``Path``, ``Summary`` - can't name that class in the annotation of a 

89# field: the class body's namespace, where annotations are evaluated, binds the name to the property. 

90_Path = Path 

91 

92 

93@export 

94class FormatVersion(IntEnum): 

95 """ 

96 Version of coverage.py's JSON report format, as a report's ``meta.format`` states it. 

97 

98 coverage.py 7.4.1 added ``meta.format`` with format 2. coverage.py 7.6 and later write format 3, which adds the 

99 functions and classes of a file. A report of an earlier coverage.py states no format version and isn't supported. 

100 """ 

101 

102 Version2 = 2 #: Format 2, written by coverage.py 7.4.1 to 7.5. 

103 Version3 = 3 #: Format 3, written by coverage.py 7.6 and later. 

104 

105 @classmethod 

106 def Parse(cls, value: int) -> Self: 

107 """ 

108 Convert the version, as a report's ``meta.format`` states it, to the member of that version. 

109 

110 :param value: The version. 

111 :returns: The member of that version. 

112 :raises ValueError: If parameter ``value`` is ``None``. 

113 :raises TypeError: If parameter ``value`` is not of type :class:`int`. 

114 :raises ValueError: If parameter ``value`` is not a supported format version. 

115 """ 

116 if value is None: 

117 raise ValueError("Parameter 'value' is None.") 

118 elif not isinstance(value, int) or isinstance(value, bool): 

119 ex = TypeError("Parameter 'value' is not of type 'int'.") 

120 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.") 

121 raise ex 

122 elif value not in [member.value for member in cls]: 

123 ex = ValueError("Parameter 'value' is not a supported coverage.py JSON format version.") 

124 ex.add_note(f"Got value '{value}'.") 

125 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in cls)}.") 

126 raise ex 

127 

128 return cls(value) 

129 

130 

131SCHEMAS: dict[FormatVersion, str] = { 

132 FormatVersion.Version2: "CoveragePy-2.schema.json", 

133 FormatVersion.Version3: "CoveragePy-3.schema.json" 

134} #: Per format version, the JSON Schema a report of that version is validated against. 

135 

136 

137@export 

138class Summary(metaclass=ExtendedType, slots=True): 

139 """ 

140 A ``summary``: the counters coverage.py computed for the whole report, a file or a region. 

141 

142 coverage.py measures lines. What it calls a statement (``num_statements``) is a line number: a statement spanning 

143 several lines counts by its first line, several statements on one line count once. 

144 """ 

145 

146 _lineCount: int #: Number of executable lines, without the excluded ones. 

147 _coveredLineCount: int #: Number of executed lines. 

148 _missingLineCount: int #: Number of executable lines, which never ran. 

149 _excludedLineCount: int #: Number of excluded lines. 

150 _percentCovered: float #: Coverage of lines and branches, in percent. 

151 _branchCount: Nullable[int] #: Number of branches, if branch coverage was measured. 

152 _coveredBranchCount: Nullable[int] #: Number of taken branches, if branch coverage was measured. 

153 _partialBranchCount: Nullable[int] #: Number of branches never taken from executed lines, if branches were measured. 

154 

155 def __init__( 

156 self, 

157 lineCount: int, 

158 coveredLineCount: int, 

159 missingLineCount: int, 

160 excludedLineCount: int, 

161 percentCovered: float, 

162 branchCount: Nullable[int] = None, 

163 coveredBranchCount: Nullable[int] = None, 

164 partialBranchCount: Nullable[int] = None 

165 ) -> None: 

166 """ 

167 Initialize the summary from its counters. 

168 

169 :param lineCount: Number of executable lines, without the excluded ones. 

170 :param coveredLineCount: Number of executed lines. 

171 :param missingLineCount: Number of executable lines, which never ran. 

172 :param excludedLineCount: Number of excluded lines. 

173 :param percentCovered: Coverage of lines and branches, in percent. 

174 :param branchCount: Optional, number of branches. Default: ``None`` (branch coverage wasn't measured). 

175 :param coveredBranchCount: Optional, number of taken branches. Default: ``None`` (branch coverage wasn't measured). 

176 :param partialBranchCount: Optional, number of branches never taken from executed lines. |br| 

177 Default: ``None`` (branch coverage wasn't measured). 

178 :raises ValueError: If parameter ``lineCount`` is ``None``. 

179 :raises TypeError: If parameter ``lineCount`` is not of type :class:`int`. 

180 :raises ValueError: If parameter ``lineCount`` is negative. 

181 :raises ValueError: If parameter ``coveredLineCount`` is ``None``. 

182 :raises TypeError: If parameter ``coveredLineCount`` is not of type :class:`int`. 

183 :raises ValueError: If parameter ``coveredLineCount`` is negative. 

184 :raises ValueError: If parameter ``missingLineCount`` is ``None``. 

185 :raises TypeError: If parameter ``missingLineCount`` is not of type :class:`int`. 

186 :raises ValueError: If parameter ``missingLineCount`` is negative. 

187 :raises ValueError: If parameter ``excludedLineCount`` is ``None``. 

188 :raises TypeError: If parameter ``excludedLineCount`` is not of type :class:`int`. 

189 :raises ValueError: If parameter ``excludedLineCount`` is negative. 

190 :raises ValueError: If parameter ``percentCovered`` is ``None``. 

191 :raises TypeError: If parameter ``percentCovered`` is not of type :class:`float`. 

192 :raises ValueError: If parameter ``percentCovered`` is out of range 0..100. 

193 :raises TypeError: If parameter ``branchCount`` is not of type :class:`int`. 

194 :raises ValueError: If parameter ``branchCount`` is negative. 

195 :raises TypeError: If parameter ``coveredBranchCount`` is not of type :class:`int`. 

196 :raises ValueError: If parameter ``coveredBranchCount`` is negative. 

197 :raises TypeError: If parameter ``partialBranchCount`` is not of type :class:`int`. 

198 :raises ValueError: If parameter ``partialBranchCount`` is negative. 

199 """ 

200 for name, count in ( 

201 ("lineCount", lineCount), 

202 ("coveredLineCount", coveredLineCount), 

203 ("missingLineCount", missingLineCount), 

204 ("excludedLineCount", excludedLineCount) 

205 ): 

206 if count is None: 

207 raise ValueError(f"Parameter '{name}' is None.") 

208 elif not isinstance(count, int): 

209 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

210 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") 

211 raise ex 

212 elif count < 0: 212 ↛ 213line 212 didn't jump to line 213 because the condition on line 212 was never true

213 ex = ValueError(f"Parameter '{name}' is negative.") 

214 ex.add_note(f"Got value '{count}'.") 

215 raise ex 

216 

217 if percentCovered is None: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 raise ValueError("Parameter 'percentCovered' is None.") 

219 elif not isinstance(percentCovered, (int, float)): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true

220 ex = TypeError("Parameter 'percentCovered' is not of type 'float'.") 

221 ex.add_note(f"Got type '{getFullyQualifiedName(percentCovered)}'.") 

222 raise ex 

223 elif not 0 <= percentCovered <= 100: 

224 ex = ValueError("Parameter 'percentCovered' is out of range 0..100.") 

225 ex.add_note(f"Got value '{percentCovered}'.") 

226 raise ex 

227 

228 for name, count in ( 

229 ("branchCount", branchCount), 

230 ("coveredBranchCount", coveredBranchCount), 

231 ("partialBranchCount", partialBranchCount) 

232 ): 

233 if count is None: 

234 continue 

235 elif not isinstance(count, int): 235 ↛ 236line 235 didn't jump to line 236 because the condition on line 235 was never true

236 ex = TypeError(f"Parameter '{name}' is not of type 'int'.") 

237 ex.add_note(f"Got type '{getFullyQualifiedName(count)}'.") 

238 raise ex 

239 elif count < 0: 

240 ex = ValueError(f"Parameter '{name}' is negative.") 

241 ex.add_note(f"Got value '{count}'.") 

242 raise ex 

243 

244 self._lineCount = lineCount 

245 self._coveredLineCount = coveredLineCount 

246 self._missingLineCount = missingLineCount 

247 self._excludedLineCount = excludedLineCount 

248 self._percentCovered = percentCovered 

249 self._branchCount = branchCount 

250 self._coveredBranchCount = coveredBranchCount 

251 self._partialBranchCount = partialBranchCount 

252 

253 @classmethod 

254 def Parse(cls, summary: dict[str, Any]) -> Self: 

255 """ 

256 Read a summary from its JSON object. 

257 

258 :param summary: The JSON object ``summary`` or ``totals``. 

259 :returns: The summary. 

260 :raises ValueError: If parameter ``summary`` is ``None``. 

261 :raises TypeError: If parameter ``summary`` is not of type :class:`dict`. 

262 """ 

263 if summary is None: 

264 raise ValueError("Parameter 'summary' is None.") 

265 elif not isinstance(summary, dict): 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true

266 ex = TypeError("Parameter 'summary' is not of type 'dict'.") 

267 ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.") 

268 raise ex 

269 

270 return cls( 

271 summary["num_statements"], 

272 summary["covered_lines"], 

273 summary["missing_lines"], 

274 summary["excluded_lines"], 

275 summary["percent_covered"], 

276 summary.get("num_branches"), 

277 summary.get("covered_branches"), 

278 summary.get("num_partial_branches") 

279 ) 

280 

281 @readonly 

282 def LineCount(self) -> int: 

283 """ 

284 Read-only property to access the number of executable lines, without the excluded ones (:attr:`_lineCount`). 

285 

286 coverage.py states it as ``num_statements``. 

287 

288 :returns: The number of executable lines. 

289 """ 

290 return self._lineCount 

291 

292 @readonly 

293 def CoveredLineCount(self) -> int: 

294 """ 

295 Read-only property to access the number of executed lines (:attr:`_coveredLineCount`). 

296 

297 :returns: The number of executed lines. 

298 """ 

299 return self._coveredLineCount 

300 

301 @readonly 

302 def MissingLineCount(self) -> int: 

303 """ 

304 Read-only property to access the number of executable lines, which never ran (:attr:`_missingLineCount`). 

305 

306 :returns: The number of missing lines. 

307 """ 

308 return self._missingLineCount 

309 

310 @readonly 

311 def ExcludedLineCount(self) -> int: 

312 """ 

313 Read-only property to access the number of excluded lines (:attr:`_excludedLineCount`). 

314 

315 :returns: The number of excluded lines. 

316 """ 

317 return self._excludedLineCount 

318 

319 @readonly 

320 def PercentCovered(self) -> float: 

321 """ 

322 Read-only property to access the coverage of lines and branches (:attr:`_percentCovered`). 

323 

324 :returns: The coverage in percent. 

325 """ 

326 return self._percentCovered 

327 

328 @readonly 

329 def BranchCount(self) -> Nullable[int]: 

330 """ 

331 Read-only property to access the number of branches (:attr:`_branchCount`). 

332 

333 :returns: The number of branches, or ``None`` if branch coverage wasn't measured. 

334 """ 

335 return self._branchCount 

336 

337 @readonly 

338 def CoveredBranchCount(self) -> Nullable[int]: 

339 """ 

340 Read-only property to access the number of taken branches (:attr:`_coveredBranchCount`). 

341 

342 :returns: The number of taken branches, or ``None`` if branch coverage wasn't measured. 

343 """ 

344 return self._coveredBranchCount 

345 

346 @readonly 

347 def PartialBranchCount(self) -> Nullable[int]: 

348 """ 

349 Read-only property to access the number of branches never taken from executed lines (:attr:`_partialBranchCount`). 

350 

351 :returns: The number of branches, which were never taken although their line ran, or ``None`` if branch coverage 

352 wasn't measured. 

353 """ 

354 return self._partialBranchCount 

355 

356 

357_Summary = Summary 

358 

359 

360@export 

361@abstractclass 

362class Base(metaclass=ExtendedType, slots=True): 

363 """ 

364 Base-class of a file and a region: its executed, missing and excluded lines, its branches, and its summary. 

365 """ 

366 

367 _executedLines: list[int] #: The executed lines. 

368 _missingLines: list[int] #: The lines, which never ran. 

369 _excludedLines: list[int] #: The excluded lines. 

370 _executedBranches: list[tuple[int, int]] #: The taken branches, as pairs of source and destination line. 

371 _missingBranches: list[tuple[int, int]] #: The branches never taken, as pairs of source and destination line. 

372 _summary: _Summary #: The counters coverage.py computed. 

373 

374 def __init__( 

375 self, 

376 summary: Summary, 

377 executedLines: Nullable[Iterable[int]] = None, 

378 missingLines: Nullable[Iterable[int]] = None, 

379 excludedLines: Nullable[Iterable[int]] = None, 

380 executedBranches: Nullable[Iterable[tuple[int, int]]] = None, 

381 missingBranches: Nullable[Iterable[tuple[int, int]]] = None 

382 ) -> None: 

383 """ 

384 Initialize the summary, the lines and the branches. 

385 

386 :param summary: The counters coverage.py computed. 

387 :param executedLines: Optional, the executed lines. Default: ``None`` (none). 

388 :param missingLines: Optional, the lines, which never ran. Default: ``None`` (none). 

389 :param excludedLines: Optional, the excluded lines. Default: ``None`` (none). 

390 :param executedBranches: Optional, the taken branches, as pairs of source and destination line. |br| 

391 Default: ``None`` (none, or branch coverage wasn't measured). 

392 :param missingBranches: Optional, the branches never taken, as pairs of source and destination line. |br| 

393 Default: ``None`` (none, or branch coverage wasn't measured). 

394 :raises ValueError: If parameter ``summary`` is ``None``. 

395 :raises TypeError: If parameter ``summary`` is not of type :class:`Summary`. 

396 :raises TypeError: If parameter ``executedLines`` is not iterable. 

397 :raises TypeError: If an element of parameter ``executedLines`` is not of type :class:`int`. 

398 :raises TypeError: If parameter ``missingLines`` is not iterable. 

399 :raises TypeError: If an element of parameter ``missingLines`` is not of type :class:`int`. 

400 :raises TypeError: If parameter ``excludedLines`` is not iterable. 

401 :raises TypeError: If an element of parameter ``excludedLines`` is not of type :class:`int`. 

402 :raises TypeError: If parameter ``executedBranches`` is not iterable. 

403 :raises TypeError: If an element of parameter ``executedBranches`` is not a pair of :class:`int`. 

404 :raises TypeError: If parameter ``missingBranches`` is not iterable. 

405 :raises TypeError: If an element of parameter ``missingBranches`` is not a pair of :class:`int`. 

406 """ 

407 if summary is None: 

408 raise ValueError("Parameter 'summary' is None.") 

409 elif not isinstance(summary, Summary): 409 ↛ 410line 409 didn't jump to line 410 because the condition on line 409 was never true

410 ex = TypeError("Parameter 'summary' is not of type 'Summary'.") 

411 ex.add_note(f"Got type '{getFullyQualifiedName(summary)}'.") 

412 raise ex 

413 

414 lineLists: list[list[int]] = [] 

415 for name, numbers in ( 

416 ("executedLines", executedLines), 

417 ("missingLines", missingLines), 

418 ("excludedLines", excludedLines) 

419 ): 

420 lineList: list[int] = [] 

421 if numbers is not None: 

422 if not isinstance(numbers, Iterable): 422 ↛ 423line 422 didn't jump to line 423 because the condition on line 422 was never true

423 ex = TypeError(f"Parameter '{name}' is not iterable.") 

424 ex.add_note(f"Got type '{getFullyQualifiedName(numbers)}'.") 

425 raise ex 

426 

427 for number in numbers: 

428 if not isinstance(number, int): 

429 ex = TypeError(f"An element of parameter '{name}' is not of type 'int'.") 

430 ex.add_note(f"Got type '{getFullyQualifiedName(number)}'.") 

431 raise ex 

432 

433 lineList.append(number) 

434 lineLists.append(lineList) 

435 

436 branchLists: list[list[tuple[int, int]]] = [] 

437 for name, branches in ( 

438 ("executedBranches", executedBranches), 

439 ("missingBranches", missingBranches) 

440 ): 

441 branchList: list[tuple[int, int]] = [] 

442 if branches is not None: 

443 if not isinstance(branches, Iterable): 443 ↛ 444line 443 didn't jump to line 444 because the condition on line 443 was never true

444 ex = TypeError(f"Parameter '{name}' is not iterable.") 

445 ex.add_note(f"Got type '{getFullyQualifiedName(branches)}'.") 

446 raise ex 

447 

448 for branch in branches: 

449 if not (isinstance(branch, tuple) and len(branch) == 2 and all(isinstance(line, int) for line in branch)): 

450 ex = TypeError(f"An element of parameter '{name}' is not a pair of 'int'.") 

451 ex.add_note(f"Got '{branch!r}' of type '{getFullyQualifiedName(branch)}'.") 

452 raise ex 

453 

454 branchList.append(branch) 

455 branchLists.append(branchList) 

456 

457 self._executedLines, self._missingLines, self._excludedLines = lineLists 

458 self._executedBranches, self._missingBranches = branchLists 

459 self._summary = summary 

460 

461 @readonly 

462 def ExecutedLines(self) -> list[int]: 

463 """ 

464 Read-only property to access the executed lines (:attr:`_executedLines`). 

465 

466 :returns: The line numbers. 

467 """ 

468 return self._executedLines 

469 

470 @readonly 

471 def MissingLines(self) -> list[int]: 

472 """ 

473 Read-only property to access the lines, which never ran (:attr:`_missingLines`). 

474 

475 :returns: The line numbers. 

476 """ 

477 return self._missingLines 

478 

479 @readonly 

480 def ExcludedLines(self) -> list[int]: 

481 """ 

482 Read-only property to access the excluded lines (:attr:`_excludedLines`). 

483 

484 :returns: The line numbers. 

485 """ 

486 return self._excludedLines 

487 

488 @readonly 

489 def ExecutedBranches(self) -> list[tuple[int, int]]: 

490 """ 

491 Read-only property to access the taken branches (:attr:`_executedBranches`). 

492 

493 :returns: The branches as pairs of source and destination line; empty, if branch coverage wasn't measured. 

494 """ 

495 return self._executedBranches 

496 

497 @readonly 

498 def MissingBranches(self) -> list[tuple[int, int]]: 

499 """ 

500 Read-only property to access the branches never taken (:attr:`_missingBranches`). 

501 

502 :returns: The branches as pairs of source and destination line; empty, if branch coverage wasn't measured. 

503 """ 

504 return self._missingBranches 

505 

506 @readonly 

507 def Summary(self) -> Summary: 

508 """ 

509 Read-only property to access the counters coverage.py computed (:attr:`_summary`). 

510 

511 :returns: The summary. 

512 """ 

513 return self._summary 

514 

515 @readonly 

516 def AllLines(self) -> list[int]: 

517 """ 

518 Read-only property to return the executed, missing and excluded lines. 

519 

520 :returns: The line numbers, sorted. 

521 """ 

522 return sorted({*self._executedLines, *self._missingLines, *self._excludedLines}) 

523 

524 

525@export 

526class RegionKind(Enum): 

527 """ 

528 Kind of a region of a file: a function or a class. 

529 

530 A file states its regions of each kind in their own JSON object: ``functions`` and ``classes``. 

531 """ 

532 

533 Function = 1 #: A function or a method, listed in ``functions``. 

534 Class = 2 #: A class, listed in ``classes``. 

535 

536 

537@export 

538class Region(Base): 

539 """ 

540 A function or a class of a file - in format 3 -, named by its qualified name. 

541 

542 A method ``Area`` of a class ``Circle`` is named ``Circle.Area``. 

543 """ 

544 

545 _parent: Nullable[File] #: The file the region belongs to. 

546 _name: str #: Qualified name of the function or class. 

547 _kind: RegionKind #: Kind of the region: a function or a class. 

548 _startLine: int #: The region's first line. 

549 

550 def __init__( 

551 self, 

552 name: str, 

553 kind: RegionKind, 

554 startLine: int, 

555 summary: Summary, 

556 executedLines: Nullable[Iterable[int]] = None, 

557 missingLines: Nullable[Iterable[int]] = None, 

558 excludedLines: Nullable[Iterable[int]] = None, 

559 executedBranches: Nullable[Iterable[tuple[int, int]]] = None, 

560 missingBranches: Nullable[Iterable[tuple[int, int]]] = None, 

561 *, 

562 parent: Nullable[File] = None 

563 ) -> None: 

564 """ 

565 Initialize the region from its name, its kind, its first line, its summary, its lines and its branches, and add it 

566 to the functions or classes of its file. 

567 

568 :param name: Qualified name of the function or class, e.g. ``Circle.Area``. 

569 :param kind: Kind of the region: a function or a class. 

570 :param startLine: The region's first line. 

571 :param summary: The counters coverage.py computed. 

572 :param executedLines: Optional, the executed lines. Default: ``None`` (none). 

573 :param missingLines: Optional, the lines, which never ran. Default: ``None`` (none). 

574 :param excludedLines: Optional, the excluded lines. Default: ``None`` (none). 

575 :param executedBranches: Optional, the taken branches, as pairs of source and destination line. |br| 

576 Default: ``None`` (none, or branch coverage wasn't measured). 

577 :param missingBranches: Optional, the branches never taken, as pairs of source and destination line. |br| 

578 Default: ``None`` (none, or branch coverage wasn't measured). 

579 :param parent: Optional, the file the region belongs to; the region is added to its functions or classes 

580 by :attr:`Name`, as its :attr:`Kind` says. Default: ``None``. 

581 :raises ValueError: If parameter ``name`` is ``None``. 

582 :raises TypeError: If parameter ``name`` is not of type :class:`str`. 

583 :raises ValueError: If parameter ``name`` is empty. 

584 :raises ValueError: If parameter ``kind`` is ``None``. 

585 :raises TypeError: If parameter ``kind`` is not of type :class:`RegionKind`. 

586 :raises ValueError: If parameter ``startLine`` is ``None``. 

587 :raises TypeError: If parameter ``startLine`` is not of type :class:`int`. 

588 :raises ValueError: If parameter ``startLine`` is less than 1. 

589 :raises TypeError: If parameter ``parent`` is not of type :class:`File`. 

590 :raises ValueError: If parameter ``parent`` contains a region of the same kind and name already. 

591 """ 

592 super().__init__(summary, executedLines, missingLines, excludedLines, executedBranches, missingBranches) 

593 

594 if name is None: 594 ↛ 595line 594 didn't jump to line 595 because the condition on line 594 was never true

595 raise ValueError("Parameter 'name' is None.") 

596 elif not isinstance(name, str): 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

597 ex = TypeError("Parameter 'name' is not of type 'str'.") 

598 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

599 raise ex 

600 elif name == "": 

601 raise ValueError("Parameter 'name' is empty.") 

602 

603 if kind is None: 603 ↛ 604line 603 didn't jump to line 604 because the condition on line 603 was never true

604 raise ValueError("Parameter 'kind' is None.") 

605 elif not isinstance(kind, RegionKind): 

606 ex = TypeError("Parameter 'kind' is not of type 'RegionKind'.") 

607 ex.add_note(f"Got type '{getFullyQualifiedName(kind)}'.") 

608 raise ex 

609 

610 if startLine is None: 610 ↛ 611line 610 didn't jump to line 611 because the condition on line 610 was never true

611 raise ValueError("Parameter 'startLine' is None.") 

612 elif not isinstance(startLine, int): 612 ↛ 613line 612 didn't jump to line 613 because the condition on line 612 was never true

613 ex = TypeError("Parameter 'startLine' is not of type 'int'.") 

614 ex.add_note(f"Got type '{getFullyQualifiedName(startLine)}'.") 

615 raise ex 

616 elif startLine < 1: 

617 ex = ValueError("Parameter 'startLine' is less than 1.") 

618 ex.add_note(f"Got value '{startLine}'.") 

619 raise ex 

620 

621 if parent is not None: 

622 if not isinstance(parent, File): 

623 ex = TypeError("Parameter 'parent' is not of type 'File'.") 

624 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

625 raise ex 

626 

627 regions = parent._functions if kind is RegionKind.Function else parent._classes 

628 if name in regions: 

629 raise ValueError(f"Parameter 'parent' contains {kind.name.lower()} '{name}' already.") 

630 

631 self._parent = parent 

632 self._name = name 

633 self._kind = kind 

634 self._startLine = startLine 

635 

636 if parent is not None: 

637 regions[name] = self 

638 

639 @classmethod 

640 def Parse(cls, name: str, kind: RegionKind, region: dict[str, Any], *, parent: Nullable[File] = None) -> Self: 

641 """ 

642 Read a region from its JSON object. 

643 

644 :param name: Qualified name of the function or class: the region's key in ``functions`` or ``classes``. 

645 :param kind: Kind of the region: :attr:`RegionKind.Function` in ``functions``, :attr:`RegionKind.Class` in 

646 ``classes``. 

647 :param region: The JSON object of the region. 

648 :param parent: Optional, the file the region belongs to. Default: ``None``. 

649 :returns: The region. 

650 :raises ValueError: If parameter ``region`` is ``None``. 

651 :raises TypeError: If parameter ``region`` is not of type :class:`dict`. 

652 """ 

653 if region is None: 653 ↛ 654line 653 didn't jump to line 654 because the condition on line 653 was never true

654 raise ValueError("Parameter 'region' is None.") 

655 elif not isinstance(region, dict): 

656 ex = TypeError("Parameter 'region' is not of type 'dict'.") 

657 ex.add_note(f"Got type '{getFullyQualifiedName(region)}'.") 

658 raise ex 

659 

660 return cls( 

661 name, 

662 kind, 

663 region["start_line"], 

664 Summary.Parse(region["summary"]), 

665 region["executed_lines"], 

666 region["missing_lines"], 

667 region["excluded_lines"], 

668 (tuple(branch) for branch in region.get("executed_branches", [])), 

669 (tuple(branch) for branch in region.get("missing_branches", [])), 

670 parent=parent 

671 ) 

672 

673 @readonly 

674 def Parent(self) -> Nullable[File]: 

675 """ 

676 Read-only property to access the file the region belongs to (:attr:`_parent`). 

677 

678 :returns: The file; ``None`` if the region belongs to no file. 

679 """ 

680 return self._parent 

681 

682 @readonly 

683 def Name(self) -> str: 

684 """ 

685 Read-only property to access the qualified name of the function or class (:attr:`_name`). 

686 

687 :returns: The qualified name. 

688 """ 

689 return self._name 

690 

691 @readonly 

692 def Kind(self) -> RegionKind: 

693 """ 

694 Read-only property to access the kind of the region (:attr:`_kind`). 

695 

696 :returns: The kind: a function or a class. 

697 """ 

698 return self._kind 

699 

700 @readonly 

701 def StartLine(self) -> int: 

702 """ 

703 Read-only property to access the region's first line (:attr:`_startLine`). 

704 

705 :returns: The line number. 

706 """ 

707 return self._startLine 

708 

709 

710@export 

711class File(Base): 

712 """ 

713 A measured file: its lines, branches and summary, and - in format 3 - its functions and classes. 

714 

715 The regions named ``""`` - the lines outside of every function or class - aren't kept: :meth:`Parse` skips them. 

716 """ 

717 

718 _parent: Nullable[Report] #: The report the file belongs to. 

719 _path: _Path #: The file's path, relative to the directory coverage.py ran in. 

720 _functions: dict[str, Region] #: The functions, by qualified name. 

721 _classes: dict[str, Region] #: The classes, by qualified name. 

722 

723 def __init__( 

724 self, 

725 path: Path, 

726 summary: Summary, 

727 executedLines: Nullable[Iterable[int]] = None, 

728 missingLines: Nullable[Iterable[int]] = None, 

729 excludedLines: Nullable[Iterable[int]] = None, 

730 executedBranches: Nullable[Iterable[tuple[int, int]]] = None, 

731 missingBranches: Nullable[Iterable[tuple[int, int]]] = None, 

732 *, 

733 parent: Nullable[Report] = None 

734 ) -> None: 

735 """ 

736 Initialize the file from its path, its summary, its lines and its branches, and add it to the files of its report. 

737 

738 Its functions and classes are added by creating them with this file as their parent. 

739 

740 :param path: The file's path, relative to the directory coverage.py ran in. 

741 :param summary: The counters coverage.py computed. 

742 :param executedLines: Optional, the executed lines. Default: ``None`` (none). 

743 :param missingLines: Optional, the lines, which never ran. Default: ``None`` (none). 

744 :param excludedLines: Optional, the excluded lines. Default: ``None`` (none). 

745 :param executedBranches: Optional, the taken branches, as pairs of source and destination line. |br| 

746 Default: ``None`` (none, or branch coverage wasn't measured). 

747 :param missingBranches: Optional, the branches never taken, as pairs of source and destination line. |br| 

748 Default: ``None`` (none, or branch coverage wasn't measured). 

749 :param parent: Optional, the report the file belongs to; the file is added to its files by :attr:`Path`. 

750 Default: ``None``. 

751 :raises ValueError: If parameter ``path`` is ``None``. 

752 :raises TypeError: If parameter ``path`` is not of type :class:`~pathlib.Path`. 

753 :raises TypeError: If parameter ``parent`` is not of type :class:`Report`. 

754 :raises ValueError: If parameter ``parent`` contains a file of the same path already. 

755 """ 

756 super().__init__(summary, executedLines, missingLines, excludedLines, executedBranches, missingBranches) 

757 

758 if path is None: 758 ↛ 759line 758 didn't jump to line 759 because the condition on line 758 was never true

759 raise ValueError("Parameter 'path' is None.") 

760 elif not isinstance(path, Path): 

761 ex = TypeError("Parameter 'path' is not of type 'Path'.") 

762 ex.add_note(f"Got type '{getFullyQualifiedName(path)}'.") 

763 raise ex 

764 

765 if parent is not None: 

766 if not isinstance(parent, Report): 

767 ex = TypeError("Parameter 'parent' is not of type 'Report'.") 

768 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

769 raise ex 

770 elif path in parent._files: 

771 raise ValueError(f"Parameter 'parent' contains file '{path.as_posix()}' already.") 

772 

773 self._parent = parent 

774 self._path = path 

775 self._functions = {} 

776 self._classes = {} 

777 

778 if parent is not None: 

779 parent._files[path] = self 

780 

781 @classmethod 

782 def Parse(cls, name: str, file: dict[str, Any], *, parent: Nullable[Report] = None) -> Self: 

783 """ 

784 Read a file, its functions and classes from its JSON object. 

785 

786 The regions named ``""`` - the lines outside of every function or class - are skipped. 

787 

788 :param name: The file's path as the report states it - its key in ``files`` -, relative to the 

789 directory coverage.py ran in; ``\\`` separators become ``/``. 

790 :param file: The JSON object of the file. 

791 :param parent: Optional, the report the file belongs to. Default: ``None``. 

792 :returns: The file. 

793 :raises ValueError: If parameter ``name`` is ``None``. 

794 :raises TypeError: If parameter ``name`` is not of type :class:`str`. 

795 :raises ValueError: If parameter ``file`` is ``None``. 

796 :raises TypeError: If parameter ``file`` is not of type :class:`dict`. 

797 :raises CodeCoverageError: If the report names the file's path twice. 

798 """ 

799 if name is None: 

800 raise ValueError("Parameter 'name' is None.") 

801 elif not isinstance(name, str): 801 ↛ 802line 801 didn't jump to line 802 because the condition on line 801 was never true

802 ex = TypeError("Parameter 'name' is not of type 'str'.") 

803 ex.add_note(f"Got type '{getFullyQualifiedName(name)}'.") 

804 raise ex 

805 

806 if file is None: 806 ↛ 807line 806 didn't jump to line 807 because the condition on line 806 was never true

807 raise ValueError("Parameter 'file' is None.") 

808 elif not isinstance(file, dict): 808 ↛ 809line 808 didn't jump to line 809 because the condition on line 808 was never true

809 ex = TypeError("Parameter 'file' is not of type 'dict'.") 

810 ex.add_note(f"Got type '{getFullyQualifiedName(file)}'.") 

811 raise ex 

812 

813 path = Path(name.replace("\\", "/")) 

814 if parent is not None and path in parent._files: 

815 raise CodeCoverageError(f"coverage.py report names file '{path.as_posix()}' twice.") 

816 

817 measuredFile = cls( 

818 path, 

819 Summary.Parse(file["summary"]), 

820 file["executed_lines"], 

821 file["missing_lines"], 

822 file["excluded_lines"], 

823 (tuple(branch) for branch in file.get("executed_branches", [])), 

824 (tuple(branch) for branch in file.get("missing_branches", [])), 

825 parent=parent 

826 ) 

827 

828 for kind, regions in ((RegionKind.Function, "functions"), (RegionKind.Class, "classes")): 

829 for regionName, region in file.get(regions, {}).items(): 

830 if regionName != "": 

831 Region.Parse(regionName, kind, region, parent=measuredFile) 

832 

833 return measuredFile 

834 

835 @readonly 

836 def Parent(self) -> Nullable[Report]: 

837 """ 

838 Read-only property to access the report the file belongs to (:attr:`_parent`). 

839 

840 :returns: The report; ``None`` if the file belongs to no report. 

841 """ 

842 return self._parent 

843 

844 @readonly 

845 def Path(self) -> Path: 

846 """ 

847 Read-only property to access the file's path (:attr:`_path`). 

848 

849 :returns: The path, relative to the directory coverage.py ran in. 

850 """ 

851 return self._path 

852 

853 @readonly 

854 def Functions(self) -> dict[str, Region]: 

855 """ 

856 Read-only property to access the functions (:attr:`_functions`). 

857 

858 :returns: The functions, by qualified name; empty in format 2. 

859 """ 

860 return self._functions 

861 

862 @readonly 

863 def Classes(self) -> dict[str, Region]: 

864 """ 

865 Read-only property to access the classes (:attr:`_classes`). 

866 

867 :returns: The classes, by qualified name; empty in format 2. 

868 """ 

869 return self._classes 

870 

871 

872@export 

873class Report(metaclass=ExtendedType, mixin=True): 

874 """ 

875 The report's root: how and when it was written, the measured files, and the totals. 

876 """ 

877 

878 _format: Nullable[FormatVersion] #: Version of the report format. 

879 _version: Nullable[SemanticVersion] #: Version of coverage.py. 

880 _timestamp: Nullable[datetime] #: Time the report was written, local time without time zone. 

881 _branchCoverage: bool #: Whether branch coverage was measured. 

882 _hasContexts: bool #: Whether the report lists the contexts of the lines. 

883 _files: dict[Path, File] #: The measured files, by path. 

884 _totals: Nullable[Summary] #: The counters of the whole report. 

885 

886 def __init__(self) -> None: 

887 """ 

888 Initialize an empty report. 

889 """ 

890 self._format = None 

891 self._version = None 

892 self._timestamp = None 

893 self._branchCoverage = False 

894 self._hasContexts = False 

895 self._files = {} 

896 self._totals = None 

897 

898 @readonly 

899 def Format(self) -> Nullable[FormatVersion]: 

900 """ 

901 Read-only property to access the version of the report format (:attr:`_format`). 

902 

903 :returns: The version; ``None`` before the report was converted. 

904 """ 

905 return self._format 

906 

907 @readonly 

908 def Version(self) -> Nullable[SemanticVersion]: 

909 """ 

910 Read-only property to access the version of coverage.py, which wrote the report (:attr:`_version`). 

911 

912 The report states it as ``meta.version``, e.g. ``7.16.1``. 

913 

914 :returns: The version; ``None`` before the report was converted. 

915 """ 

916 return self._version 

917 

918 @readonly 

919 def Timestamp(self) -> Nullable[datetime]: 

920 """ 

921 Read-only property to access the time the report was written (:attr:`_timestamp`). 

922 

923 :returns: The time in the writer's local time, without a time zone; ``None`` before the report was converted. 

924 """ 

925 return self._timestamp 

926 

927 @readonly 

928 def BranchCoverage(self) -> bool: 

929 """ 

930 Read-only property to access whether branch coverage was measured (:attr:`_branchCoverage`). 

931 

932 :returns: ``True``, if branch coverage was measured. 

933 """ 

934 return self._branchCoverage 

935 

936 @readonly 

937 def HasContexts(self) -> bool: 

938 """ 

939 Read-only property to access whether the report lists the contexts of the lines (:attr:`_hasContexts`). 

940 

941 coverage.py includes them, when configured with ``[json] show_contexts = True``; the report states it as 

942 ``show_contexts``. 

943 

944 :returns: ``True``, if the files list the contexts of their lines. 

945 """ 

946 return self._hasContexts 

947 

948 @readonly 

949 def Files(self) -> dict[Path, File]: 

950 """ 

951 Read-only property to access the measured files (:attr:`_files`). 

952 

953 :returns: The files, by path. 

954 """ 

955 return self._files 

956 

957 @readonly 

958 def Totals(self) -> Nullable[Summary]: 

959 """ 

960 Read-only property to access the counters of the whole report (:attr:`_totals`). 

961 

962 :returns: The totals; ``None`` before the report was converted. 

963 """ 

964 return self._totals 

965 

966 

967@export 

968class Document(cc_Document, Report): 

969 """ 

970 A coverage.py JSON code coverage report: read into the format's model, and converted to the common model. 

971 """ 

972 

973 _jsonDocument: Nullable[dict[str, Any]] #: The parsed and validated JSON document, after :meth:`Analyze`. 

974 

975 def __init__(self, jsonReportFile: Path, analyzeAndConvert: bool = False) -> None: 

976 """ 

977 Initialize the report, and optionally read it. 

978 

979 :param jsonReportFile: Path to the JSON file. 

980 :param analyzeAndConvert: Optional, if true, analyze the file and convert its content. Default: ``False``. 

981 """ 

982 super().__init__(jsonReportFile) 

983 Report.__init__(self) 

984 

985 self._jsonDocument = None 

986 

987 if analyzeAndConvert: 

988 self.Analyze() 

989 self.Convert() 

990 

991 def Analyze(self) -> None: 

992 """ 

993 Parse the JSON file, read the format version it states, and validate it against the JSON Schema of that version 

994 (:data:`SCHEMAS`). 

995 

996 :raises CodeCoverageError: If the file doesn't exist. 

997 :raises CodeCoverageError: If the file can't be read. 

998 :raises CodeCoverageError: If the file isn't UTF-8 encoded. 

999 :raises CodeCoverageError: If the file isn't valid JSON. 

1000 :raises CodeCoverageError: If the file states no supported format version. |br| 

1001 The note lists the supported format versions. 

1002 :raises CodeCoverageError: If the JSON Schema can't be read. 

1003 :raises CodeCoverageError: If the file isn't valid according to the JSON Schema of its format version. 

1004 """ 

1005 if not self._path.exists(): 

1006 raise CodeCoverageError(f"coverage.py report file '{self._path}' does not exist.") \ 

1007 from FileNotFoundError(f"File '{self._path}' not found.") 

1008 

1009 from jsonschema import Draft202012Validator 

1010 

1011 with Stopwatch() as sw: 

1012 try: 

1013 content = self._path.read_text(encoding="utf-8") 

1014 except OSError as ex: 

1015 raise CodeCoverageError(f"Couldn't read coverage.py report file '{self._path}'.") from ex 

1016 except UnicodeDecodeError as ex: 

1017 raise CodeCoverageError(f"coverage.py report file '{self._path}' is not UTF-8 encoded.") from ex 

1018 

1019 try: 

1020 jsonDocument = loads(content) 

1021 except JSONDecodeError as ex: 

1022 raise CodeCoverageError(f"JSON syntax error in coverage.py report file '{self._path}'.") from ex 

1023 

1024 meta = jsonDocument.get("meta") if isinstance(jsonDocument, dict) else None 

1025 version = meta.get("format") if isinstance(meta, dict) else None 

1026 try: 

1027 formatVersion = FormatVersion.Parse(version) 

1028 except (ValueError, TypeError) as cause: 

1029 ex = CodeCoverageError(f"coverage.py report file '{self._path}' states an unsupported format version.") 

1030 got = f"value '{version}'" if version is not None else "no value" 

1031 ex.add_note(f"Got {got} at '/meta/format'.") 

1032 ex.add_note(f"Supported format versions: {', '.join(str(member.value) for member in FormatVersion)}.") 

1033 raise ex from cause 

1034 

1035 schemaFile = SCHEMAS[formatVersion] 

1036 try: 

1037 schema = loads(readResourceFile(Resources, schemaFile)) 

1038 except (ToolingException, JSONDecodeError) as ex: 

1039 raise CodeCoverageError(f"Couldn't read JSON Schema '{schemaFile}' from package resources.") from ex 

1040 

1041 errors = sorted(Draft202012Validator(schema).iter_errors(jsonDocument), key=lambda error: list(error.path)) 

1042 if len(errors) > 0: 

1043 ex = CodeCoverageError(f"Validation error for '{self._path}' using JSON Schema '{schemaFile}'.") 

1044 for error in errors: 

1045 ex.add_note(f"/{'/'.join(str(part) for part in error.path)}: {error.message}") 

1046 raise ex 

1047 

1048 self._jsonDocument = jsonDocument 

1049 

1050 self._analysisDuration = sw.Duration 

1051 

1052 def Convert(self) -> None: 

1053 """ 

1054 Convert the parsed and validated JSON document to the format's model. 

1055 

1056 :raises CodeCoverageError: If the JSON file was not analyzed before. |br| 

1057 Call 'Document.Analyze()' or create the document using 

1058 'Document(path, analyzeAndConvert=True)'. 

1059 :raises CodeCoverageError: If the coverage.py version isn't a semantic version. 

1060 :raises CodeCoverageError: If the timestamp isn't in ISO 8601 format. 

1061 :raises CodeCoverageError: If the report names a file's path twice. 

1062 """ 

1063 if self._jsonDocument is None: 

1064 ex = CodeCoverageError(f"coverage.py report file '{self._path}' needs to be read and analyzed by a JSON parser.") 

1065 ex.add_note("Call 'Document.Analyze()' or create the document using 'Document(path, analyzeAndConvert=True)'.") 

1066 raise ex 

1067 

1068 with Stopwatch() as sw: 

1069 meta = self._jsonDocument["meta"] 

1070 try: 

1071 version = SemanticVersion.Parse(meta["version"]) 

1072 except ValueError as cause: 

1073 ex = CodeCoverageError(f"coverage.py report file '{self._path}' states a malformed coverage.py version.") 

1074 ex.add_note(f"Got value '{meta['version']}' at '/meta/version'.") 

1075 raise ex from cause 

1076 

1077 try: 

1078 timestamp = datetime.fromisoformat(meta["timestamp"]) 

1079 except ValueError as cause: 

1080 ex = CodeCoverageError(f"coverage.py report file '{self._path}' states a malformed timestamp.") 

1081 ex.add_note(f"Got value '{meta['timestamp']}' at '/meta/timestamp'.") 

1082 raise ex from cause 

1083 

1084 self._format = FormatVersion(meta["format"]) 

1085 self._version = version 

1086 self._timestamp = timestamp 

1087 self._branchCoverage = meta["branch_coverage"] 

1088 self._hasContexts = meta["show_contexts"] 

1089 self._totals = Summary.Parse(self._jsonDocument["totals"]) 

1090 

1091 for name, record in self._jsonDocument["files"].items(): 

1092 File.Parse(name, record, parent=self) 

1093 

1094 self._conversionDuration = sw.Duration 

1095 

1096 def ToCoverageSummary(self) -> CoverageSummary: 

1097 """ 

1098 Convert the format's model to the common model, and aggregate it. 

1099 

1100 :returns: The report's root of the common model, named after the report file. 

1101 :raises CodeCoverageError: If a file's path runs through another file. 

1102 """ 

1103 summary = CoverageSummary(self._path.stem) 

1104 

1105 for file in self._files.values(): 

1106 commonFile = summary.GetOrAddFile(file._path) 

1107 self._ConvertLines(file, commonFile) 

1108 self._ConvertUnits(file, commonFile, summary) 

1109 

1110 summary.Aggregate() 

1111 return summary 

1112 

1113 @staticmethod 

1114 def _ConvertLines(file: File, commonFile: cc_File) -> None: 

1115 """ 

1116 Convert a file's lines and branches to lines of the common model. 

1117 

1118 :param file: The file of the format's model. 

1119 :param commonFile: The file of the common model. 

1120 """ 

1121 missingSources = {source for source, _ in file._missingBranches} 

1122 

1123 statuses: dict[int, LineCoverageStatus] = {} 

1124 for number in file._executedLines: 

1125 partial = number in missingSources 

1126 statuses[number] = LineCoverageStatus.PartiallyCovered if partial else LineCoverageStatus.Covered 

1127 

1128 for number in file._missingLines: 

1129 statuses[number] = LineCoverageStatus.Uncovered 

1130 

1131 for number in file._excludedLines: 

1132 statuses[number] = LineCoverageStatus.Excluded 

1133 

1134 for number in sorted(statuses): 

1135 cc_Line(number, statuses[number], parent=commonFile) 

1136 

1137 lines = commonFile._lines 

1138 lastLineNumber = commonFile._lastLineNumber 

1139 for status, arcs in ( 

1140 (LineCoverageStatus.Covered, file._executedBranches), 

1141 (LineCoverageStatus.Uncovered, file._missingBranches) 

1142 ): 

1143 for source, target in arcs: 

1144 cc_Branch(status, target=lines[target] if 0 < target <= lastLineNumber else None, parent=lines[source]) 

1145 

1146 @staticmethod 

1147 def _ConvertUnits(file: File, commonFile: cc_File, summary: CoverageSummary) -> None: 

1148 """ 

1149 Convert a file to units: its directories to packages, the file to a module, its classes and functions to classes, 

1150 methods and functions. 

1151 

1152 :param file: The file of the format's model. 

1153 :param commonFile: The file of the common model. 

1154 :param summary: The report's root of the common model. 

1155 """ 

1156 path = file._path 

1157 parent: cc_Unit | CoverageSummary = summary 

1158 for part in path.parent.parts: 

1159 parent = parent._units[part] if part in parent._units else cc_Package(part, parent=parent) 

1160 

1161 lines = commonFile._lines 

1162 lastLineNumber = commonFile._lastLineNumber 

1163 module = cc_Module( 

1164 path.stem, 

1165 file=commonFile, 

1166 startLine=next(commonFile.IterateLines(), None), 

1167 endLine=lines[lastLineNumber] if lastLineNumber > 0 else None, 

1168 parent=parent 

1169 ) 

1170 

1171 for kind, regions in ((RegionKind.Class, file._classes), (RegionKind.Function, file._functions)): 

1172 for name in sorted(regions): 

1173 region = regions[name] 

1174 *outerNames, ownName = name.split(".") 

1175 

1176 # the enclosing class or function, found by the outer names; a region outside of a known one is left out 

1177 container: Nullable[cc_Unit] = module 

1178 for outerName in outerNames: 

1179 if (container := container._units.get(outerName)) is None: 1179 ↛ 1180line 1179 didn't jump to line 1180 because the condition on line 1179 was never true

1180 break 

1181 

1182 if container is None or ownName in container._units: 1182 ↛ 1183line 1182 didn't jump to line 1183 because the condition on line 1182 was never true

1183 continue 

1184 

1185 if kind is RegionKind.Class: 

1186 unitClass: type[cc_Unit] = cc_Class 

1187 elif isinstance(container, cc_Class): 

1188 unitClass = cc_Method 

1189 else: 

1190 unitClass = cc_Function 

1191 

1192 numbers = [ 

1193 number for number in (region._startLine, *region.AllLines) 

1194 if number <= lastLineNumber and lines[number] is not None 

1195 ] 

1196 

1197 unitClass( 

1198 ownName, 

1199 file=commonFile, 

1200 startLine=lines[min(numbers)] if len(numbers) > 0 else None, 

1201 endLine=lines[max(numbers)] if len(numbers) > 0 else None, 

1202 parent=container 

1203 )