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

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

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 :mod:`pyEDAA.Reports.DocumentationCoverage.DocStrCoverage` 

57 |rarr| The converter of docstr_coverage's analysis. 

58""" 

59from __future__ import annotations 

60 

61from datetime import timedelta 

62from enum import Enum, Flag 

63from pathlib import Path 

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

65 

66from pyTooling.Common import getFullyQualifiedName, getTypeName 

67from pyTooling.Decorators import export, readonly 

68from pyTooling.Exceptions import AlreadyInHierarchyError, DuplicateElementError 

69from pyTooling.MetaClasses import ExtendedType, abstractmethod 

70from pyTooling.Warning import CriticalWarning 

71 

72from pyEDAA.Reports import ReportException 

73 

74 

75@export 

76class DocCoverageError(ReportException): 

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

78 

79 

80@export 

81class AlreadyInDocumentationReportError(DocCoverageError, AlreadyInHierarchyError): 

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

83 

84 

85@export 

86class DuplicateDirectoryError(DocCoverageError, DuplicateElementError): 

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

88 

89 

90@export 

91class DuplicateFileError(DocCoverageError, DuplicateElementError): 

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

93 

94 

95@export 

96class SourceFileWarning(CriticalWarning): 

97 """ 

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

99 

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

101 reading, decoding or parsing raised. 

102 

103 .. important:: 

104 

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

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

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

108 """ 

109 

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

111 

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

113 """ 

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

115 

116 :param message: The warning's message. 

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

118 """ 

119 super().__init__(message) 

120 self._file = file 

121 

122 @readonly 

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

124 """ 

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

126 

127 Its path, relative to the analyzed directory, is 

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

129 

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

131 """ 

132 return self._file 

133 

134 

135@export 

136class CoverageState(Flag): 

137 """ 

138 The documentation state of an item, as flags. 

139 

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

141 

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

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

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

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

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

147 """ 

148 

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

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

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

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

153 Covered = 8 #: Counts as documented. 

154 

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

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

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

158 Detailed = 128 #: Documented in detail. 

159 

160 Undocumented = 256 #: Has no documentation. 

161 Documented = 512 #: Has documentation. 

162 

163 Parameters = 1024 #: Its parameters are documented. 

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

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

166 Types = 16384 #: Its types are documented. 

167 

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

169 

170 

171@export 

172class ItemCategory(Enum): 

173 """ 

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

175 

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

177 down the same way. 

178 """ 

179 

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

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

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

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

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

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

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

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

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

189 

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

191 

192 

193@export 

194class ItemKind(Enum): 

195 """ 

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

197 

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

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

200 (:attr:`Category`). 

201 """ 

202 

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

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

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

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

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

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

209 Entity = 7 #: A VHDL entity. 

210 Architecture = 8 #: A VHDL architecture. 

211 Configuration = 9 #: A VHDL configuration. 

212 Context = 10 #: A VHDL context. 

213 

214 Class = 11 #: A class. 

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

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

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

218 Enumeration = 15 #: An enumeration. 

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

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

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

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

223 Component = 20 #: A VHDL component declaration. 

224 

225 Function = 21 #: A function. 

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

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

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

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

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

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

232 Process = 28 #: A VHDL process. 

233 

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

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

236 

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

238 Signal = 32 #: A VHDL signal. 

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

240 Constant = 34 #: A constant. 

241 

242 Generic = 35 #: A VHDL generic. 

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

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

245 Port = 38 #: A VHDL port. 

246 

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

248 Attribute = 40 #: A VHDL attribute declaration. 

249 Other = 41 #: Any other documentable item. 

250 

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

252 

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

254 Library: ItemCategory.Container, 

255 Package: ItemCategory.Container, 

256 PackageBody: ItemCategory.Container, 

257 Namespace: ItemCategory.Container, 

258 Module: ItemCategory.Container, 

259 SourceFile: ItemCategory.Container, 

260 Entity: ItemCategory.Container, 

261 Architecture: ItemCategory.Container, 

262 Configuration: ItemCategory.Container, 

263 Context: ItemCategory.Container, 

264 Class: ItemCategory.Type, 

265 Struct: ItemCategory.Type, 

266 Union: ItemCategory.Type, 

267 Interface: ItemCategory.Type, 

268 Enumeration: ItemCategory.Type, 

269 Record: ItemCategory.Type, 

270 Type: ItemCategory.Type, 

271 TypeAlias: ItemCategory.Type, 

272 TypeVariable: ItemCategory.Type, 

273 Component: ItemCategory.Type, 

274 Function: ItemCategory.Callable, 

275 Method: ItemCategory.Callable, 

276 Constructor: ItemCategory.Callable, 

277 Destructor: ItemCategory.Callable, 

278 Property: ItemCategory.Callable, 

279 Procedure: ItemCategory.Callable, 

280 Macro: ItemCategory.Callable, 

281 Process: ItemCategory.Callable, 

282 Field: ItemCategory.Field, 

283 EnumerationMember: ItemCategory.Field, 

284 Variable: ItemCategory.Variable, 

285 Signal: ItemCategory.Variable, 

286 FileObject: ItemCategory.Variable, 

287 Constant: ItemCategory.Constant, 

288 Generic: ItemCategory.Generic, 

289 TypeParameter: ItemCategory.Generic, 

290 Parameter: ItemCategory.Parameter, 

291 Port: ItemCategory.Parameter, 

292 Alias: ItemCategory.Other, 

293 Attribute: ItemCategory.Other, 

294 Other: ItemCategory.Other, 

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

296 

297 @readonly 

298 def Category(self) -> ItemCategory: 

299 """ 

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

301 

302 :returns: The kind's category. 

303 """ 

304 return self.__CATEGORIES[self.value] 

305 

306 

307@export 

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

309 """ 

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

311 

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

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

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

315 

316 .. code-block:: text 

317 

318 total = expected + ignored + excluded 

319 expected = covered + uncovered 

320 coverage = covered / expected 

321 """ 

322 

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

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

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

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

327 

328 def __init__(self) -> None: 

329 """ 

330 Initializes counters of zero items. 

331 """ 

332 self._expected = 0 

333 self._covered = 0 

334 self._ignored = 0 

335 self._excluded = 0 

336 

337 @readonly 

338 def Total(self) -> int: 

339 """ 

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

341 

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

343 """ 

344 return self._expected + self._ignored + self._excluded 

345 

346 @readonly 

347 def Expected(self) -> int: 

348 """ 

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

350 

351 :returns: The number of expected items. 

352 """ 

353 return self._expected 

354 

355 @readonly 

356 def Covered(self) -> int: 

357 """ 

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

359 

360 :returns: The number of covered items. 

361 """ 

362 return self._covered 

363 

364 @readonly 

365 def Uncovered(self) -> int: 

366 """ 

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

368 

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

370 """ 

371 return self._expected - self._covered 

372 

373 @readonly 

374 def Ignored(self) -> int: 

375 """ 

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

377 (:attr:`_ignored`). 

378 

379 :returns: The number of ignored items. 

380 """ 

381 return self._ignored 

382 

383 @readonly 

384 def Excluded(self) -> int: 

385 """ 

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

387 

388 :returns: The number of excluded items. 

389 """ 

390 return self._excluded 

391 

392 @readonly 

393 def Coverage(self) -> float: 

394 """ 

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

396 

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

398 """ 

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

400 

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

402 """ 

403 Count one item of a coverage state. 

404 

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

406 """ 

407 if CoverageState.Empty in status: 

408 return 

409 elif CoverageState.Excluded in status: 

410 self._excluded += 1 

411 elif CoverageState.Ignored in status: 

412 self._ignored += 1 

413 else: 

414 self._expected += 1 

415 if CoverageState.Covered in status: 

416 self._covered += 1 

417 

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

419 """ 

420 Add the numbers of other counters to these. 

421 

422 :param other: The other counters. 

423 """ 

424 self._expected += other._expected 

425 self._covered += other._covered 

426 self._ignored += other._ignored 

427 self._excluded += other._excluded 

428 

429 def __repr__(self) -> str: 

430 """ 

431 Return a representation of the counters for debugging. 

432 

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

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

435 

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

437 """ 

438 return ( 

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

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

441 ) 

442 

443 

444@export 

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

446 """ 

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

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

449 """ 

450 

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

452 

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

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

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

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

457 

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

459 """ 

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

461 

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

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

464 

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

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

467 """ 

468 if parent is None: 

469 self._parent = None 

470 self._root = None 

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

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

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

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

475 raise ex 

476 else: 

477 self._parent = parent 

478 self._root = parent._root 

479 

480 self._counters = CoverageCounters() 

481 self._categories = {} 

482 

483 @property 

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

485 """ 

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

487 

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

489 element below it. 

490 

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

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

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

494 :attr:`_PARENT_TYPE`. 

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

496 parent. 

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

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

499 of this name. 

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

501 this name. 

502 """ 

503 return self._parent 

504 

505 @Parent.setter 

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

507 if parent is None: 

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

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

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

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

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

513 raise ex 

514 elif self._parent is not None: 

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

516 

517 parent._AddElement(self) 

518 self._parent = parent 

519 self._SetRoot(parent._root) 

520 

521 @readonly 

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

523 """ 

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

525 

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

527 

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

529 """ 

530 return self._root 

531 

532 @readonly 

533 def Counters(self) -> CoverageCounters: 

534 """ 

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

536 

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

538 

539 :returns: The counters. 

540 """ 

541 return self._counters 

542 

543 @readonly 

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

545 """ 

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

547 

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

549 

550 :returns: The counters, by category. 

551 """ 

552 return self._categories 

553 

554 def _ResetCounters(self) -> None: 

555 """ 

556 Set the counters to zero items. 

557 """ 

558 self._counters = CoverageCounters() 

559 self._categories = {} 

560 

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

562 """ 

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

564 

565 :param item: The item. 

566 """ 

567 self._counters._Count(item._status) 

568 category = item._kind.Category 

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

570 counters = CoverageCounters() 

571 self._categories[category] = counters 

572 

573 counters._Count(item._status) 

574 

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

576 """ 

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

578 

579 :param other: The other element, aggregated. 

580 """ 

581 self._counters._Add(other._counters) 

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

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

584 counters = CoverageCounters() 

585 self._categories[category] = counters 

586 

587 counters._Add(otherCounters) 

588 

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

590 """ 

591 Set the analysis' root of this element. 

592 

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

594 

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

596 """ 

597 self._root = root 

598 

599 @abstractmethod 

600 def Aggregate(self) -> None: 

601 """ 

602 Compute the counters. 

603 """ 

604 

605 

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

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

608_Path = Path 

609 

610 

611@export 

612class BaseWithPath(Base): 

613 """ 

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

615 

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

617 """ 

618 

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

620 

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

622 """ 

623 Initializes the parent, the name and the counters. 

624 

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

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

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

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

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

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

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

632 """ 

633 super().__init__(parent=parent) 

634 

635 if name is None: 

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

637 elif not isinstance(name, Path): 

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

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

640 raise ex 

641 elif name.name == "": 

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

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

644 raise ex 

645 elif len(name.parts) > 1: 

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

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

648 raise ex 

649 

650 self._name = name 

651 

652 @readonly 

653 def Name(self) -> Path: 

654 """ 

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

656 

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

658 """ 

659 return self._name 

660 

661 @readonly 

662 def Path(self) -> Path: 

663 """ 

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

665 

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

667 

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

669 """ 

670 if self._parent is None: 

671 return self._name 

672 

673 return self._parent.Path / self._name 

674 

675 

676@export 

677class Directory(BaseWithPath): 

678 """ 

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

680 """ 

681 

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

683 

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

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

686 

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

688 """ 

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

690 

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

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

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

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

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

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

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

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

699 """ 

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

701 

702 self._directories = {} 

703 self._files = {} 

704 

705 if parent is not None: 

706 parent._AddElement(self) 

707 

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

709 """ 

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

711 

712 :param element: The directory or file. 

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

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

715 """ 

716 if isinstance(element, Directory): 

717 elements = self._directories 

718 exception = DuplicateDirectoryError 

719 else: 

720 elements = self._files 

721 exception = DuplicateFileError 

722 

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

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

725 

726 elements[element._name] = element 

727 

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

729 """ 

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

731 

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

733 """ 

734 self._root = root 

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

736 directory._SetRoot(root) 

737 

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

739 file._SetRoot(root) 

740 

741 @readonly 

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

743 """ 

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

745 

746 :returns: The directories, by name. 

747 """ 

748 return self._directories 

749 

750 @readonly 

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

752 """ 

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

754 

755 :returns: The files, by name. 

756 """ 

757 return self._files 

758 

759 @readonly 

760 def FileCount(self) -> int: 

761 """ 

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

763 

764 :returns: The number of files. 

765 """ 

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

767 

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

769 """ 

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

771 

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

773 :returns: The file. 

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

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

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

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

778 directory's. 

779 """ 

780 if path is None: 

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

782 elif not isinstance(path, Path): 

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

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

785 raise ex 

786 

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

788 if len(parts) == 0: 

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

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

791 raise ex 

792 

793 directory = self 

794 for part in parts[:-1]: 

795 if part in directory._files: 

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

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

798 subdirectory = Directory(part, parent=directory) 

799 directory = subdirectory 

800 

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

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

803 

804 return file 

805 

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

807 """ 

808 Iterate the source files in this directory and below. 

809 

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

811 

812 :returns: A generator of the files. 

813 """ 

814 yield from self._files.values() 

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

816 yield from directory.IterateFiles() 

817 

818 def Aggregate(self) -> None: 

819 """ 

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

821 """ 

822 self._ResetCounters() 

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

824 child.Aggregate() 

825 self._AddCounters(child) 

826 

827 def __repr__(self) -> str: 

828 """ 

829 Return a representation of the directory for debugging. 

830 

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

832 

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

834 """ 

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

836 

837 

838Directory._PARENT_TYPE = (Directory, ) 

839 

840 

841@export 

842class DocumentationReport(Directory): 

843 """ 

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

845 top-level items of the logical hierarchy. 

846 

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

848 the items declared in a source file. 

849 """ 

850 

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

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

853 

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

855 """ 

856 Initializes the report of a documentation coverage analysis. 

857 

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

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

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

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

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

863 """ 

864 super().__init__(name) 

865 

866 self._root = self 

867 self._items = [] 

868 self._analysisDuration = None 

869 

870 @Base.Parent.setter 

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

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

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

874 raise ex 

875 

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

877 """ 

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

879 

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

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

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

883 """ 

884 if isinstance(element, BaseWithPath): 

885 super()._AddElement(element) 

886 else: 

887 self._items.append(element) 

888 

889 @readonly 

890 def Path(self) -> Path: 

891 """ 

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

893 

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

895 

896 :returns: ``.`` 

897 """ 

898 return Path(".") 

899 

900 @readonly 

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

902 """ 

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

904 

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

906 """ 

907 return self._items 

908 

909 @readonly 

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

911 """ 

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

913 

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

915 

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

917 """ 

918 return self._analysisDuration 

919 

920 def Aggregate(self) -> None: 

921 """ 

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

923 report's counters sum. 

924 """ 

925 super().Aggregate() 

926 

927 self._ResetCounters() 

928 for item in self._items: 

929 item.Aggregate() 

930 self._AddCounters(item) 

931 

932 

933@export 

934class File(BaseWithPath): 

935 """ 

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

937 

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

939 it contains, which name their file themselves. 

940 """ 

941 

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

943 

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

945 

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

947 """ 

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

949 

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

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

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

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

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

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

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

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

958 """ 

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

960 

961 self._items = [] 

962 

963 if parent is not None: 

964 parent._AddElement(self) 

965 

966 @readonly 

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

968 """ 

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

970 

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

972 """ 

973 return self._items 

974 

975 def Aggregate(self) -> None: 

976 """ 

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

978 """ 

979 self._ResetCounters() 

980 for item in self._items: 

981 self._CountItem(item) 

982 

983 def __repr__(self) -> str: 

984 """ 

985 Return a representation of the file for debugging. 

986 

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

988 

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

990 """ 

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

992 

993 

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

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

996_File = File 

997 

998 

999@export 

1000class Item(Base): 

1001 """ 

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

1003 

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

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

1006 

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

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

1009 """ 

1010 

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

1012 

1013 _name: str #: Name of the item. 

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

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

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

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

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

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

1020 

1021 def __init__( 

1022 self, 

1023 name: str, 

1024 kind: ItemKind, 

1025 status: CoverageState, 

1026 *, 

1027 file: Nullable[File] = None, 

1028 startLine: Nullable[int] = None, 

1029 endLine: Nullable[int] = None, 

1030 parent: Nullable[Item | DocumentationReport] = None 

1031 ) -> None: 

1032 """ 

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

1034 

1035 :param name: Name of the item. 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1056 """ 

1057 super().__init__(parent=parent) 

1058 

1059 if name is None: 

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

1061 elif not isinstance(name, str): 

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

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

1064 raise ex 

1065 elif name == "": 

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

1067 

1068 if kind is None: 

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

1070 elif not isinstance(kind, ItemKind): 

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

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

1073 raise ex 

1074 

1075 if status is None: 

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

1077 elif not isinstance(status, CoverageState): 

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

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

1080 raise ex 

1081 elif status is CoverageState.Unknown: 

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

1083 

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

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

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

1087 raise ex 

1088 

1089 if startLine is not None: 

1090 if not isinstance(startLine, int): 

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

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

1093 raise ex 

1094 elif startLine < 1: 

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

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

1097 raise ex 

1098 

1099 if endLine is not None: 

1100 if not isinstance(endLine, int): 

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

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

1103 raise ex 

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

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

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

1107 raise ex 

1108 

1109 self._name = name 

1110 self._kind = kind 

1111 self._status = status 

1112 self._file = file 

1113 self._startLine = startLine 

1114 self._endLine = endLine 

1115 self._items = [] 

1116 

1117 if parent is not None: 

1118 parent._AddElement(self) 

1119 

1120 if file is not None: 

1121 file._items.append(self) 

1122 

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

1124 """ 

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

1126 

1127 :param item: The item. 

1128 """ 

1129 self._items.append(item) 

1130 

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

1132 """ 

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

1134 

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

1136 """ 

1137 self._root = root 

1138 for item in self._items: 

1139 item._SetRoot(root) 

1140 

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

1142 """ 

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

1144 followed by the items below it. 

1145 

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

1147 item's items are still iterated. 

1148 

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

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

1151 """ 

1152 if predicate is None: 

1153 for item in self._items: 

1154 yield item 

1155 yield from item.IterateDescendants() 

1156 else: 

1157 for item in self._items: 

1158 if predicate(item): 

1159 yield item 

1160 

1161 yield from item.IterateDescendants(predicate) 

1162 

1163 @readonly 

1164 def Name(self) -> str: 

1165 """ 

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

1167 

1168 :returns: The name. 

1169 """ 

1170 return self._name 

1171 

1172 @readonly 

1173 def Kind(self) -> ItemKind: 

1174 """ 

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

1176 

1177 :returns: The item's kind. 

1178 """ 

1179 return self._kind 

1180 

1181 @readonly 

1182 def Category(self) -> ItemCategory: 

1183 """ 

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

1185 

1186 :returns: The item's category. 

1187 """ 

1188 return self._kind.Category 

1189 

1190 @readonly 

1191 def Status(self) -> CoverageState: 

1192 """ 

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

1194 

1195 :returns: The coverage state. 

1196 """ 

1197 return self._status 

1198 

1199 @readonly 

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

1201 """ 

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

1203 

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

1205 """ 

1206 return self._file 

1207 

1208 @readonly 

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

1210 """ 

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

1212 

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

1214 """ 

1215 return self._startLine 

1216 

1217 @readonly 

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

1219 """ 

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

1221 

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

1223 """ 

1224 return self._endLine 

1225 

1226 @readonly 

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

1228 """ 

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

1230 

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

1232 """ 

1233 return self._items 

1234 

1235 @readonly 

1236 def QualifiedName(self) -> str: 

1237 """ 

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

1239 

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

1241 ``myPackage.Shapes.Circle.Area``. 

1242 

1243 :returns: The qualified name. 

1244 """ 

1245 if isinstance(self._parent, Item): 

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

1247 

1248 return self._name 

1249 

1250 def __len__(self) -> int: 

1251 """ 

1252 Return the number of items this one contains. 

1253 

1254 :returns: The number of items. 

1255 """ 

1256 return len(self._items) 

1257 

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

1259 """ 

1260 Iterate the items this one contains. 

1261 

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

1263 """ 

1264 return iter(self._items) 

1265 

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

1267 """ 

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

1269 

1270 :param name: The name. 

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

1272 """ 

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

1274 

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

1276 """ 

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

1278 

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

1280 

1281 :param name: The name. 

1282 :returns: The item. 

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

1284 """ 

1285 for item in self._items: 

1286 if item._name == name: 

1287 return item 

1288 

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

1290 

1291 def Aggregate(self) -> None: 

1292 """ 

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

1294 """ 

1295 self._ResetCounters() 

1296 self._CountItem(self) 

1297 for item in self._items: 

1298 item.Aggregate() 

1299 self._AddCounters(item) 

1300 

1301 def __repr__(self) -> str: 

1302 """ 

1303 Return a representation of the item for debugging. 

1304 

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

1306 

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

1308 """ 

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

1310 

1311 

1312Item._PARENT_TYPE = (Item, DocumentationReport)