Coverage for pyEDAA / Reports / DocumentationCoverage / Python.py: 78%
319 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:27 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 22:27 +0000
1# ==================================================================================================================== #
2# _____ ____ _ _ ____ _ #
3# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ #
4# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| #
5# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ #
6# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ #
7# |_| |___/ |_| #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2024-2025 Electronic Design Automation Abstraction (EDA²) #
15# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany #
16# #
17# Licensed under the Apache License, Version 2.0 (the "License"); #
18# you may not use this file except in compliance with the License. #
19# You may obtain a copy of the License at #
20# #
21# http://www.apache.org/licenses/LICENSE-2.0 #
22# #
23# Unless required by applicable law or agreed to in writing, software #
24# distributed under the License is distributed on an "AS IS" BASIS, #
25# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
26# See the License for the specific language governing permissions and #
27# limitations under the License. #
28# #
29# SPDX-License-Identifier: Apache-2.0 #
30# ==================================================================================================================== #
31#
32"""
33**Abstract code documentation coverage data model for Python code.**
34"""
35from pathlib import Path
36from typing import Optional as Nullable, Iterable, Dict, Union, Tuple, List
38from pyTooling.Decorators import export, readonly
39from pyTooling.MetaClasses import ExtendedType
41from pyEDAA.Reports.DocumentationCoverage import Class, Module, Package, CoverageState, DocCoverageException
44@export
45class Coverage(metaclass=ExtendedType, mixin=True):
46 """
47 This base-class for :class:`ClassCoverage` and :class:`AggregatedCoverage` represents a basic set of documentation coverage metrics.
49 Besides the *total* number of coverable items, it distinguishes items as *excluded*, *ignored*, and *expected*. |br|
50 Expected items are further distinguished into *covered* and *uncovered* items. |br|
51 If no item is expected, then *coverage* is always 100 |%|.
53 All coverable items
54 total = excluded + ignored + expected
56 All expected items
57 expected = covered + uncovered
59 Coverage [0.00..1.00]
60 coverage = covered / expected
61 """
62 _total: int
63 _excluded: int
64 _ignored: int
65 _expected: int
66 _covered: int
67 _uncovered: int
69 _coverage: float
71 def __init__(self) -> None:
72 self._total = 0
73 self._excluded = 0
74 self._ignored = 0
75 self._expected = 0
76 self._covered = 0
77 self._uncovered = 0
79 self._coverage = -1.0
81 @readonly
82 def Total(self) -> int:
83 return self._total
85 @readonly
86 def Excluded(self) -> int:
87 return self._excluded
89 @readonly
90 def Ignored(self) -> int:
91 return self._ignored
93 @readonly
94 def Expected(self) -> int:
95 return self._expected
97 @readonly
98 def Covered(self) -> int:
99 return self._covered
101 @readonly
102 def Uncovered(self) -> int:
103 return self._uncovered
105 @readonly
106 def Coverage(self) -> float:
107 return self._coverage
109 def CalculateCoverage(self) -> None:
110 self._uncovered = self._expected - self._covered
111 if self._expected != 0:
112 self._coverage = self._covered / self._expected
113 else:
114 self._coverage = 1.0
116 def _CountCoverage(self, iterator: Iterable[CoverageState]) -> Tuple[int, int, int, int, int]:
117 total = 0
118 excluded = 0
119 ignored = 0
120 expected = 0
121 covered = 0
122 for coverageState in iterator:
123 if coverageState is CoverageState.Unknown:
124 raise Exception(f"")
126 total += 1
128 if CoverageState.Excluded in coverageState:
129 excluded += 1
130 elif CoverageState.Ignored in coverageState:
131 ignored += 1
133 expected += 1
134 if CoverageState.Covered in coverageState:
135 covered += 1
137 return total, excluded, ignored, expected, covered
140@export
141class AggregatedCoverage(Coverage, mixin=True):
142 """
143 This base-class for :class:`ModuleCoverage` and :class:`PackageCoverage` represents an extended set of documentation coverage metrics, especially with aggregated metrics.
145 As inherited from :class:`~Coverage`, it provides the *total* number of coverable items, which are distinguished into
146 *excluded*, *ignored*, and *expected* items. |br|
147 Expected items are further distinguished into *covered* and *uncovered* items. |br|
148 If no item is expected, then *coverage* and *aggregated coverage* are always 100 |%|.
150 In addition, all previously mentioned metrics are collected as *aggregated...*, too. |br|
152 All coverable items
153 total = excluded + ignored + expected
155 All expected items
156 expected = covered + uncovered
158 Coverage [0.00..1.00]
159 coverage = covered / expected
160 """
161 _file: Path
163 _aggregatedTotal: int
164 _aggregatedExcluded: int
165 _aggregatedIgnored: int
166 _aggregatedExpected: int
167 _aggregatedCovered: int
168 _aggregatedUncovered: int
170 _aggregatedCoverage: float
172 def __init__(self, file: Path) -> None:
173 super().__init__()
174 self._file = file
176 @readonly
177 def File(self) -> Path:
178 return self._file
180 @readonly
181 def AggregatedTotal(self) -> int:
182 return self._aggregatedTotal
184 @readonly
185 def AggregatedExcluded(self) -> int:
186 return self._aggregatedExcluded
188 @readonly
189 def AggregatedIgnored(self) -> int:
190 return self._aggregatedIgnored
192 @readonly
193 def AggregatedExpected(self) -> int:
194 return self._aggregatedExpected
196 @readonly
197 def AggregatedCovered(self) -> int:
198 return self._aggregatedCovered
200 @readonly
201 def AggregatedUncovered(self) -> int:
202 return self._aggregatedUncovered
204 @readonly
205 def AggregatedCoverage(self) -> float:
206 return self._aggregatedCoverage
208 def Aggregate(self) -> None:
209 if self._aggregatedExpected != 0:
210 self._aggregatedCoverage = self._aggregatedCovered / self._aggregatedExpected
211 else:
212 self._aggregatedCoverage = 1.0
215@export
216class ClassCoverage(Class, Coverage):
217 """
218 This class represents the class documentation coverage for Python classes.
219 """
220 _fields: Dict[str, CoverageState]
221 _methods: Dict[str, CoverageState]
222 _classes: Dict[str, "ClassCoverage"]
224 def __init__(self, name: str, parent: Union["PackageCoverage", "ClassCoverage", None] = None) -> None:
225 super().__init__(name, parent)
226 Coverage.__init__(self)
228 if parent is not None:
229 parent._classes[name] = self
231 self._fields = {}
232 self._methods = {}
233 self._classes = {}
235 @readonly
236 def Fields(self) -> Dict[str, CoverageState]:
237 return self._fields
239 @readonly
240 def Methods(self) -> Dict[str, CoverageState]:
241 return self._methods
243 @readonly
244 def Classes(self) -> Dict[str, "ClassCoverage"]:
245 return self._classes
247 def CalculateCoverage(self) -> None:
248 for cls in self._classes.values():
249 cls.CalculateCoverage()
251 self._total, self._excluded, self._ignored, self._expected, self._covered = \
252 self._CountCoverage(zip(
253 self._fields.values(),
254 self._methods.values()
255 ))
257 super().CalculateCoverage()
259 def __str__(self) -> str:
260 return f"<ClassCoverage - tot:{self._total}, ex:{self._excluded}, ig:{self._ignored}, exp:{self._expected}, cov:{self._covered}, un:{self._uncovered} => {self._coverage:.1%}>"
263@export
264class ModuleCoverage(Module, AggregatedCoverage):
265 """
266 This class represents the module documentation coverage for Python modules.
267 """
268 _variables: Dict[str, CoverageState]
269 _functions: Dict[str, CoverageState]
270 _classes: Dict[str, ClassCoverage]
272 def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None:
273 super().__init__(name, parent)
274 AggregatedCoverage.__init__(self, file)
276 if parent is not None:
277 parent._modules[name] = self
279 self._file = file
280 self._variables = {}
281 self._functions = {}
282 self._classes = {}
284 @readonly
285 def Variables(self) -> Dict[str, CoverageState]:
286 return self._variables
288 @readonly
289 def Functions(self) -> Dict[str, CoverageState]:
290 return self._functions
292 @readonly
293 def Classes(self) -> Dict[str, ClassCoverage]:
294 return self._classes
296 def CalculateCoverage(self) -> None:
297 for cls in self._classes.values():
298 cls.CalculateCoverage()
300 self._total, self._excluded, self._ignored, self._expected, self._covered = \
301 self._CountCoverage(zip(
302 self._variables.values(),
303 self._functions.values()
304 ))
306 super().CalculateCoverage()
308 def Aggregate(self) -> None:
309 self._aggregatedTotal = self._total
310 self._aggregatedExcluded = self._excluded
311 self._aggregatedIgnored = self._ignored
312 self._aggregatedExpected = self._expected
313 self._aggregatedCovered = self._covered
314 self._aggregatedUncovered = self._uncovered
316 for cls in self._classes.values():
317 self._aggregatedTotal += cls._total
318 self._aggregatedExcluded += cls._excluded
319 self._aggregatedIgnored += cls._ignored
320 self._aggregatedExpected += cls._expected
321 self._aggregatedCovered += cls._covered
322 self._aggregatedUncovered += cls._uncovered
324 super().Aggregate()
326 def __str__(self) -> str:
327 return f"<ModuleCoverage - tot:{self._total}|{self._aggregatedTotal}, ex:{self._excluded}|{self._aggregatedExcluded}, ig:{self._ignored}|{self._aggregatedIgnored}, exp:{self._expected}|{self._aggregatedExpected}, cov:{self._covered}|{self._aggregatedCovered}, un:{self._uncovered}|{self._aggregatedUncovered} => {self._coverage:.1%}|{self._aggregatedCoverage:.1%}>"
330@export
331class PackageCoverage(Package, AggregatedCoverage):
332 """
333 This class represents the package documentation coverage for Python packages.
334 """
335 _fileCount: int
336 _variables: Dict[str, CoverageState]
337 _functions: Dict[str, CoverageState]
338 _classes: Dict[str, ClassCoverage]
339 _modules: Dict[str, ModuleCoverage]
340 _packages: Dict[str, "PackageCoverage"]
342 def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None:
343 super().__init__(name, parent)
344 AggregatedCoverage.__init__(self, file)
346 if parent is not None:
347 parent._packages[name] = self
349 self._file = file
350 self._fileCount = 1
351 self._variables = {}
352 self._functions = {}
353 self._classes = {}
354 self._modules = {}
355 self._packages = {}
357 @readonly
358 def FileCount(self) -> int:
359 return self._fileCount
361 @readonly
362 def Variables(self) -> Dict[str, CoverageState]:
363 return self._variables
365 @readonly
366 def Functions(self) -> Dict[str, CoverageState]:
367 return self._functions
369 @readonly
370 def Classes(self) -> Dict[str, ClassCoverage]:
371 return self._classes
373 @readonly
374 def Modules(self) -> Dict[str, ModuleCoverage]:
375 return self._modules
377 @readonly
378 def Packages(self) -> Dict[str, "PackageCoverage"]:
379 return self._packages
381 def __getitem__(self, key: str) -> Union["PackageCoverage", ModuleCoverage]:
382 try:
383 return self._modules[key]
384 except KeyError:
385 return self._packages[key]
387 def CalculateCoverage(self) -> None:
388 for cls in self._classes.values():
389 cls.CalculateCoverage()
391 for mod in self._modules.values():
392 mod.CalculateCoverage()
394 for pkg in self._packages.values():
395 pkg.CalculateCoverage()
397 self._total, self._excluded, self._ignored, self._expected, self._covered = \
398 self._CountCoverage(zip(
399 self._variables.values(),
400 self._functions.values()
401 ))
403 super().CalculateCoverage()
405 def Aggregate(self) -> None:
406 self._fileCount = len(self._modules) + 1
407 self._aggregatedTotal = self._total
408 self._aggregatedExcluded = self._excluded
409 self._aggregatedIgnored = self._ignored
410 self._aggregatedExpected = self._expected
411 self._aggregatedCovered = self._covered
412 self._aggregatedUncovered = self._uncovered
414 for pkg in self._packages.values():
415 pkg.Aggregate()
416 self._fileCount += pkg._fileCount
417 self._aggregatedTotal += pkg._total
418 self._aggregatedExcluded += pkg._excluded
419 self._aggregatedIgnored += pkg._ignored
420 self._aggregatedExpected += pkg._expected
421 self._aggregatedCovered += pkg._covered
422 self._aggregatedUncovered += pkg._uncovered
424 for mod in self._modules.values():
425 mod.Aggregate()
426 self._aggregatedTotal += mod._total
427 self._aggregatedExcluded += mod._excluded
428 self._aggregatedIgnored += mod._ignored
429 self._aggregatedExpected += mod._expected
430 self._aggregatedCovered += mod._covered
431 self._aggregatedUncovered += mod._uncovered
433 super().Aggregate()
435 def __str__(self) -> str:
436 return f"<PackageCoverage - tot:{self._total}|{self._aggregatedTotal}, ex:{self._excluded}|{self._aggregatedExcluded}, ig:{self._ignored}|{self._aggregatedIgnored}, exp:{self._expected}|{self._aggregatedExpected}, cov:{self._covered}|{self._aggregatedCovered}, un:{self._uncovered}|{self._aggregatedUncovered} => {self._coverage:.1%}|{self._aggregatedCoverage:.1%}>"
439@export
440class DocStrCoverageError(DocCoverageException):
441 pass
444@export
445class DocStrCoverage(metaclass=ExtendedType):
446 """
447 A wrapper class for the docstr_coverage package and it's analyzer producing a documentation coverage model.
448 """
449 from docstr_coverage import ResultCollection
451 _packageName: str
452 _searchDirectory: Path
453 _moduleFiles: List[Path]
454 _coverageReport: ResultCollection
456 def __init__(self, packageName: str, directory: Path) -> None:
457 if not directory.exists(): 457 ↛ 458line 457 didn't jump to line 458 because the condition on line 457 was never true
458 raise DocStrCoverageError(f"Package source directory '{directory}' does not exist.") from FileNotFoundError(f"Directory '{directory}' does not exist.")
460 self._searchDirectory = directory
461 self._packageName = packageName
462 self._moduleFiles = [file for file in directory.glob("**/*.py")]
464 @readonly
465 def SearchDirectories(self) -> Path:
466 return self._searchDirectory
468 @readonly
469 def PackageName(self) -> str:
470 return self._packageName
472 @readonly
473 def ModuleFiles(self) -> List[Path]:
474 return self._moduleFiles
476 @readonly
477 def CoverageReport(self) -> ResultCollection:
478 return self._coverageReport
480 def Analyze(self) -> ResultCollection:
481 from docstr_coverage import analyze, ResultCollection
483 self._coverageReport: ResultCollection = analyze(self._moduleFiles, show_progress=False)
484 return self._coverageReport
486 def Convert(self) -> PackageCoverage:
487 from docstr_coverage.result_collection import FileCount
489 rootPackageCoverage = PackageCoverage(self._packageName, self._searchDirectory / "__init__.py")
491 for key, value in self._coverageReport.files():
492 path: Path = key.relative_to(self._searchDirectory)
493 perFileResult: FileCount = value.count_aggregate()
495 moduleName = path.stem
496 modulePath = path.parent.parts
498 currentCoverageObject: AggregatedCoverage = rootPackageCoverage
499 for packageName in modulePath:
500 try:
501 currentCoverageObject = currentCoverageObject[packageName]
502 except KeyError:
503 currentCoverageObject = PackageCoverage(packageName, path, currentCoverageObject)
505 if moduleName != "__init__":
506 currentCoverageObject = ModuleCoverage(moduleName, path, currentCoverageObject)
508 currentCoverageObject._expected = perFileResult.needed
509 currentCoverageObject._covered = perFileResult.found
510 currentCoverageObject._uncovered = perFileResult.missing
512 if currentCoverageObject._expected != 0:
513 currentCoverageObject._coverage = currentCoverageObject._covered / currentCoverageObject._expected
514 else:
515 currentCoverageObject._coverage = 1.0
517 if currentCoverageObject._uncovered != currentCoverageObject._expected - currentCoverageObject._covered: 517 ↛ 518line 517 didn't jump to line 518 because the condition on line 517 was never true
518 currentCoverageObject._coverage = -2.0
520 return rootPackageCoverage
522 del ResultCollection