Coverage for pyEDAA/Reports/DocumentationCoverage/__init__.py: 100%

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

32Abstract data model of documentation coverage, the common model every language's reader fills. 

33 

34The model has two hierarchies over the same documentable items: 

35 

36* The **physical** hierarchy - :class:`DocumentationReport`, :class:`Directory` and :class:`File` - is built from the 

37 source files a reader analyzed. A file counts the items declared in it; a directory sums its directories and files. 

38* The **logical** hierarchy holds the documentable items of a language - :class:`Item` -, each with its kind, its 

39 documentation state, its source file and lines, and the items it contains. The report holds its top-level items too. 

40 

41An item - a package, a class, a method, a field, a VHDL entity, ... - names the source file it is declared in and its 

42first and last line, where the reader knows them. 

43 

44An item's :class:`ItemKind` names the language construct, e.g. a class, a method or a VHDL entity. Each kind belongs to 

45an :class:`ItemCategory` shared by all languages - containers, types, callables, fields, variables, parameters -, so 

46the counters can be broken down the same way for every language. 

47 

48An item's :class:`CoverageState` says, whether it is documented, and whether it is expected to be. The counters 

49(:class:`CoverageCounters`) of an item, a file, a directory or the whole analysis are computed from these states by 

50``Aggregate()``. 

51 

52.. seealso:: 

53 

54 :mod:`pyEDAA.Reports.DocumentationCoverage.Python` 

55 |rarr| The reader of Python source files. 

56""" 

57from __future__ import annotations 

58 

59from datetime import timedelta 

60from enum import Enum, Flag 

61from pathlib import Path 

62from typing import Callable, ClassVar, Generator, Iterator, Optional as Nullable 

63 

64from pyTooling.Common import getFullyQualifiedName, getTypeName 

65from pyTooling.Decorators import export, readonly 

66from pyTooling.Exceptions import AlreadyInHierarchyError, DuplicateElementError 

67from pyTooling.MetaClasses import ExtendedType, abstractmethod 

68from pyTooling.Warning import CriticalWarning 

69 

70from pyEDAA.Reports import ReportException 

71 

72 

73@export 

74class DocCoverageError(ReportException): 

75 """Base-exception of the documentation coverage data model and its readers.""" 

76 

77 

78@export 

79class AlreadyInDocumentationReportError(DocCoverageError, AlreadyInHierarchyError): 

80 """The exception is raised when an element is added to an element, but it already has a parent.""" 

81 

82 

83@export 

84class DuplicateDirectoryError(DocCoverageError, DuplicateElementError): 

85 """The exception is raised when a directory already contains a directory or a file of the same name.""" 

86 

87 

88@export 

89class DuplicateFileError(DocCoverageError, DuplicateElementError): 

90 """The exception is raised when a directory or a report already contains a file or a directory of the same name.""" 

91 

92 

93@export 

94class SourceFileWarning(CriticalWarning): 

95 """ 

96 A source file can't be analyzed: it can't be read, decoded or parsed. 

97 

98 The reader excludes the file's item from the analysis, and continues with the next file. The cause is the exception 

99 reading, decoding or parsing raised. 

100 

101 .. important:: 

102 

103 This warning is critical: it must be collected by a :class:`~pyTooling.Warning.WarningCollector` up the 

104 call-hierarchy, otherwise :meth:`~pyTooling.Warning.WarningCollector.Raise` raises an 

105 :exc:`~pyTooling.Warning.UnhandledCriticalWarningError`. 

106 """ 

107 

108 _file: Nullable[_File] #: The report's file of the source file. 

109 

110 def __init__(self, message: str, /, *, file: Nullable[_File] = None) -> None: 

111 """ 

112 Initializes the warning with the report's file of the source file that can't be analyzed. 

113 

114 :param message: The warning's message. 

115 :param file: Optional, the report's file of the source file. 

116 """ 

117 super().__init__(message) 

118 self._file = file 

119 

120 @readonly 

121 def File(self) -> Nullable[_File]: 

122 """ 

123 Read-only property to access the report's file of the source file that can't be analyzed (:attr:`_file`). 

124 

125 Its path, relative to the analyzed directory, is 

126 :attr:`File.Path <pyEDAA.Reports.DocumentationCoverage.File.Path>`. 

127 

128 :returns: The file, or ``None`` if it wasn't recorded. 

129 """ 

130 return self._file 

131 

132 

133@export 

134class CoverageState(Flag): 

135 """ 

136 The documentation state of an item, as flags. 

137 

138 :class:`CoverageCounters` counts an item by the first of these flags it has: 

139 

140 * :attr:`Empty`: not counted. 

141 * :attr:`Excluded`: counted as excluded. 

142 * :attr:`Ignored`: counted as ignored. 

143 * :attr:`Covered`: counted as expected and covered. 

144 * None of them: counted as expected and uncovered. 

145 """ 

146 

147 Unknown = 0 #: No state; an item can't have it. 

148 Excluded = 1 #: Excluded from the analysis, e.g. by a name pattern. 

149 Ignored = 2 #: Needs no documentation by the reader's rules, e.g. a property's setter. 

150 Empty = 4 #: Has nothing to document, e.g. an empty module; not counted. 

151 Covered = 8 #: Counts as documented. 

152 

153 Weak = 16 #: Documented, but too briefly. 

154 Incomplete = 32 #: Documented, but not every part of it. 

155 Inherited = 64 #: Undocumented, but takes the documentation of what it overrides, as a comment states. 

156 Detailed = 128 #: Documented in detail. 

157 

158 Undocumented = 256 #: Has no documentation. 

159 Documented = 512 #: Has documentation. 

160 

161 Parameters = 1024 #: Its parameters are documented. 

162 ReturnValue = 2048 #: Its return value is documented. 

163 Exceptions = 8192 #: The exceptions it raises are documented. 

164 Types = 16384 #: Its types are documented. 

165 

166 Excused = 32768 #: Undocumented, but a comment excuses the missing documentation. 

167 

168 

169@export 

170class ItemCategory(Enum): 

171 """ 

172 The category of a documentable item, shared by all languages. 

173 

174 Each :class:`ItemKind` belongs to one category (:attr:`ItemKind.Category`), so the counters of every language break 

175 down the same way. 

176 """ 

177 

178 Container = 1 #: Contains other items, e.g. a package, a module, a namespace or a VHDL entity. 

179 Type = 2 #: Declares a type, e.g. a class, a struct, an enumeration or a type alias. 

180 Callable = 3 #: Can be called, e.g. a function, a method, a procedure or a property. 

181 Field = 4 #: Data of a type, e.g. a class' field, a record's element or an enumeration's member. 

182 Variable = 5 #: Data of a container, whose value can change, e.g. a module's variable or a VHDL signal. 

183 Constant = 6 #: Data of a container, whose value can't change, e.g. a constant of C or VHDL. 

184 Generic = 7 #: A compile-time parameter, e.g. a VHDL generic or a C++ template parameter. 

185 Parameter = 8 #: A run-time parameter declared as an item of its own, e.g. a VHDL port. 

186 Other = 9 #: Belongs to no other category. 

187 

188 DEFAULT = Other #: The category of an item, which belongs to no other category. 

189 

190 

191@export 

192class ItemKind(Enum): 

193 """ 

194 The kind of a documentable item: the language construct it is. 

195 

196 Languages share a kind, where they share the construct, e.g. a package of Python, Java and Go. A construct only one 

197 language has, has a kind of its own, e.g. a VHDL entity. Each kind belongs to an :class:`ItemCategory` 

198 (:attr:`Category`). 

199 """ 

200 

201 Library = 1 #: A library of design units, e.g. a VHDL library. 

202 Package = 2 #: A package, e.g. of Python, Java or Go, or a VHDL package. 

203 PackageBody = 3 #: The body of a VHDL package. 

204 Namespace = 4 #: A namespace, e.g. of C++ or TCL. 

205 Module = 5 #: A module, e.g. a Python module. 

206 SourceFile = 6 #: A source file, where the file is the language's unit, e.g. in C. 

207 Entity = 7 #: A VHDL entity. 

208 Architecture = 8 #: A VHDL architecture. 

209 Configuration = 9 #: A VHDL configuration. 

210 Context = 10 #: A VHDL context. 

211 

212 Class = 11 #: A class. 

213 Struct = 12 #: A structure, e.g. of C, C++ or Go. 

214 Union = 13 #: A union of C or C++. 

215 Interface = 14 #: An interface, e.g. of Java or Go. 

216 Enumeration = 15 #: An enumeration. 

217 Record = 16 #: A record, e.g. of Java or a VHDL record type. 

218 Type = 17 #: Any other type declaration, e.g. a VHDL type or subtype. 

219 TypeAlias = 18 #: A type alias, e.g. a C ``typedef`` or Python's ``type`` statement. 

220 TypeVariable = 19 #: A type variable, e.g. Python's ``TypeVar``. 

221 Component = 20 #: A VHDL component declaration. 

222 

223 Function = 21 #: A function. 

224 Method = 22 #: A method of a type. 

225 Constructor = 23 #: A type's constructor, e.g. Python's ``__init__``. 

226 Destructor = 24 #: A type's destructor, e.g. of C++, or Python's ``__del__``. 

227 Property = 25 #: A property: a method accessed like a field. 

228 Procedure = 26 #: A procedure, e.g. of VHDL or TCL. 

229 Macro = 27 #: A preprocessor macro, e.g. of C. 

230 Process = 28 #: A VHDL process. 

231 

232 Field = 29 #: A field of a type, e.g. a class' field or a record's element. 

233 EnumerationMember = 30 #: A member of an enumeration. 

234 

235 Variable = 31 #: A variable of a container, e.g. a module's variable. 

236 Signal = 32 #: A VHDL signal. 

237 FileObject = 33 #: A file object, e.g. declared by a VHDL file declaration. 

238 Constant = 34 #: A constant. 

239 

240 Generic = 35 #: A VHDL generic. 

241 TypeParameter = 36 #: A type parameter, e.g. of a C++ template or a Java generic class. 

242 Parameter = 37 #: A parameter declared as an item of its own, e.g. of a VHDL subprogram. 

243 Port = 38 #: A VHDL port. 

244 

245 Alias = 39 #: An alias, e.g. a VHDL alias declaration. 

246 Attribute = 40 #: A VHDL attribute declaration. 

247 Other = 41 #: Any other documentable item. 

248 

249 DEFAULT = Other #: The kind of an item, which is no other kind. 

250 

251 __CATEGORIES: ClassVar[dict[int, ItemCategory]] = { 

252 Library: ItemCategory.Container, 

253 Package: ItemCategory.Container, 

254 PackageBody: ItemCategory.Container, 

255 Namespace: ItemCategory.Container, 

256 Module: ItemCategory.Container, 

257 SourceFile: ItemCategory.Container, 

258 Entity: ItemCategory.Container, 

259 Architecture: ItemCategory.Container, 

260 Configuration: ItemCategory.Container, 

261 Context: ItemCategory.Container, 

262 Class: ItemCategory.Type, 

263 Struct: ItemCategory.Type, 

264 Union: ItemCategory.Type, 

265 Interface: ItemCategory.Type, 

266 Enumeration: ItemCategory.Type, 

267 Record: ItemCategory.Type, 

268 Type: ItemCategory.Type, 

269 TypeAlias: ItemCategory.Type, 

270 TypeVariable: ItemCategory.Type, 

271 Component: ItemCategory.Type, 

272 Function: ItemCategory.Callable, 

273 Method: ItemCategory.Callable, 

274 Constructor: ItemCategory.Callable, 

275 Destructor: ItemCategory.Callable, 

276 Property: ItemCategory.Callable, 

277 Procedure: ItemCategory.Callable, 

278 Macro: ItemCategory.Callable, 

279 Process: ItemCategory.Callable, 

280 Field: ItemCategory.Field, 

281 EnumerationMember: ItemCategory.Field, 

282 Variable: ItemCategory.Variable, 

283 Signal: ItemCategory.Variable, 

284 FileObject: ItemCategory.Variable, 

285 Constant: ItemCategory.Constant, 

286 Generic: ItemCategory.Generic, 

287 TypeParameter: ItemCategory.Generic, 

288 Parameter: ItemCategory.Parameter, 

289 Port: ItemCategory.Parameter, 

290 Alias: ItemCategory.Other, 

291 Attribute: ItemCategory.Other, 

292 Other: ItemCategory.Other, 

293 } #: The category of each kind, by the kind's value. 

294 

295 @readonly 

296 def Category(self) -> ItemCategory: 

297 """ 

298 Read-only property to return the category this kind belongs to. 

299 

300 :returns: The kind's category. 

301 """ 

302 return self.__CATEGORIES[self.value] 

303 

304 

305@export 

306class CoverageCounters(metaclass=ExtendedType, slots=True): 

307 """ 

308 The numbers of documentable items by their coverage state, and the coverage computed from them. 

309 

310 * An item is *expected* to be documented, unless it is *ignored* or *excluded*; an item without anything to document 

311 (:attr:`CoverageState.Empty`) isn't counted at all. 

312 * An expected item is either *covered* or *uncovered*. 

313 

314 .. code-block:: text 

315 

316 total = expected + ignored + excluded 

317 expected = covered + uncovered 

318 coverage = covered / expected 

319 """ 

320 

321 _expected: int #: Number of items expected to be documented. 

322 _covered: int #: Number of expected items, which count as documented. 

323 _ignored: int #: Number of items, which need no documentation by the reader's rules. 

324 _excluded: int #: Number of items excluded from the analysis. 

325 

326 def __init__(self) -> None: 

327 """ 

328 Initializes counters of zero items. 

329 """ 

330 self._expected = 0 

331 self._covered = 0 

332 self._ignored = 0 

333 self._excluded = 0 

334 

335 @readonly 

336 def Total(self) -> int: 

337 """ 

338 Read-only property to return the number of counted items. 

339 

340 :returns: The sum of expected, ignored and excluded items. 

341 """ 

342 return self._expected + self._ignored + self._excluded 

343 

344 @readonly 

345 def Expected(self) -> int: 

346 """ 

347 Read-only property to access the number of items expected to be documented (:attr:`_expected`). 

348 

349 :returns: The number of expected items. 

350 """ 

351 return self._expected 

352 

353 @readonly 

354 def Covered(self) -> int: 

355 """ 

356 Read-only property to access the number of expected items, which count as documented (:attr:`_covered`). 

357 

358 :returns: The number of covered items. 

359 """ 

360 return self._covered 

361 

362 @readonly 

363 def Uncovered(self) -> int: 

364 """ 

365 Read-only property to return the number of expected items, which don't count as documented. 

366 

367 :returns: :attr:`Expected` minus :attr:`Covered`. 

368 """ 

369 return self._expected - self._covered 

370 

371 @readonly 

372 def Ignored(self) -> int: 

373 """ 

374 Read-only property to access the number of items, which need no documentation by the reader's rules 

375 (:attr:`_ignored`). 

376 

377 :returns: The number of ignored items. 

378 """ 

379 return self._ignored 

380 

381 @readonly 

382 def Excluded(self) -> int: 

383 """ 

384 Read-only property to access the number of items excluded from the analysis (:attr:`_excluded`). 

385 

386 :returns: The number of excluded items. 

387 """ 

388 return self._excluded 

389 

390 @readonly 

391 def Coverage(self) -> float: 

392 """ 

393 Read-only property to return the documentation coverage: the ratio of covered to expected items. 

394 

395 :returns: The coverage in range 0.0..1.0; ``1.0`` if no item is expected. 

396 """ 

397 return 1.0 if self._expected == 0 else self._covered / self._expected 

398 

399 def _Count(self, status: CoverageState) -> None: 

400 """ 

401 Count one item of a coverage state. 

402 

403 :param status: The item's coverage state. 

404 """ 

405 if CoverageState.Empty in status: 

406 return 

407 elif CoverageState.Excluded in status: 

408 self._excluded += 1 

409 elif CoverageState.Ignored in status: 

410 self._ignored += 1 

411 else: 

412 self._expected += 1 

413 if CoverageState.Covered in status: 

414 self._covered += 1 

415 

416 def _Add(self, other: CoverageCounters) -> None: 

417 """ 

418 Add the numbers of other counters to these. 

419 

420 :param other: The other counters. 

421 """ 

422 self._expected += other._expected 

423 self._covered += other._covered 

424 self._ignored += other._ignored 

425 self._excluded += other._excluded 

426 

427 def __repr__(self) -> str: 

428 """ 

429 Return a representation of the counters for debugging. 

430 

431 Counters of 3 covered out of 4 expected items, 1 ignored item and no excluded item read 

432 ``<CoverageCounters 3/4, ignored 1, excluded 0: 75.0%>``. 

433 

434 :returns: The covered, expected, ignored and excluded items and the coverage. 

435 """ 

436 return ( 

437 f"<CoverageCounters {self._covered}/{self._expected}, ignored {self._ignored}, excluded {self._excluded}: " 

438 f"{self.Coverage:.1%}>" 

439 ) 

440 

441 

442@export 

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

444 """ 

445 Base-class of every element of the documentation coverage model: a reference to the element containing it and to 

446 the analysis' root, and the counters computed by :meth:`Aggregate`. 

447 """ 

448 

449 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: The types a parent may have; assigned by each class having a parent. 

450 

451 _parent: Nullable[Base] #: The element containing this one, or ``None``. 

452 _root: Nullable[DocumentationReport] #: The report, or ``None`` while the element isn't part of one. 

453 _counters: CoverageCounters #: The counters of the items this element counts. 

454 _categories: dict[ItemCategory, CoverageCounters] #: The counters per category of the items. 

455 

456 def __init__(self, *, parent: Nullable[Base] = None) -> None: 

457 """ 

458 Initializes an element with its parent, the parent's root, and counters of zero items. 

459 

460 The parent doesn't list the element yet: the class setting the field the parent stores it by - a name - adds it 

461 with ``parent._AddElement(self)``. 

462 

463 :param parent: Optional, the element containing this one. Default: ``None``. 

464 :raises TypeError: If parameter ``parent`` isn't of a type this class declares in :attr:`_PARENT_TYPE`. 

465 """ 

466 if parent is None: 

467 self._parent = None 

468 self._root = None 

469 elif not isinstance(parent, self._PARENT_TYPE): 

470 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE) 

471 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.") 

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

473 raise ex 

474 else: 

475 self._parent = parent 

476 self._root = parent._root 

477 

478 self._counters = CoverageCounters() 

479 self._categories = {} 

480 

481 @property 

482 def Parent(self) -> Nullable[Base]: 

483 """ 

484 Property to access the element containing this one (:attr:`_parent`). 

485 

486 Assigning a parent adds this element to it, and sets the parent's root as the root of this element and of every 

487 element below it. 

488 

489 :returns: The parent, or ``None``. 

490 :raises ValueError: If ``None`` is assigned. 

491 :raises TypeError: If the assigned parent isn't of a type this class declares in 

492 :attr:`_PARENT_TYPE`. 

493 :raises TypeError: If a parent is assigned to a :class:`DocumentationReport`, which has no 

494 parent. 

495 :raises AlreadyInDocumentationReportError: If the element already has a parent. 

496 :raises DuplicateDirectoryError: If the assigned parent is a directory, which already contains a directory 

497 of this name. 

498 :raises DuplicateFileError: If the assigned parent is a directory, which already contains a file of 

499 this name. 

500 """ 

501 return self._parent 

502 

503 @Parent.setter 

504 def Parent(self, parent: Base) -> None: 

505 if parent is None: 

506 raise ValueError("Parameter 'parent' is None.") 

507 elif not isinstance(parent, self._PARENT_TYPE): 

508 typeNames = " or ".join(f"'{parentType.__name__}'" for parentType in self._PARENT_TYPE) 

509 ex = TypeError(f"Parameter 'parent' is not of type {typeNames}.") 

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

511 raise ex 

512 elif self._parent is not None: 

513 raise AlreadyInDocumentationReportError(f"{getTypeName(self)} already has a parent.") 

514 

515 parent._AddElement(self) 

516 self._parent = parent 

517 self._SetRoot(parent._root) 

518 

519 @readonly 

520 def Root(self) -> Nullable[DocumentationReport]: 

521 """ 

522 Read-only property to access the analysis' root (:attr:`_root`). 

523 

524 The root is maintained by :attr:`Parent`: assigning a parent sets it for this element and every element below it. 

525 

526 :returns: The root, or ``None`` while the element isn't part of an analysis. 

527 """ 

528 return self._root 

529 

530 @readonly 

531 def Counters(self) -> CoverageCounters: 

532 """ 

533 Read-only property to access the counters of the items this element counts (:attr:`_counters`). 

534 

535 They are zero until :meth:`Aggregate` computed them. 

536 

537 :returns: The counters. 

538 """ 

539 return self._counters 

540 

541 @readonly 

542 def Categories(self) -> dict[ItemCategory, CoverageCounters]: 

543 """ 

544 Read-only property to access the counters per category of the items this element counts (:attr:`_categories`). 

545 

546 A category without items has no entry. The counters are computed by :meth:`Aggregate`. 

547 

548 :returns: The counters, by category. 

549 """ 

550 return self._categories 

551 

552 def _ResetCounters(self) -> None: 

553 """ 

554 Set the counters to zero items. 

555 """ 

556 self._counters = CoverageCounters() 

557 self._categories = {} 

558 

559 def _CountItem(self, item: Item) -> None: 

560 """ 

561 Count an item's own coverage state, in the counters and in the counters of its category. 

562 

563 :param item: The item. 

564 """ 

565 self._counters._Count(item._status) 

566 category = item._kind.Category 

567 if (counters := self._categories.get(category, None)) is None: 

568 counters = CoverageCounters() 

569 self._categories[category] = counters 

570 

571 counters._Count(item._status) 

572 

573 def _AddCounters(self, other: Base) -> None: 

574 """ 

575 Add the counters of another element to this one's. 

576 

577 :param other: The other element, aggregated. 

578 """ 

579 self._counters._Add(other._counters) 

580 for category, otherCounters in other._categories.items(): 

581 if (counters := self._categories.get(category, None)) is None: 

582 counters = CoverageCounters() 

583 self._categories[category] = counters 

584 

585 counters._Add(otherCounters) 

586 

587 def _SetRoot(self, root: Nullable[DocumentationReport]) -> None: 

588 """ 

589 Set the analysis' root of this element. 

590 

591 A class containing elements overrides it, to set the root of those too. 

592 

593 :param root: The root, or ``None`` while the element isn't part of an analysis. 

594 """ 

595 self._root = root 

596 

597 @abstractmethod 

598 def Aggregate(self) -> None: 

599 """ 

600 Compute the counters. 

601 """ 

602 

603 

604# A class with a property named like a class - ``Path`` - can't name that class in the annotation of a field: the class 

605# body's namespace, where annotations are evaluated, binds the name to the property. 

606_Path = Path 

607 

608 

609@export 

610class BaseWithPath(Base): 

611 """ 

612 Base-class of the physical hierarchy - directories and source files -: a name and a path below the report. 

613 

614 Its parent is the :class:`Directory` containing it. 

615 """ 

616 

617 _name: _Path #: Name of the directory or file: a path of one component. 

618 

619 def __init__(self, name: Path, *, parent: Nullable[Directory]) -> None: 

620 """ 

621 Initializes the parent, the name and the counters. 

622 

623 :param name: Name of the directory or file: a path of one component. 

624 :param parent: The directory containing this one, or ``None``. 

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

626 :raises TypeError: If parameter ``name`` isn't of type :class:`~pathlib.Path`. 

627 :raises ValueError: If parameter ``name`` is empty or ``.``. 

628 :raises ValueError: If parameter ``name`` has more than one component. 

629 :raises TypeError: If parameter ``parent`` isn't of type :class:`Directory`. 

630 """ 

631 super().__init__(parent=parent) 

632 

633 if name is None: 

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

635 elif not isinstance(name, Path): 

636 ex = TypeError("Parameter 'name' is not of type 'Path'.") 

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

638 raise ex 

639 elif name.name == "": 

640 ex = ValueError("Parameter 'name' is empty.") 

641 ex.add_note(f"Got path '{name}'.") 

642 raise ex 

643 elif len(name.parts) > 1: 

644 ex = ValueError("Parameter 'name' has more than one component.") 

645 ex.add_note(f"Got path '{name.as_posix()}'.") 

646 raise ex 

647 

648 self._name = name 

649 

650 @readonly 

651 def Name(self) -> Path: 

652 """ 

653 Read-only property to access the name of the directory or file (:attr:`_name`). 

654 

655 :returns: The name: a path of one component. 

656 """ 

657 return self._name 

658 

659 @readonly 

660 def Path(self) -> Path: 

661 """ 

662 Read-only property to return the path below the report: the names of the parent directories and the own name. 

663 

664 A file ``Shapes.py`` in a directory ``myPackage`` has the path ``myPackage/Shapes.py``. 

665 

666 :returns: The path; the name, if there is no parent. 

667 """ 

668 if self._parent is None: 

669 return self._name 

670 

671 return self._parent.Path / self._name 

672 

673 

674@export 

675class Directory(BaseWithPath): 

676 """ 

677 A directory: its directories and source files, and their summed counters. 

678 """ 

679 

680 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: A directory is in a directory; assigned below the class. 

681 

682 _directories: dict[_Path, Directory] #: The directories in this directory, by name. 

683 _files: dict[_Path, File] #: The source files in this directory, by name. 

684 

685 def __init__(self, name: Path, *, parent: Nullable[Directory] = None) -> None: 

686 """ 

687 Initializes a directory, and adds it to its parent directory. 

688 

689 :param name: Name of the directory: a path of one component. 

690 :param parent: Optional, the directory containing this one. Default: ``None``. 

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

692 :raises TypeError: If parameter ``name`` isn't of type :class:`~pathlib.Path`. 

693 :raises ValueError: If parameter ``name`` is empty or ``.``. 

694 :raises ValueError: If parameter ``name`` has more than one component. 

695 :raises TypeError: If parameter ``parent`` isn't of type :class:`Directory`. 

696 :raises DuplicateDirectoryError: If the parent directory already contains a file or directory of this name. 

697 """ 

698 super().__init__(name, parent=parent) 

699 

700 self._directories = {} 

701 self._files = {} 

702 

703 if parent is not None: 

704 parent._AddElement(self) 

705 

706 def _AddElement(self, element: Directory | File) -> None: 

707 """ 

708 Add a directory or file, which names this directory as its parent. 

709 

710 :param element: The directory or file. 

711 :raises DuplicateDirectoryError: If this directory already contains a directory of this name. 

712 :raises DuplicateFileError: If this directory already contains a file of this name. 

713 """ 

714 if isinstance(element, Directory): 

715 elements = self._directories 

716 exception = DuplicateDirectoryError 

717 else: 

718 elements = self._files 

719 exception = DuplicateFileError 

720 

721 if element._name in self._directories or element._name in self._files: 

722 raise exception(f"Directory '{self.Path.as_posix()}' already contains '{element._name.as_posix()}'.") 

723 

724 elements[element._name] = element 

725 

726 def _SetRoot(self, root: Nullable[DocumentationReport]) -> None: 

727 """ 

728 Set the analysis' root of this directory, and of the directories and files below it. 

729 

730 :param root: The root, or ``None`` while the directory isn't part of an analysis. 

731 """ 

732 self._root = root 

733 for directory in self._directories.values(): 

734 directory._SetRoot(root) 

735 

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

737 file._SetRoot(root) 

738 

739 @readonly 

740 def Directories(self) -> dict[Path, Directory]: 

741 """ 

742 Read-only property to access the directories in this directory (:attr:`_directories`). 

743 

744 :returns: The directories, by name. 

745 """ 

746 return self._directories 

747 

748 @readonly 

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

750 """ 

751 Read-only property to access the source files in this directory (:attr:`_files`). 

752 

753 :returns: The files, by name. 

754 """ 

755 return self._files 

756 

757 @readonly 

758 def FileCount(self) -> int: 

759 """ 

760 Read-only property to return the number of source files in this directory and below. 

761 

762 :returns: The number of files. 

763 """ 

764 return len(self._files) + sum(directory.FileCount for directory in self._directories.values()) 

765 

766 def GetOrAddFile(self, path: Path) -> File: 

767 """ 

768 Return the source file at a path below this directory, adding it and the directories on its way, if missing. 

769 

770 :param path: The file's path relative to this directory, e.g. ``myPackage/Shapes.py``. 

771 :returns: The file. 

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

773 :raises TypeError: If parameter ``path`` isn't of type :class:`~pathlib.Path`. 

774 :raises ValueError: If parameter ``path`` names no file. 

775 :raises DocCoverageError: If a part of the path is a file instead of a directory, or the file's name is a 

776 directory's. 

777 """ 

778 if path is None: 

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

780 elif not isinstance(path, Path): 

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

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

783 raise ex 

784 

785 parts = [Path(part) for part in path.parts if part not in ("", ".", path.anchor)] 

786 if len(parts) == 0: 

787 ex = ValueError("Parameter 'path' names no file.") 

788 ex.add_note(f"Got path '{path}'.") 

789 raise ex 

790 

791 directory = self 

792 for part in parts[:-1]: 

793 if part in directory._files: 

794 raise DocCoverageError(f"'{(directory.Path / part).as_posix()}' is a file, not a directory.") 

795 elif (subdirectory := directory._directories.get(part)) is None: 

796 subdirectory = Directory(part, parent=directory) 

797 directory = subdirectory 

798 

799 if (file := directory._files.get(parts[-1])) is None: 

800 file = File(parts[-1], parent=directory) 

801 

802 return file 

803 

804 def IterateFiles(self) -> Generator[File, None, None]: 

805 """ 

806 Iterate the source files in this directory and below. 

807 

808 A directory's files come before its directories' files, each in the order they were added. 

809 

810 :returns: A generator of the files. 

811 """ 

812 yield from self._files.values() 

813 for directory in self._directories.values(): 

814 yield from directory.IterateFiles() 

815 

816 def Aggregate(self) -> None: 

817 """ 

818 Aggregate the directories and files in this directory, then sum their counters. 

819 """ 

820 self._ResetCounters() 

821 for child in (*self._directories.values(), *self._files.values()): 

822 child.Aggregate() 

823 self._AddCounters(child) 

824 

825 def __repr__(self) -> str: 

826 """ 

827 Return a representation of the directory for debugging. 

828 

829 A directory ``myPackage`` of 3 files reads ``<Directory myPackage: 3 files, 75.0%>``. 

830 

831 :returns: The directory's path, its number of files and its documentation coverage. 

832 """ 

833 return f"<Directory {self.Path.as_posix()}: {self.FileCount} files, {self._counters.Coverage:.1%}>" 

834 

835 

836Directory._PARENT_TYPE = (Directory, ) 

837 

838 

839@export 

840class DocumentationReport(Directory): 

841 """ 

842 The report of a documentation coverage analysis: the directory the source files' paths are relative to, and the 

843 top-level items of the logical hierarchy. 

844 

845 It is its own root. Its counters count every item of the logical hierarchy; its directories and files count only 

846 the items declared in a source file. 

847 """ 

848 

849 _items: list[Item] #: The top-level items, in the order they were added. 

850 _analysisDuration: Nullable[timedelta] #: Duration of the analysis, which produced the report, if measured. 

851 

852 def __init__(self, name: Path) -> None: 

853 """ 

854 Initializes the report of a documentation coverage analysis. 

855 

856 :param name: Name of the report: a path of one component, e.g. the analyzed package's name. 

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

858 :raises TypeError: If parameter ``name`` isn't of type :class:`~pathlib.Path`. 

859 :raises ValueError: If parameter ``name`` is empty or ``.``. 

860 :raises ValueError: If parameter ``name`` has more than one component. 

861 """ 

862 super().__init__(name) 

863 

864 self._root = self 

865 self._items = [] 

866 self._analysisDuration = None 

867 

868 @Base.Parent.setter 

869 def Parent(self, parent: None) -> None: 

870 ex = TypeError(f"A '{getFullyQualifiedName(self)}' has no parent.") 

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

872 raise ex 

873 

874 def _AddElement(self, element: Directory | File | Item) -> None: 

875 """ 

876 Add a top-level item, a directory or a file, which names the report as its parent. 

877 

878 :param element: The item, directory or file. 

879 :raises DuplicateDirectoryError: If the report already contains a directory of this name. 

880 :raises DuplicateFileError: If the report already contains a file of this name. 

881 """ 

882 if isinstance(element, BaseWithPath): 

883 super()._AddElement(element) 

884 else: 

885 self._items.append(element) 

886 

887 @readonly 

888 def Path(self) -> Path: 

889 """ 

890 Read-only property to return the path below the report, which is the report's own: ``.``. 

891 

892 So the paths of the directories and files below are relative to the report, without its name. 

893 

894 :returns: ``.`` 

895 """ 

896 return Path(".") 

897 

898 @readonly 

899 def Items(self) -> list[Item]: 

900 """ 

901 Read-only property to access the top-level items of the logical hierarchy (:attr:`_items`). 

902 

903 :returns: The items, in the order they were added. 

904 """ 

905 return self._items 

906 

907 @readonly 

908 def AnalysisDuration(self) -> Nullable[timedelta]: 

909 """ 

910 Read-only property to access the duration of the analysis, which produced the report (:attr:`_analysisDuration`). 

911 

912 A reader measures it; a report built otherwise has none. 

913 

914 :returns: The duration, or ``None`` if it wasn't measured. 

915 """ 

916 return self._analysisDuration 

917 

918 def Aggregate(self) -> None: 

919 """ 

920 Aggregate the physical hierarchy - directories and files -, then the items of the logical hierarchy, which the 

921 report's counters sum. 

922 """ 

923 super().Aggregate() 

924 

925 self._ResetCounters() 

926 for item in self._items: 

927 item.Aggregate() 

928 self._AddCounters(item) 

929 

930 

931@export 

932class File(BaseWithPath): 

933 """ 

934 A source file: the items declared in it, and their counters. 

935 

936 The items are those of the logical hierarchy naming this file; the file counts each item's own state, not the items 

937 it contains, which name their file themselves. 

938 """ 

939 

940 _PARENT_TYPE: ClassVar[tuple[type, ...]] = (Directory, ) #: A file is in a directory. 

941 

942 _items: list[Item] #: The items declared in this file, in the order they were added. 

943 

944 def __init__(self, name: Path, *, parent: Nullable[Directory] = None) -> None: 

945 """ 

946 Initializes a source file, and adds it to its directory. 

947 

948 :param name: Name of the file: a path of one component. 

949 :param parent: Optional, the directory containing the file. Default: ``None``. 

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

951 :raises TypeError: If parameter ``name`` isn't of type :class:`~pathlib.Path`. 

952 :raises ValueError: If parameter ``name`` is empty or ``.``. 

953 :raises ValueError: If parameter ``name`` has more than one component. 

954 :raises TypeError: If parameter ``parent`` isn't of type :class:`Directory`. 

955 :raises DuplicateFileError: If the directory already contains a file or directory of this name. 

956 """ 

957 super().__init__(name, parent=parent) 

958 

959 self._items = [] 

960 

961 if parent is not None: 

962 parent._AddElement(self) 

963 

964 @readonly 

965 def Items(self) -> list[Item]: 

966 """ 

967 Read-only property to access the items declared in this file (:attr:`_items`). 

968 

969 :returns: The items, in the order they were added. 

970 """ 

971 return self._items 

972 

973 def Aggregate(self) -> None: 

974 """ 

975 Count the own coverage state of each item declared in this file. 

976 """ 

977 self._ResetCounters() 

978 for item in self._items: 

979 self._CountItem(item) 

980 

981 def __repr__(self) -> str: 

982 """ 

983 Return a representation of the file for debugging. 

984 

985 A file ``Shapes.py`` of 4 items reads ``<File myPackage/Shapes.py: 4 items, 75.0%>``. 

986 

987 :returns: The file's path, its number of items and its documentation coverage. 

988 """ 

989 return f"<File {self.Path.as_posix()}: {len(self._items)} items, {self._counters.Coverage:.1%}>" 

990 

991 

992# A class with a property named like a class - ``File`` - can't name that class in the annotation of a field: the class 

993# body's namespace, where annotations are evaluated, binds the name to the property. 

994_File = File 

995 

996 

997@export 

998class Item(Base): 

999 """ 

1000 A documentable item of a language, its coverage state, and the items it contains. 

1001 

1002 Its parent is the :class:`Item` containing it, or the :class:`DocumentationReport` for a top-level item. Items of one 

1003 parent may share a name, e.g. a property's getter and setter. 

1004 

1005 A container's own documentation - a module's or a class' doc-string - is the container's own coverage state; the 

1006 items it contains have states of their own. So an item's counters count the item itself and every item below it. 

1007 """ 

1008 

1009 _PARENT_TYPE: ClassVar[tuple[type, ...]] #: An item is in an item or the report; assigned below the class. 

1010 

1011 _name: str #: Name of the item. 

1012 _kind: ItemKind #: The language construct the item is. 

1013 _status: CoverageState #: The item's own coverage state. 

1014 _file: Nullable[_File] #: The source file the item is declared in, if the reader knows it. 

1015 _startLine: Nullable[int] #: The item's first line, if the reader knows it. 

1016 _endLine: Nullable[int] #: The item's last line, if the reader knows it. 

1017 _items: list[Item] #: The items this one contains, in the order they were added. 

1018 

1019 def __init__( 

1020 self, 

1021 name: str, 

1022 kind: ItemKind, 

1023 status: CoverageState, 

1024 *, 

1025 file: Nullable[File] = None, 

1026 startLine: Nullable[int] = None, 

1027 endLine: Nullable[int] = None, 

1028 parent: Nullable[Item | DocumentationReport] = None 

1029 ) -> None: 

1030 """ 

1031 Initializes an item, and adds it to its file and to the item or report containing it. 

1032 

1033 :param name: Name of the item. 

1034 :param kind: The language construct the item is. 

1035 :param status: The item's own coverage state. 

1036 :param file: Optional, the source file the item is declared in. Default: ``None``. 

1037 :param startLine: Optional, the item's first line. Default: ``None``. 

1038 :param endLine: Optional, the item's last line. Default: ``None``. 

1039 :param parent: Optional, the item or report containing this item. Default: ``None``. 

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

1041 :raises TypeError: If parameter ``name`` isn't of type :class:`str`. 

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

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

1044 :raises TypeError: If parameter ``kind`` isn't of type :class:`ItemKind`. 

1045 :raises ValueError: If parameter ``status`` is ``None``. 

1046 :raises TypeError: If parameter ``status`` isn't of type :class:`CoverageState`. 

1047 :raises ValueError: If parameter ``status`` is ``CoverageState.Unknown``. 

1048 :raises TypeError: If parameter ``file`` isn't of type :class:`File`. 

1049 :raises TypeError: If parameter ``startLine`` isn't of type :class:`int`. 

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

1051 :raises TypeError: If parameter ``endLine`` isn't of type :class:`int`. 

1052 :raises ValueError: If parameter ``endLine`` is before parameter ``startLine``, or less than 1. 

1053 :raises TypeError: If parameter ``parent`` isn't of type :class:`Item` or :class:`DocumentationReport`. 

1054 """ 

1055 super().__init__(parent=parent) 

1056 

1057 if name is None: 

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

1059 elif not isinstance(name, str): 

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

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

1062 raise ex 

1063 elif name == "": 

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

1065 

1066 if kind is None: 

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

1068 elif not isinstance(kind, ItemKind): 

1069 ex = TypeError("Parameter 'kind' is not of type 'ItemKind'.") 

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

1071 raise ex 

1072 

1073 if status is None: 

1074 raise ValueError("Parameter 'status' is None.") 

1075 elif not isinstance(status, CoverageState): 

1076 ex = TypeError("Parameter 'status' is not of type 'CoverageState'.") 

1077 ex.add_note(f"Got type '{getFullyQualifiedName(status)}'.") 

1078 raise ex 

1079 elif status is CoverageState.Unknown: 

1080 raise ValueError("Parameter 'status' is 'Unknown'.") 

1081 

1082 if file is not None and not isinstance(file, File): 

1083 ex = TypeError("Parameter 'file' is not of type 'File'.") 

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

1085 raise ex 

1086 

1087 if startLine is not None: 

1088 if not isinstance(startLine, int): 

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

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

1091 raise ex 

1092 elif startLine < 1: 

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

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

1095 raise ex 

1096 

1097 if endLine is not None: 

1098 if not isinstance(endLine, int): 

1099 ex = TypeError("Parameter 'endLine' is not of type 'int'.") 

1100 ex.add_note(f"Got type '{getFullyQualifiedName(endLine)}'.") 

1101 raise ex 

1102 elif endLine < (1 if startLine is None else startLine): 

1103 ex = ValueError("Parameter 'endLine' is before parameter 'startLine', or less than 1.") 

1104 ex.add_note(f"Got lines {startLine} to {endLine}.") 

1105 raise ex 

1106 

1107 self._name = name 

1108 self._kind = kind 

1109 self._status = status 

1110 self._file = file 

1111 self._startLine = startLine 

1112 self._endLine = endLine 

1113 self._items = [] 

1114 

1115 if parent is not None: 

1116 parent._AddElement(self) 

1117 

1118 if file is not None: 

1119 file._items.append(self) 

1120 

1121 def _AddElement(self, item: Item) -> None: 

1122 """ 

1123 Add an item, which names this item as its parent. 

1124 

1125 :param item: The item. 

1126 """ 

1127 self._items.append(item) 

1128 

1129 def _SetRoot(self, root: Nullable[DocumentationReport]) -> None: 

1130 """ 

1131 Set the analysis' root of this item, and of the items below it. 

1132 

1133 :param root: The root, or ``None`` while the item isn't part of an analysis. 

1134 """ 

1135 self._root = root 

1136 for item in self._items: 

1137 item._SetRoot(root) 

1138 

1139 def IterateDescendants(self, predicate: Nullable[Callable[[Item], bool]] = None) -> Generator[Item, None, None]: 

1140 """ 

1141 Iterate all or selected items below this one - the items it contains, theirs, ... -, depth-first: each item 

1142 followed by the items below it. 

1143 

1144 If parameter ``predicate`` is not None, the given filter function is used to skip items in the generator. A skipped 

1145 item's items are still iterated. 

1146 

1147 :param predicate: Optional, filter function accepting an item and returning a boolean. Default: ``None``. 

1148 :returns: A generator of the items below this one. 

1149 """ 

1150 if predicate is None: 

1151 for item in self._items: 

1152 yield item 

1153 yield from item.IterateDescendants() 

1154 else: 

1155 for item in self._items: 

1156 if predicate(item): 

1157 yield item 

1158 

1159 yield from item.IterateDescendants(predicate) 

1160 

1161 @readonly 

1162 def Name(self) -> str: 

1163 """ 

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

1165 

1166 :returns: The name. 

1167 """ 

1168 return self._name 

1169 

1170 @readonly 

1171 def Kind(self) -> ItemKind: 

1172 """ 

1173 Read-only property to access the language construct the item is (:attr:`_kind`). 

1174 

1175 :returns: The item's kind. 

1176 """ 

1177 return self._kind 

1178 

1179 @readonly 

1180 def Category(self) -> ItemCategory: 

1181 """ 

1182 Read-only property to access the category of the item's kind (:attr:`_kind`). 

1183 

1184 :returns: The item's category. 

1185 """ 

1186 return self._kind.Category 

1187 

1188 @readonly 

1189 def Status(self) -> CoverageState: 

1190 """ 

1191 Read-only property to access the item's own coverage state (:attr:`_status`). 

1192 

1193 :returns: The coverage state. 

1194 """ 

1195 return self._status 

1196 

1197 @readonly 

1198 def File(self) -> Nullable[File]: 

1199 """ 

1200 Read-only property to access the source file the item is declared in (:attr:`_file`). 

1201 

1202 :returns: The file, or ``None`` if the reader doesn't know it. 

1203 """ 

1204 return self._file 

1205 

1206 @readonly 

1207 def StartLine(self) -> Nullable[int]: 

1208 """ 

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

1210 

1211 :returns: The line number, or ``None`` if the reader doesn't know it. 

1212 """ 

1213 return self._startLine 

1214 

1215 @readonly 

1216 def EndLine(self) -> Nullable[int]: 

1217 """ 

1218 Read-only property to access the item's last line (:attr:`_endLine`). 

1219 

1220 :returns: The line number, or ``None`` if the reader doesn't know it. 

1221 """ 

1222 return self._endLine 

1223 

1224 @readonly 

1225 def Items(self) -> list[Item]: 

1226 """ 

1227 Read-only property to access the items this one contains (:attr:`_items`). 

1228 

1229 :returns: The items, in the order they were added. 

1230 """ 

1231 return self._items 

1232 

1233 @readonly 

1234 def QualifiedName(self) -> str: 

1235 """ 

1236 Read-only property to return the names of the items from the top down to this one, joined by ``.``. 

1237 

1238 A method ``Area`` of a class ``Circle`` in a module ``myPackage.Shapes`` has the qualified name 

1239 ``myPackage.Shapes.Circle.Area``. 

1240 

1241 :returns: The qualified name. 

1242 """ 

1243 if isinstance(self._parent, Item): 

1244 return f"{self._parent.QualifiedName}.{self._name}" 

1245 

1246 return self._name 

1247 

1248 def __len__(self) -> int: 

1249 """ 

1250 Return the number of items this one contains. 

1251 

1252 :returns: The number of items. 

1253 """ 

1254 return len(self._items) 

1255 

1256 def __iter__(self) -> Iterator[Item]: 

1257 """ 

1258 Iterate the items this one contains. 

1259 

1260 :returns: An iterator of the items, in the order they were added. 

1261 """ 

1262 return iter(self._items) 

1263 

1264 def __contains__(self, name: str) -> bool: 

1265 """ 

1266 Return whether this item contains an item of a name. 

1267 

1268 :param name: The name. 

1269 :returns: ``True``, if an item of this name is contained. 

1270 """ 

1271 return any(item._name == name for item in self._items) 

1272 

1273 def __getitem__(self, name: str) -> Item: 

1274 """ 

1275 Return the contained item of a name; the first one, if several share it. 

1276 

1277 A property's getter is added before its setter, so it is the one returned. 

1278 

1279 :param name: The name. 

1280 :returns: The item. 

1281 :raises KeyError: If no item of this name is contained. 

1282 """ 

1283 for item in self._items: 

1284 if item._name == name: 

1285 return item 

1286 

1287 raise KeyError(f"Item '{self.QualifiedName}' contains no item '{name}'.") 

1288 

1289 def Aggregate(self) -> None: 

1290 """ 

1291 Aggregate the items this one contains, then count this item's own coverage state and add their counters. 

1292 """ 

1293 self._ResetCounters() 

1294 self._CountItem(self) 

1295 for item in self._items: 

1296 item.Aggregate() 

1297 self._AddCounters(item) 

1298 

1299 def __repr__(self) -> str: 

1300 """ 

1301 Return a representation of the item for debugging. 

1302 

1303 A method ``Area`` of class ``Circle`` in module ``Shapes`` reads ``<Item Method Shapes.Circle.Area: 100.0%>``. 

1304 

1305 :returns: The item's kind, qualified name and documentation coverage. 

1306 """ 

1307 return f"<Item {self._kind.name} {self.QualifiedName}: {self._counters.Coverage:.1%}>" 

1308 

1309 

1310Item._PARENT_TYPE = (Item, DocumentationReport)