Coverage for pyEDAA/Reports/CodeCoverage/Cobertura/Records.py: 97%

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

32The elements of the Cobertura XML format below ``<coverage>``: its packages, classes, methods, lines and conditions. 

33""" 

34from __future__ import annotations 

35 

36from typing import Iterable, Optional as Nullable 

37 

38from lxml.etree import Element as XMLElement, SubElement, _Element 

39from pyTooling.Common import getFullyQualifiedName 

40from pyTooling.Decorators import export, readonly 

41from pyTooling.MetaClasses import ExtendedType 

42 

43from pyEDAA.Reports.CodeCoverage import Class as cc_Class, CoverageSummary, File as cc_File, Function as cc_Function 

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

45from pyEDAA.Reports.CodeCoverage import Package as cc_Package, SourceFile as cc_SourceFile, Unit as cc_Unit 

46 

47 

48@export 

49class Condition(metaclass=ExtendedType, slots=True): 

50 """ 

51 A ``<condition>`` of a line: a condition of a branching line, as gcovr writes it. 

52 """ 

53 

54 _number: int #: Number of the condition in its line. 

55 _type: str #: Kind of condition, e.g. ``jump``. 

56 _coverage: str #: Coverage of the condition, e.g. ``50%``. 

57 

58 def __init__(self, number: int, conditionType: str, coverage: str) -> None: 

59 """ 

60 Initialize a condition. 

61 

62 :param number: Number of the condition in its line. 

63 :param conditionType: Kind of condition, e.g. ``jump``. 

64 :param coverage: Coverage of the condition, e.g. ``50%``. 

65 """ 

66 self._number = number 

67 self._type = conditionType 

68 self._coverage = coverage 

69 

70 @readonly 

71 def Number(self) -> int: 

72 """ 

73 Read-only property to access the number of the condition in its line (:attr:`_number`). 

74 

75 :returns: The number. 

76 """ 

77 return self._number 

78 

79 @readonly 

80 def Type(self) -> str: 

81 """ 

82 Read-only property to access the kind of condition (:attr:`_type`). 

83 

84 The kind is stated as the report states it, e.g. ``jump``. 

85 

86 :returns: The kind. 

87 """ 

88 return self._type 

89 

90 @readonly 

91 def Coverage(self) -> str: 

92 """ 

93 Read-only property to access the coverage of the condition (:attr:`_coverage`). 

94 

95 The coverage is stated as the report states it, e.g. ``50%``. 

96 

97 :returns: The coverage. 

98 """ 

99 return self._coverage 

100 

101 def Generate(self) -> _Element: 

102 """ 

103 Generate the condition's ``<condition>`` element. 

104 

105 :returns: The ``<condition>`` element. 

106 """ 

107 conditionElement = XMLElement("condition") 

108 conditionElement.attrib["number"] = str(self._number) 

109 conditionElement.attrib["type"] = self._type 

110 conditionElement.attrib["coverage"] = self._coverage 

111 return conditionElement 

112 

113 

114@export 

115class Line(metaclass=ExtendedType, slots=True): 

116 """ 

117 A ``<line>``: a line's hits, whether it branches, its condition coverage and its conditions. 

118 """ 

119 

120 _number: int #: Line number. 

121 _hits: int #: How often the line ran. 

122 _branch: bool #: Whether the line branches. 

123 _conditionCoverage: Nullable[tuple[int, int]] #: Taken and all branches, if the line states them. 

124 _conditions: list[Condition] #: The line's conditions. 

125 

126 def __init__( 

127 self, 

128 number: int, 

129 hits: int, 

130 branch: bool = False, 

131 conditionCoverage: Nullable[tuple[int, int]] = None, 

132 conditions: list[Condition] | None = None 

133 ) -> None: 

134 """ 

135 Initialize a line. 

136 

137 :param number: Line number. 

138 :param hits: How often the line ran. 

139 :param branch: Optional, whether the line branches. Default: ``False``. 

140 :param conditionCoverage: Optional, taken and all branches. Default: ``None``. 

141 :param conditions: Optional, the line's conditions. Default: none. 

142 """ 

143 self._number = number 

144 self._hits = hits 

145 self._branch = branch 

146 self._conditionCoverage = conditionCoverage 

147 self._conditions = [] if conditions is None else conditions 

148 

149 @readonly 

150 def Number(self) -> int: 

151 """ 

152 Read-only property to access the line number (:attr:`_number`). 

153 

154 :returns: The line number. 

155 """ 

156 return self._number 

157 

158 @readonly 

159 def Hits(self) -> int: 

160 """ 

161 Read-only property to access how often the line ran (:attr:`_hits`). 

162 

163 :returns: The hits. 

164 """ 

165 return self._hits 

166 

167 @readonly 

168 def Branch(self) -> bool: 

169 """ 

170 Read-only property to access whether the line branches (:attr:`_branch`). 

171 

172 :returns: ``True``, if the line branches. 

173 """ 

174 return self._branch 

175 

176 @readonly 

177 def ConditionCoverage(self) -> Nullable[tuple[int, int]]: 

178 """ 

179 Read-only property to access the line's taken and all branches (:attr:`_conditionCoverage`). 

180 

181 A line with one of two branches taken has ``(1, 2)``. 

182 

183 :returns: The taken and all branches; ``None`` if the line doesn't state them. 

184 """ 

185 return self._conditionCoverage 

186 

187 @readonly 

188 def Conditions(self) -> list[Condition]: 

189 """ 

190 Read-only property to access the line's conditions (:attr:`_conditions`). 

191 

192 :returns: The conditions. 

193 """ 

194 return self._conditions 

195 

196 @classmethod 

197 def FromLine(cls, line: cc_Line) -> Line: 

198 """ 

199 Convert a line of the common model. 

200 

201 A line without count has ``1`` hit, if it ran, else ``0``. A line with branches is a branching line; its condition 

202 coverage counts its branches with state :attr:`~pyEDAA.Reports.CodeCoverage.LineCoverageStatus.Covered` and all its 

203 branches. 

204 

205 :param line: The line of the common model. 

206 :returns: The line. 

207 :raises ValueError: If parameter ``line`` is ``None``. 

208 :raises TypeError: If parameter ``line`` isn't of type :class:`~pyEDAA.Reports.CodeCoverage.Line`. 

209 """ 

210 if line is None: 

211 raise ValueError("Parameter 'line' is None.") 

212 elif not isinstance(line, cc_Line): 

213 ex = TypeError("Parameter 'line' is not of type 'Line'.") 

214 ex.add_note(f"Got type '{getFullyQualifiedName(line)}'.") 

215 raise ex 

216 

217 if line._coverageCount is not None: 

218 hits = line._coverageCount 

219 elif line._status in (LineCoverageStatus.Covered, LineCoverageStatus.PartiallyCovered): 

220 hits = 1 

221 else: 

222 hits = 0 

223 

224 if len(line._branches) == 0: 

225 return cls(line._lineNumber, hits) 

226 

227 covered = sum(1 for branch in line._branches if branch._status is LineCoverageStatus.Covered) 

228 return cls(line._lineNumber, hits, True, (covered, len(line._branches))) 

229 

230 def Generate(self) -> _Element: 

231 """ 

232 Generate the line's ``<line>`` element with its conditions. 

233 

234 :returns: The ``<line>`` element. 

235 """ 

236 lineElement = XMLElement("line") 

237 lineElement.attrib["number"] = str(self._number) 

238 lineElement.attrib["hits"] = str(self._hits) 

239 if self._branch: 

240 lineElement.attrib["branch"] = "true" 

241 

242 if self._conditionCoverage is not None: 

243 covered, total = self._conditionCoverage 

244 percent = 100 if total == 0 else 100 * covered // total 

245 lineElement.attrib["condition-coverage"] = f"{percent}% ({covered}/{total})" 

246 

247 if len(self._conditions) > 0: 

248 conditionsElement = SubElement(lineElement, "conditions") 

249 for condition in self._conditions: 

250 conditionsElement.append(condition.Generate()) 

251 

252 return lineElement 

253 

254 

255@export 

256class Element(metaclass=ExtendedType, slots=True): 

257 """ 

258 Base-class of the named elements with rates - packages, classes and methods -, as the report states the rates. 

259 """ 

260 

261 _name: str #: Name of the element. 

262 _lineRate: Nullable[float] #: Line coverage, as the report states it. 

263 _branchRate: Nullable[float] #: Branch coverage, as the report states it. 

264 

265 def __init__(self, name: str, lineRate: Nullable[float] = None, branchRate: Nullable[float] = None) -> None: 

266 """ 

267 Initialize the name and the rates. 

268 

269 :param name: Name of the element. 

270 :param lineRate: Optional, line coverage, as the report states it. Default: ``None``. 

271 :param branchRate: Optional, branch coverage, as the report states it. Default: ``None``. 

272 """ 

273 self._name = name 

274 self._lineRate = lineRate 

275 self._branchRate = branchRate 

276 

277 @readonly 

278 def Name(self) -> str: 

279 """ 

280 Read-only property to access the element's name (:attr:`_name`). 

281 

282 :returns: The name. 

283 """ 

284 return self._name 

285 

286 @readonly 

287 def LineRate(self) -> Nullable[float]: 

288 """ 

289 Read-only property to access the line coverage, as the report states it (:attr:`_lineRate`). 

290 

291 :returns: The rate in range 0.0..1.0, or ``None`` if the report doesn't state it. 

292 """ 

293 return self._lineRate 

294 

295 @readonly 

296 def BranchRate(self) -> Nullable[float]: 

297 """ 

298 Read-only property to access the branch coverage, as the report states it (:attr:`_branchRate`). 

299 

300 :returns: The rate in range 0.0..1.0, or ``None`` if the report doesn't state it. 

301 """ 

302 return self._branchRate 

303 

304 @staticmethod 

305 def _CountLines(lines: Iterable[Line]) -> tuple[int, int, int, int]: 

306 """ 

307 Count lines and their branches. 

308 

309 :param lines: The lines. 

310 :returns: The number of lines, of lines, which ran, of branches, and of branches, which were taken. 

311 """ 

312 linesValid = linesCovered = branchesValid = branchesCovered = 0 

313 for line in lines: 

314 linesValid += 1 

315 if line._hits > 0: 

316 linesCovered += 1 

317 

318 if line._conditionCoverage is not None: 

319 branchesCovered += line._conditionCoverage[0] 

320 branchesValid += line._conditionCoverage[1] 

321 

322 return linesValid, linesCovered, branchesValid, branchesCovered 

323 

324 @staticmethod 

325 def _Rates(counts: tuple[int, int, int, int]) -> tuple[float, float]: 

326 """ 

327 Compute the line and branch rates from the counts of :meth:`_CountLines`. 

328 

329 :param counts: The number of lines, of lines, which ran, of branches, and of branches, which were taken. 

330 :returns: The line and the branch rate in range 0.0..1.0; ``1.0`` if there is no line or no branch. 

331 """ 

332 linesValid, linesCovered, branchesValid, branchesCovered = counts 

333 return ( 

334 1.0 if linesValid == 0 else linesCovered / linesValid, 

335 1.0 if branchesValid == 0 else branchesCovered / branchesValid 

336 ) 

337 

338 def _GenerateRates(self, xmlElement: _Element, lines: Iterable[Line]) -> None: 

339 """ 

340 Add the line and branch rate to the element's XML element: as stated, or computed from its lines. 

341 

342 :param xmlElement: The element's XML element. 

343 :param lines: The element's lines. 

344 """ 

345 lineRate = self._lineRate 

346 branchRate = self._branchRate 

347 if lineRate is None or branchRate is None: 

348 computedLineRate, computedBranchRate = self._Rates(self._CountLines(lines)) 

349 lineRate = computedLineRate if lineRate is None else lineRate 

350 branchRate = computedBranchRate if branchRate is None else branchRate 

351 

352 xmlElement.attrib["line-rate"] = str(round(lineRate, 4)) 

353 xmlElement.attrib["branch-rate"] = str(round(branchRate, 4)) 

354 

355 

356@export 

357class Method(Element): 

358 """ 

359 A ``<method>`` of a class: its signature and lines. 

360 """ 

361 

362 _signature: Nullable[str] #: Signature of the method. 

363 _lines: dict[int, Line] #: The method's lines, by number. 

364 

365 def __init__( 

366 self, 

367 name: str, 

368 signature: Nullable[str] = None, 

369 lineRate: Nullable[float] = None, 

370 branchRate: Nullable[float] = None 

371 ) -> None: 

372 """ 

373 Initialize a method. 

374 

375 :param name: Name of the method. 

376 :param signature: Optional, signature of the method. Default: ``None``. 

377 :param lineRate: Optional, line coverage, as the report states it. Default: ``None``. 

378 :param branchRate: Optional, branch coverage, as the report states it. Default: ``None``. 

379 """ 

380 super().__init__(name, lineRate, branchRate) 

381 

382 self._signature = signature 

383 self._lines = {} 

384 

385 @readonly 

386 def Signature(self) -> Nullable[str]: 

387 """ 

388 Read-only property to access the method's signature (:attr:`_signature`). 

389 

390 :returns: The signature, or ``None`` if the report doesn't state it. 

391 """ 

392 return self._signature 

393 

394 @readonly 

395 def Lines(self) -> dict[int, Line]: 

396 """ 

397 Read-only property to access the method's lines (:attr:`_lines`). 

398 

399 :returns: The lines, by number. 

400 """ 

401 return self._lines 

402 

403 def Generate(self) -> _Element: 

404 """ 

405 Generate the method's ``<method>`` element with its lines. 

406 

407 A rate the method doesn't state is computed from its lines; a missing signature is written as empty text. 

408 

409 :returns: The ``<method>`` element. 

410 """ 

411 methodElement = XMLElement("method") 

412 methodElement.attrib["name"] = self._name 

413 methodElement.attrib["signature"] = "" if self._signature is None else self._signature 

414 self._GenerateRates(methodElement, self._lines.values()) 

415 

416 linesElement = SubElement(methodElement, "lines") 

417 for line in self._lines.values(): 

418 linesElement.append(line.Generate()) 

419 

420 return methodElement 

421 

422 

423@export 

424class Class(Element): 

425 """ 

426 A ``<class>`` of a package: its source file, methods and lines. 

427 """ 

428 

429 _filename: str #: Path of the class' source file, relative to a ``<source>`` directory. 

430 _complexity: Nullable[float] #: Complexity, as the report states it. 

431 _methods: dict[str, Method] #: The class' methods, by name and signature. 

432 _lines: dict[int, Line] #: The class' lines, by number. 

433 

434 def __init__( 

435 self, 

436 name: str, 

437 filename: str, 

438 lineRate: Nullable[float] = None, 

439 branchRate: Nullable[float] = None, 

440 complexity: Nullable[float] = None 

441 ) -> None: 

442 """ 

443 Initialize a class. 

444 

445 :param name: Name of the class. 

446 :param filename: Path of the class' source file, relative to a ``<source>`` directory. 

447 :param lineRate: Optional, line coverage, as the report states it. Default: ``None``. 

448 :param branchRate: Optional, branch coverage, as the report states it. Default: ``None``. 

449 :param complexity: Optional, complexity, as the report states it. Default: ``None``. 

450 """ 

451 super().__init__(name, lineRate, branchRate) 

452 

453 self._filename = filename 

454 self._complexity = complexity 

455 self._methods = {} 

456 self._lines = {} 

457 

458 @readonly 

459 def Filename(self) -> str: 

460 """ 

461 Read-only property to access the path of the class' source file (:attr:`_filename`). 

462 

463 :returns: The path, relative to a ``<source>`` directory. 

464 """ 

465 return self._filename 

466 

467 @readonly 

468 def Complexity(self) -> Nullable[float]: 

469 """ 

470 Read-only property to access the complexity, as the report states it (:attr:`_complexity`). 

471 

472 :returns: The complexity, or ``None`` if the report doesn't state it. 

473 """ 

474 return self._complexity 

475 

476 @readonly 

477 def Methods(self) -> dict[str, Method]: 

478 """ 

479 Read-only property to access the class' methods (:attr:`_methods`). 

480 

481 :returns: The methods, by name - and signature, if two methods share a name. 

482 """ 

483 return self._methods 

484 

485 @readonly 

486 def Lines(self) -> dict[int, Line]: 

487 """ 

488 Read-only property to access the class' lines (:attr:`_lines`). 

489 

490 :returns: The lines, by number. 

491 """ 

492 return self._lines 

493 

494 @classmethod 

495 def _FromFile(cls, file: cc_File, packages: dict[str, Package]) -> None: 

496 """ 

497 Convert a file of the common model to classes, and add them to their packages. 

498 

499 :param file: The file of the common model. 

500 :param packages: The packages, by name; a missing package is added. 

501 """ 

502 filename = file.Path.as_posix() 

503 directories = file.Path.parent.parts 

504 directoryPackage = ".".join(directories) if len(directories) > 0 else "." 

505 

506 fileClasses: list[Class] = [] 

507 

508 def addClass(packageName: str, className: str) -> Class: 

509 """ 

510 Nested function creating a class of this file, and adding it to its package. 

511 

512 :param packageName: Name of the package. 

513 :param className: Name of the class. 

514 :returns: The class. 

515 """ 

516 if (package := packages.get(packageName)) is None: 

517 package = packages[packageName] = Package(packageName, complexity=0.0) 

518 

519 klass = cls(className, filename, complexity=0.0) 

520 package._classes.append(klass) 

521 fileClasses.append(klass) 

522 return klass 

523 

524 classes: dict[cc_Unit, Class] = {} 

525 spanned: list[cc_Unit] = [] 

526 for unit in file._units: 

527 if not isinstance(unit, (cc_SourceFile, cc_Module, cc_Class)): 

528 continue 

529 

530 chain: list[cc_Unit] = [] 

531 element: cc_Unit | CoverageSummary = unit 

532 while isinstance(element, cc_Unit): 

533 chain.insert(0, element) 

534 element = element._parent 

535 

536 packageCount = 0 

537 while isinstance(chain[packageCount], cc_Package): 

538 packageCount += 1 

539 

540 classes[unit] = addClass( 

541 ".".join(part._name for part in chain[:packageCount]) if packageCount > 0 else directoryPackage, 

542 ".".join(part._name for part in chain[packageCount:]) 

543 ) 

544 if unit._startLine is not None and unit._endLine is not None: 

545 spanned.append(unit) 

546 

547 # outer units first: an inner unit overwrites the owner of its lines 

548 owners: dict[int, Class] = {} 

549 for unit in sorted(spanned, key=lambda unit: (unit._startLine._lineNumber, -unit._endLine._lineNumber)): 

550 for line in file.IterateLines(unit._startLine, unit._endLine): 

551 owners[line._lineNumber] = classes[unit] 

552 

553 fallback: Nullable[Class] = None 

554 lines: dict[int, Line] = {} 

555 for commonLine in file.IterateLines(): 

556 if commonLine._status is LineCoverageStatus.Excluded: 

557 continue 

558 

559 line = lines[commonLine._lineNumber] = Line.FromLine(commonLine) 

560 if (klass := owners.get(line._number)) is None: 

561 if fallback is None: 

562 fallback = addClass(directoryPackage, file._name) 

563 klass = fallback 

564 

565 klass._lines[line._number] = line 

566 

567 for unit in file._units: 

568 if not isinstance(unit, (cc_Function, cc_Method)): 

569 continue 

570 

571 names = [unit._name] 

572 element = unit._parent 

573 while isinstance(element, cc_Unit) and element not in classes: 573 ↛ 574line 573 didn't jump to line 574 because the condition on line 573 was never true

574 if not isinstance(element, cc_Package): 

575 names.insert(0, element._name) 

576 element = element._parent 

577 

578 if isinstance(element, cc_Unit): 

579 klass = classes[element] 

580 else: 

581 if fallback is None: 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true

582 fallback = addClass(directoryPackage, file._name) 

583 klass = fallback 

584 

585 method = Method(".".join(names)) 

586 if unit._startLine is not None and unit._endLine is not None: 586 ↛ 591line 586 didn't jump to line 591 because the condition on line 586 was always true

587 for commonLine in file.IterateLines(unit._startLine, unit._endLine): 

588 if (line := lines.get(commonLine._lineNumber)) is not None: 588 ↛ 587line 588 didn't jump to line 587 because the condition on line 588 was always true

589 method._lines[line._number] = line 

590 

591 method._lineRate, method._branchRate = cls._Rates(cls._CountLines(method._lines.values())) 

592 klass._methods[method._name] = method 

593 

594 for klass in fileClasses: 

595 klass._lineRate, klass._branchRate = cls._Rates(cls._CountLines(klass._lines.values())) 

596 

597 def Generate(self) -> _Element: 

598 """ 

599 Generate the class' ``<class>`` element with its methods and lines. 

600 

601 A rate the class doesn't state is computed from its lines; a missing complexity is written as ``0``. 

602 

603 :returns: The ``<class>`` element. 

604 """ 

605 classElement = XMLElement("class") 

606 classElement.attrib["name"] = self._name 

607 classElement.attrib["filename"] = self._filename 

608 self._GenerateRates(classElement, self._lines.values()) 

609 classElement.attrib["complexity"] = str(0 if self._complexity is None else self._complexity) 

610 

611 methodsElement = SubElement(classElement, "methods") 

612 for method in self._methods.values(): 

613 methodsElement.append(method.Generate()) 

614 

615 linesElement = SubElement(classElement, "lines") 

616 for line in self._lines.values(): 

617 linesElement.append(line.Generate()) 

618 

619 return classElement 

620 

621 

622@export 

623class Package(Element): 

624 """ 

625 A ``<package>``: its classes. 

626 """ 

627 

628 _complexity: Nullable[float] #: Complexity, as the report states it. 

629 _classes: list[Class] #: The package's classes, in the report's order. 

630 

631 def __init__( 

632 self, 

633 name: str, 

634 lineRate: Nullable[float] = None, 

635 branchRate: Nullable[float] = None, 

636 complexity: Nullable[float] = None 

637 ) -> None: 

638 """ 

639 Initialize a package. 

640 

641 :param name: Name of the package. 

642 :param lineRate: Optional, line coverage, as the report states it. Default: ``None``. 

643 :param branchRate: Optional, branch coverage, as the report states it. Default: ``None``. 

644 :param complexity: Optional, complexity, as the report states it. Default: ``None``. 

645 """ 

646 super().__init__(name, lineRate, branchRate) 

647 

648 self._complexity = complexity 

649 self._classes = [] 

650 

651 @readonly 

652 def Complexity(self) -> Nullable[float]: 

653 """ 

654 Read-only property to access the complexity, as the report states it (:attr:`_complexity`). 

655 

656 :returns: The complexity, or ``None`` if the report doesn't state it. 

657 """ 

658 return self._complexity 

659 

660 @readonly 

661 def Classes(self) -> list[Class]: 

662 """ 

663 Read-only property to access the package's classes (:attr:`_classes`). 

664 

665 :returns: The classes, in the report's order; two classes may share a name. 

666 """ 

667 return self._classes 

668 

669 @classmethod 

670 def _FromFiles(cls, files: Iterable[cc_File]) -> list[Package]: 

671 """ 

672 Convert the files of the common model to packages of their classes. 

673 

674 :param files: The files of the common model. 

675 :returns: The packages, by name. 

676 """ 

677 packages: dict[str, Package] = {} 

678 for file in files: 

679 Class._FromFile(file, packages) 

680 

681 for package in packages.values(): 

682 package._lineRate, package._branchRate = cls._Rates(cls._CountLines( 

683 line for klass in package._classes for line in klass._lines.values() 

684 )) 

685 

686 return [packages[name] for name in sorted(packages)] 

687 

688 def Generate(self) -> _Element: 

689 """ 

690 Generate the package's ``<package>`` element with its classes. 

691 

692 A rate the package doesn't state is computed from its classes' lines; a missing complexity is written as ``0``. 

693 

694 :returns: The ``<package>`` element. 

695 """ 

696 packageElement = XMLElement("package") 

697 packageElement.attrib["name"] = self._name 

698 self._GenerateRates(packageElement, [line for klass in self._classes for line in klass._lines.values()]) 

699 packageElement.attrib["complexity"] = str(0 if self._complexity is None else self._complexity) 

700 

701 classesElement = SubElement(packageElement, "classes") 

702 for klass in self._classes: 

703 classesElement.append(klass.Generate()) 

704 

705 return packageElement