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

492 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:49 +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 jsonschema import Draft202012Validator 

73from pyTooling.Common import getFullyQualifiedName, readResourceFile 

74from pyTooling.Decorators import export, readonly 

75from pyTooling.Exceptions import ToolingException 

76from pyTooling.MetaClasses import ExtendedType, abstractclass 

77from pyTooling.Stopwatch import Stopwatch 

78from pyTooling.Versioning import SemanticVersion 

79 

80from pyEDAA.Reports import Resources 

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

82from pyEDAA.Reports.CodeCoverage import Document as cc_Document, File as cc_File, Function as cc_Function 

83from pyEDAA.Reports.CodeCoverage import Line as cc_Line, LineCoverageStatus, Method as cc_Method, Module as cc_Module 

84from pyEDAA.Reports.CodeCoverage import Package as cc_Package, Unit as cc_Unit 

85 

86 

87__all__ = ["SCHEMAS"] 

88 

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

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

91_Path = Path 

92 

93 

94@export 

95class FormatVersion(IntEnum): 

96 """ 

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

98 

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

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

101 """ 

102 

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

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

105 

106 @classmethod 

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

108 """ 

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

110 

111 :param value: The version. 

112 :returns: The member of that version. 

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

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

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

116 """ 

117 if value is None: 

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

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

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

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

122 raise ex 

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

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

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

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

127 raise ex 

128 

129 return cls(value) 

130 

131 

132SCHEMAS: dict[FormatVersion, str] = { 

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

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

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

136 

137 

138@export 

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

140 """ 

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

142 

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

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

145 """ 

146 

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

148 _coveredLineCount: int #: Number of executed lines. 

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

150 _excludedLineCount: int #: Number of excluded lines. 

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

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

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

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

155 

156 def __init__( 

157 self, 

158 lineCount: int, 

159 coveredLineCount: int, 

160 missingLineCount: int, 

161 excludedLineCount: int, 

162 percentCovered: float, 

163 branchCount: Nullable[int] = None, 

164 coveredBranchCount: Nullable[int] = None, 

165 partialBranchCount: Nullable[int] = None 

166 ) -> None: 

167 """ 

168 Initialize the summary from its counters. 

169 

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

171 :param coveredLineCount: Number of executed lines. 

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

173 :param excludedLineCount: Number of excluded lines. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

200 """ 

201 for name, count in ( 

202 ("lineCount", lineCount), 

203 ("coveredLineCount", coveredLineCount), 

204 ("missingLineCount", missingLineCount), 

205 ("excludedLineCount", excludedLineCount) 

206 ): 

207 if count is None: 

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

209 elif not isinstance(count, int): 

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

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

212 raise ex 

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

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

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

216 raise ex 

217 

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

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

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

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

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

223 raise ex 

224 elif not 0 <= percentCovered <= 100: 

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

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

227 raise ex 

228 

229 for name, count in ( 

230 ("branchCount", branchCount), 

231 ("coveredBranchCount", coveredBranchCount), 

232 ("partialBranchCount", partialBranchCount) 

233 ): 

234 if count is None: 

235 continue 

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

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

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

239 raise ex 

240 elif count < 0: 

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

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

243 raise ex 

244 

245 self._lineCount = lineCount 

246 self._coveredLineCount = coveredLineCount 

247 self._missingLineCount = missingLineCount 

248 self._excludedLineCount = excludedLineCount 

249 self._percentCovered = percentCovered 

250 self._branchCount = branchCount 

251 self._coveredBranchCount = coveredBranchCount 

252 self._partialBranchCount = partialBranchCount 

253 

254 @classmethod 

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

256 """ 

257 Read a summary from its JSON object. 

258 

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

260 :returns: The summary. 

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

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

263 """ 

264 if summary is None: 

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

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

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

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

269 raise ex 

270 

271 return cls( 

272 summary["num_statements"], 

273 summary["covered_lines"], 

274 summary["missing_lines"], 

275 summary["excluded_lines"], 

276 summary["percent_covered"], 

277 summary.get("num_branches"), 

278 summary.get("covered_branches"), 

279 summary.get("num_partial_branches") 

280 ) 

281 

282 @readonly 

283 def LineCount(self) -> int: 

284 """ 

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

286 

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

288 

289 :returns: The number of executable lines. 

290 """ 

291 return self._lineCount 

292 

293 @readonly 

294 def CoveredLineCount(self) -> int: 

295 """ 

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

297 

298 :returns: The number of executed lines. 

299 """ 

300 return self._coveredLineCount 

301 

302 @readonly 

303 def MissingLineCount(self) -> int: 

304 """ 

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

306 

307 :returns: The number of missing lines. 

308 """ 

309 return self._missingLineCount 

310 

311 @readonly 

312 def ExcludedLineCount(self) -> int: 

313 """ 

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

315 

316 :returns: The number of excluded lines. 

317 """ 

318 return self._excludedLineCount 

319 

320 @readonly 

321 def PercentCovered(self) -> float: 

322 """ 

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

324 

325 :returns: The coverage in percent. 

326 """ 

327 return self._percentCovered 

328 

329 @readonly 

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

331 """ 

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

333 

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

335 """ 

336 return self._branchCount 

337 

338 @readonly 

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

340 """ 

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

342 

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

344 """ 

345 return self._coveredBranchCount 

346 

347 @readonly 

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

349 """ 

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

351 

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

353 wasn't measured. 

354 """ 

355 return self._partialBranchCount 

356 

357 

358_Summary = Summary 

359 

360 

361@export 

362@abstractclass 

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

364 """ 

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

366 """ 

367 

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

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

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

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

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

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

374 

375 def __init__( 

376 self, 

377 summary: Summary, 

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

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

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

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

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

383 ) -> None: 

384 """ 

385 Initialize the summary, the lines and the branches. 

386 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

407 """ 

408 if summary is None: 

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

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

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

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

413 raise ex 

414 

415 lineLists: list[list[int]] = [] 

416 for name, numbers in ( 

417 ("executedLines", executedLines), 

418 ("missingLines", missingLines), 

419 ("excludedLines", excludedLines) 

420 ): 

421 lineList: list[int] = [] 

422 if numbers is not None: 

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

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

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

426 raise ex 

427 

428 for number in numbers: 

429 if not isinstance(number, int): 

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

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

432 raise ex 

433 

434 lineList.append(number) 

435 lineLists.append(lineList) 

436 

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

438 for name, branches in ( 

439 ("executedBranches", executedBranches), 

440 ("missingBranches", missingBranches) 

441 ): 

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

443 if branches is not None: 

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

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

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

447 raise ex 

448 

449 for branch in branches: 

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

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

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

453 raise ex 

454 

455 branchList.append(branch) 

456 branchLists.append(branchList) 

457 

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

459 self._executedBranches, self._missingBranches = branchLists 

460 self._summary = summary 

461 

462 @readonly 

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

464 """ 

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

466 

467 :returns: The line numbers. 

468 """ 

469 return self._executedLines 

470 

471 @readonly 

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

473 """ 

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

475 

476 :returns: The line numbers. 

477 """ 

478 return self._missingLines 

479 

480 @readonly 

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

482 """ 

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

484 

485 :returns: The line numbers. 

486 """ 

487 return self._excludedLines 

488 

489 @readonly 

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

491 """ 

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

493 

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

495 """ 

496 return self._executedBranches 

497 

498 @readonly 

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

500 """ 

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

502 

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

504 """ 

505 return self._missingBranches 

506 

507 @readonly 

508 def Summary(self) -> Summary: 

509 """ 

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

511 

512 :returns: The summary. 

513 """ 

514 return self._summary 

515 

516 @readonly 

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

518 """ 

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

520 

521 :returns: The line numbers, sorted. 

522 """ 

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

524 

525 

526@export 

527class RegionKind(Enum): 

528 """ 

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

530 

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

532 """ 

533 

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

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

536 

537 

538@export 

539class Region(Base): 

540 """ 

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

542 

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

544 """ 

545 

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

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

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

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

550 

551 def __init__( 

552 self, 

553 name: str, 

554 kind: RegionKind, 

555 startLine: int, 

556 summary: Summary, 

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

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

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

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

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

562 *, 

563 parent: Nullable[File] = None 

564 ) -> None: 

565 """ 

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

567 to the functions or classes of its file. 

568 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

592 """ 

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

594 

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

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

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

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

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

600 raise ex 

601 elif name == "": 

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

603 

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

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

606 elif not isinstance(kind, RegionKind): 

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

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

609 raise ex 

610 

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

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

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

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

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

616 raise ex 

617 elif startLine < 1: 

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

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

620 raise ex 

621 

622 if parent is not None: 

623 if not isinstance(parent, File): 

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

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

626 raise ex 

627 

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

629 if name in regions: 

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

631 

632 self._parent = parent 

633 self._name = name 

634 self._kind = kind 

635 self._startLine = startLine 

636 

637 if parent is not None: 

638 regions[name] = self 

639 

640 @classmethod 

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

642 """ 

643 Read a region from its JSON object. 

644 

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

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

647 ``classes``. 

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

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

650 :returns: The region. 

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

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

653 """ 

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

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

656 elif not isinstance(region, dict): 

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

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

659 raise ex 

660 

661 return cls( 

662 name, 

663 kind, 

664 region["start_line"], 

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

666 region["executed_lines"], 

667 region["missing_lines"], 

668 region["excluded_lines"], 

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

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

671 parent=parent 

672 ) 

673 

674 @readonly 

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

676 """ 

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

678 

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

680 """ 

681 return self._parent 

682 

683 @readonly 

684 def Name(self) -> str: 

685 """ 

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

687 

688 :returns: The qualified name. 

689 """ 

690 return self._name 

691 

692 @readonly 

693 def Kind(self) -> RegionKind: 

694 """ 

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

696 

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

698 """ 

699 return self._kind 

700 

701 @readonly 

702 def StartLine(self) -> int: 

703 """ 

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

705 

706 :returns: The line number. 

707 """ 

708 return self._startLine 

709 

710 

711@export 

712class File(Base): 

713 """ 

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

715 

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

717 """ 

718 

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

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

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

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

723 

724 def __init__( 

725 self, 

726 path: Path, 

727 summary: Summary, 

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

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

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

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

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

733 *, 

734 parent: Nullable[Report] = None 

735 ) -> None: 

736 """ 

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

738 

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

740 

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

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

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

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

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

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

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

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

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

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

751 Default: ``None``. 

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

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

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

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

756 """ 

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

758 

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

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

761 elif not isinstance(path, Path): 

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

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

764 raise ex 

765 

766 if parent is not None: 

767 if not isinstance(parent, Report): 

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

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

770 raise ex 

771 elif path in parent._files: 

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

773 

774 self._parent = parent 

775 self._path = path 

776 self._functions = {} 

777 self._classes = {} 

778 

779 if parent is not None: 

780 parent._files[path] = self 

781 

782 @classmethod 

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

784 """ 

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

786 

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

788 

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

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

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

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

793 :returns: The file. 

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

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

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

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

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

799 """ 

800 if name is None: 

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

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

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

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

805 raise ex 

806 

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

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

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

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

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

812 raise ex 

813 

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

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

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

817 

818 measuredFile = cls( 

819 path, 

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

821 file["executed_lines"], 

822 file["missing_lines"], 

823 file["excluded_lines"], 

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

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

826 parent=parent 

827 ) 

828 

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

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

831 if regionName != "": 

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

833 

834 return measuredFile 

835 

836 @readonly 

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

838 """ 

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

840 

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

842 """ 

843 return self._parent 

844 

845 @readonly 

846 def Path(self) -> Path: 

847 """ 

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

849 

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

851 """ 

852 return self._path 

853 

854 @readonly 

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

856 """ 

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

858 

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

860 """ 

861 return self._functions 

862 

863 @readonly 

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

865 """ 

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

867 

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

869 """ 

870 return self._classes 

871 

872 

873@export 

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

875 """ 

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

877 """ 

878 

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

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

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

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

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

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

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

886 

887 def __init__(self) -> None: 

888 """ 

889 Initialize an empty report. 

890 """ 

891 self._format = None 

892 self._version = None 

893 self._timestamp = None 

894 self._branchCoverage = False 

895 self._hasContexts = False 

896 self._files = {} 

897 self._totals = None 

898 

899 @readonly 

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

901 """ 

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

903 

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

905 """ 

906 return self._format 

907 

908 @readonly 

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

910 """ 

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

912 

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

914 

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

916 """ 

917 return self._version 

918 

919 @readonly 

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

921 """ 

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

923 

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

925 """ 

926 return self._timestamp 

927 

928 @readonly 

929 def BranchCoverage(self) -> bool: 

930 """ 

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

932 

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

934 """ 

935 return self._branchCoverage 

936 

937 @readonly 

938 def HasContexts(self) -> bool: 

939 """ 

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

941 

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

943 ``show_contexts``. 

944 

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

946 """ 

947 return self._hasContexts 

948 

949 @readonly 

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

951 """ 

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

953 

954 :returns: The files, by path. 

955 """ 

956 return self._files 

957 

958 @readonly 

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

960 """ 

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

962 

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

964 """ 

965 return self._totals 

966 

967 

968@export 

969class Document(cc_Document, Report): 

970 """ 

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

972 """ 

973 

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

975 

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

977 """ 

978 Initialize the report, and optionally read it. 

979 

980 :param jsonReportFile: Path to the JSON file. 

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

982 """ 

983 super().__init__(jsonReportFile) 

984 Report.__init__(self) 

985 

986 self._jsonDocument = None 

987 

988 if analyzeAndConvert: 

989 self.Analyze() 

990 self.Convert() 

991 

992 def Analyze(self) -> None: 

993 """ 

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

995 (:data:`SCHEMAS`). 

996 

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

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

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

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

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

1002 The note lists the supported format versions. 

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

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

1005 """ 

1006 if not self._path.exists(): 

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

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

1009 

1010 with Stopwatch() as sw: 

1011 try: 

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

1013 except OSError as ex: 

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

1015 except UnicodeDecodeError as ex: 

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

1017 

1018 try: 

1019 jsonDocument = loads(content) 

1020 except JSONDecodeError as ex: 

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

1022 

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

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

1025 try: 

1026 formatVersion = FormatVersion.Parse(version) 

1027 except (ValueError, TypeError) as cause: 

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

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

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

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

1032 raise ex from cause 

1033 

1034 schemaFile = SCHEMAS[formatVersion] 

1035 try: 

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

1037 except (ToolingException, JSONDecodeError) as ex: 

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

1039 

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

1041 if len(errors) > 0: 

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

1043 for error in errors: 

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

1045 raise ex 

1046 

1047 self._jsonDocument = jsonDocument 

1048 

1049 self._analysisDuration = sw.Duration 

1050 

1051 def Convert(self) -> None: 

1052 """ 

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

1054 

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

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

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

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

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

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

1061 """ 

1062 if self._jsonDocument is None: 

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

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

1065 raise ex 

1066 

1067 with Stopwatch() as sw: 

1068 meta = self._jsonDocument["meta"] 

1069 try: 

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

1071 except ValueError as cause: 

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

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

1074 raise ex from cause 

1075 

1076 try: 

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

1078 except ValueError as cause: 

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

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

1081 raise ex from cause 

1082 

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

1084 self._version = version 

1085 self._timestamp = timestamp 

1086 self._branchCoverage = meta["branch_coverage"] 

1087 self._hasContexts = meta["show_contexts"] 

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

1089 

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

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

1092 

1093 self._conversionDuration = sw.Duration 

1094 

1095 def ToCoverageSummary(self) -> CoverageSummary: 

1096 """ 

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

1098 

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

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

1101 """ 

1102 summary = CoverageSummary(self._path.stem) 

1103 

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

1105 commonFile = summary.GetOrAddFile(file._path) 

1106 self._ConvertLines(file, commonFile) 

1107 self._ConvertUnits(file, commonFile, summary) 

1108 

1109 summary.Aggregate() 

1110 return summary 

1111 

1112 @staticmethod 

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

1114 """ 

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

1116 

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

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

1119 """ 

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

1121 

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

1123 for number in file._executedLines: 

1124 partial = number in missingSources 

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

1126 

1127 for number in file._missingLines: 

1128 statuses[number] = LineCoverageStatus.Uncovered 

1129 

1130 for number in file._excludedLines: 

1131 statuses[number] = LineCoverageStatus.Excluded 

1132 

1133 for number in sorted(statuses): 

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

1135 

1136 lines = commonFile._lines 

1137 lastLineNumber = commonFile._lastLineNumber 

1138 for status, arcs in ( 

1139 (LineCoverageStatus.Covered, file._executedBranches), 

1140 (LineCoverageStatus.Uncovered, file._missingBranches) 

1141 ): 

1142 for source, target in arcs: 

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

1144 

1145 @staticmethod 

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

1147 """ 

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

1149 methods and functions. 

1150 

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

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

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

1154 """ 

1155 path = file._path 

1156 parent: cc_Unit | CoverageSummary = summary 

1157 for part in path.parent.parts: 

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

1159 

1160 lines = commonFile._lines 

1161 lastLineNumber = commonFile._lastLineNumber 

1162 module = cc_Module( 

1163 path.stem, 

1164 file=commonFile, 

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

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

1167 parent=parent 

1168 ) 

1169 

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

1171 for name in sorted(regions): 

1172 region = regions[name] 

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

1174 

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

1176 container: Nullable[cc_Unit] = module 

1177 for outerName in outerNames: 

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

1179 break 

1180 

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

1182 continue 

1183 

1184 if kind is RegionKind.Class: 

1185 unitClass: type[cc_Unit] = cc_Class 

1186 elif isinstance(container, cc_Class): 

1187 unitClass = cc_Method 

1188 else: 

1189 unitClass = cc_Function 

1190 

1191 numbers = [ 

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

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

1194 ] 

1195 

1196 unitClass( 

1197 ownName, 

1198 file=commonFile, 

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

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

1201 parent=container 

1202 )