Coverage for pyEDAA/Reports/DocumentationCoverage/Python.py: 78%
320 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-16 22:20 +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 assert self._aggregatedUncovered == self._aggregatedExpected - self._aggregatedCovered
211 if self._aggregatedExpected != 0:
212 self._aggregatedCoverage = self._aggregatedCovered / self._aggregatedExpected
213 else:
214 self._aggregatedCoverage = 1.0
217@export
218class ClassCoverage(Class, Coverage):
219 """
220 This class represents the class documentation coverage for Python classes.
221 """
222 _fields: Dict[str, CoverageState]
223 _methods: Dict[str, CoverageState]
224 _classes: Dict[str, "ClassCoverage"]
226 def __init__(self, name: str, parent: Union["PackageCoverage", "ClassCoverage", None] = None) -> None:
227 super().__init__(name, parent)
228 Coverage.__init__(self)
230 if parent is not None:
231 parent._classes[name] = self
233 self._fields = {}
234 self._methods = {}
235 self._classes = {}
237 @readonly
238 def Fields(self) -> Dict[str, CoverageState]:
239 return self._fields
241 @readonly
242 def Methods(self) -> Dict[str, CoverageState]:
243 return self._methods
245 @readonly
246 def Classes(self) -> Dict[str, "ClassCoverage"]:
247 return self._classes
249 def CalculateCoverage(self) -> None:
250 for cls in self._classes.values():
251 cls.CalculateCoverage()
253 self._total, self._excluded, self._ignored, self._expected, self._covered = \
254 self._CountCoverage(zip(
255 self._fields.values(),
256 self._methods.values()
257 ))
259 super().CalculateCoverage()
261 def __str__(self) -> str:
262 return f"<ClassCoverage - tot:{self._total}, ex:{self._excluded}, ig:{self._ignored}, exp:{self._expected}, cov:{self._covered}, un:{self._uncovered} => {self._coverage:.1%}>"
265@export
266class ModuleCoverage(Module, AggregatedCoverage):
267 """
268 This class represents the module documentation coverage for Python modules.
269 """
270 _variables: Dict[str, CoverageState]
271 _functions: Dict[str, CoverageState]
272 _classes: Dict[str, ClassCoverage]
274 def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None:
275 super().__init__(name, parent)
276 AggregatedCoverage.__init__(self, file)
278 if parent is not None:
279 parent._modules[name] = self
281 self._file = file
282 self._variables = {}
283 self._functions = {}
284 self._classes = {}
286 @readonly
287 def Variables(self) -> Dict[str, CoverageState]:
288 return self._variables
290 @readonly
291 def Functions(self) -> Dict[str, CoverageState]:
292 return self._functions
294 @readonly
295 def Classes(self) -> Dict[str, ClassCoverage]:
296 return self._classes
298 def CalculateCoverage(self) -> None:
299 for cls in self._classes.values():
300 cls.CalculateCoverage()
302 self._total, self._excluded, self._ignored, self._expected, self._covered = \
303 self._CountCoverage(zip(
304 self._variables.values(),
305 self._functions.values()
306 ))
308 super().CalculateCoverage()
310 def Aggregate(self) -> None:
311 self._aggregatedTotal = self._total
312 self._aggregatedExcluded = self._excluded
313 self._aggregatedIgnored = self._ignored
314 self._aggregatedExpected = self._expected
315 self._aggregatedCovered = self._covered
316 self._aggregatedUncovered = self._uncovered
318 for cls in self._classes.values():
319 self._aggregatedTotal += cls._total
320 self._aggregatedExcluded += cls._excluded
321 self._aggregatedIgnored += cls._ignored
322 self._aggregatedExpected += cls._expected
323 self._aggregatedCovered += cls._covered
324 self._aggregatedUncovered += cls._uncovered
326 super().Aggregate()
328 def __str__(self) -> str:
329 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%}>"
332@export
333class PackageCoverage(Package, AggregatedCoverage):
334 """
335 This class represents the package documentation coverage for Python packages.
336 """
337 _fileCount: int
338 _variables: Dict[str, CoverageState]
339 _functions: Dict[str, CoverageState]
340 _classes: Dict[str, ClassCoverage]
341 _modules: Dict[str, ModuleCoverage]
342 _packages: Dict[str, "PackageCoverage"]
344 def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None:
345 super().__init__(name, parent)
346 AggregatedCoverage.__init__(self, file)
348 if parent is not None:
349 parent._packages[name] = self
351 self._file = file
352 self._fileCount = 1
353 self._variables = {}
354 self._functions = {}
355 self._classes = {}
356 self._modules = {}
357 self._packages = {}
359 @readonly
360 def FileCount(self) -> int:
361 return self._fileCount
363 @readonly
364 def Variables(self) -> Dict[str, CoverageState]:
365 return self._variables
367 @readonly
368 def Functions(self) -> Dict[str, CoverageState]:
369 return self._functions
371 @readonly
372 def Classes(self) -> Dict[str, ClassCoverage]:
373 return self._classes
375 @readonly
376 def Modules(self) -> Dict[str, ModuleCoverage]:
377 return self._modules
379 @readonly
380 def Packages(self) -> Dict[str, "PackageCoverage"]:
381 return self._packages
383 def __getitem__(self, key: str) -> Union["PackageCoverage", ModuleCoverage]:
384 try:
385 return self._modules[key]
386 except KeyError:
387 return self._packages[key]
389 def CalculateCoverage(self) -> None:
390 for cls in self._classes.values():
391 cls.CalculateCoverage()
393 for mod in self._modules.values():
394 mod.CalculateCoverage()
396 for pkg in self._packages.values():
397 pkg.CalculateCoverage()
399 self._total, self._excluded, self._ignored, self._expected, self._covered = \
400 self._CountCoverage(zip(
401 self._variables.values(),
402 self._functions.values()
403 ))
405 super().CalculateCoverage()
407 def Aggregate(self) -> None:
408 self._fileCount = len(self._modules) + 1
409 self._aggregatedTotal = self._total
410 self._aggregatedExcluded = self._excluded
411 self._aggregatedIgnored = self._ignored
412 self._aggregatedExpected = self._expected
413 self._aggregatedCovered = self._covered
414 self._aggregatedUncovered = self._uncovered
416 for pkg in self._packages.values():
417 pkg.Aggregate()
418 self._fileCount += pkg._fileCount
419 self._aggregatedTotal += pkg._total
420 self._aggregatedExcluded += pkg._excluded
421 self._aggregatedIgnored += pkg._ignored
422 self._aggregatedExpected += pkg._expected
423 self._aggregatedCovered += pkg._covered
424 self._aggregatedUncovered += pkg._uncovered
426 for mod in self._modules.values():
427 mod.Aggregate()
428 self._aggregatedTotal += mod._total
429 self._aggregatedExcluded += mod._excluded
430 self._aggregatedIgnored += mod._ignored
431 self._aggregatedExpected += mod._expected
432 self._aggregatedCovered += mod._covered
433 self._aggregatedUncovered += mod._uncovered
435 super().Aggregate()
437 def __str__(self) -> str:
438 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%}>"
441@export
442class DocStrCoverageError(DocCoverageException):
443 pass
446@export
447class DocStrCoverage(metaclass=ExtendedType):
448 """
449 A wrapper class for the docstr_coverage package and it's analyzer producing a documentation coverage model.
450 """
451 from docstr_coverage import ResultCollection
453 _packageName: str
454 _searchDirectory: Path
455 _moduleFiles: List[Path]
456 _coverageReport: ResultCollection
458 def __init__(self, packageName: str, directory: Path) -> None:
459 if not directory.exists(): 459 ↛ 460line 459 didn't jump to line 460 because the condition on line 459 was never true
460 raise DocStrCoverageError(f"Package source directory '{directory}' does not exist.") from FileNotFoundError(f"Directory '{directory}' does not exist.")
462 self._searchDirectory = directory
463 self._packageName = packageName
464 self._moduleFiles = [file for file in directory.glob("**/*.py")]
466 @readonly
467 def SearchDirectories(self) -> Path:
468 return self._searchDirectory
470 @readonly
471 def PackageName(self) -> str:
472 return self._packageName
474 @readonly
475 def ModuleFiles(self) -> List[Path]:
476 return self._moduleFiles
478 @readonly
479 def CoverageReport(self) -> ResultCollection:
480 return self._coverageReport
482 def Analyze(self) -> ResultCollection:
483 from docstr_coverage import analyze, ResultCollection
485 self._coverageReport: ResultCollection = analyze(self._moduleFiles, show_progress=False)
486 return self._coverageReport
488 def Convert(self) -> PackageCoverage:
489 from docstr_coverage.result_collection import FileCount
491 rootPackageCoverage = PackageCoverage(self._packageName, self._searchDirectory / "__init__.py")
493 for key, value in self._coverageReport.files():
494 path: Path = key.relative_to(self._searchDirectory)
495 perFileResult: FileCount = value.count_aggregate()
497 moduleName = path.stem
498 modulePath = path.parent.parts
500 currentCoverageObject: AggregatedCoverage = rootPackageCoverage
501 for packageName in modulePath:
502 try:
503 currentCoverageObject = currentCoverageObject[packageName]
504 except KeyError:
505 currentCoverageObject = PackageCoverage(packageName, path, currentCoverageObject)
507 if moduleName != "__init__":
508 currentCoverageObject = ModuleCoverage(moduleName, path, currentCoverageObject)
510 currentCoverageObject._expected = perFileResult.needed
511 currentCoverageObject._covered = perFileResult.found
512 currentCoverageObject._uncovered = perFileResult.missing
514 if currentCoverageObject._expected != 0:
515 currentCoverageObject._coverage = currentCoverageObject._covered / currentCoverageObject._expected
516 else:
517 currentCoverageObject._coverage = 1.0
519 if currentCoverageObject._uncovered != currentCoverageObject._expected - currentCoverageObject._covered: 519 ↛ 520line 519 didn't jump to line 520 because the condition on line 519 was never true
520 currentCoverageObject._coverage = -2.0
522 return rootPackageCoverage
524 del ResultCollection