From 08a87782e7e89a34d2735d1b865ce5832b0914a8 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Thu, 22 Feb 2024 10:48:38 +0100 Subject: [PATCH 1/3] Added Python documentation coverage. --- .idea/pyEDAA.Reports.iml | 2 +- doc/conf.py | 5 +- .../Reports/DocumentationCoverage/Python.py | 394 ++++++++++++++++++ .../Reports/DocumentationCoverage/__init__.py | 81 +++- pyEDAA/Reports/Unittesting/JUnit.py | 3 +- pyEDAA/Reports/Unittesting/__init__.py | 2 +- tests/unit/DocumentationCoverage/DataModel.py | 61 +++ tests/unit/DocumentationCoverage/__init__.py | 0 8 files changed, 537 insertions(+), 11 deletions(-) create mode 100644 pyEDAA/Reports/DocumentationCoverage/Python.py create mode 100644 tests/unit/DocumentationCoverage/DataModel.py create mode 100644 tests/unit/DocumentationCoverage/__init__.py diff --git a/.idea/pyEDAA.Reports.iml b/.idea/pyEDAA.Reports.iml index 2f3ab18d..989330a9 100644 --- a/.idea/pyEDAA.Reports.iml +++ b/.idea/pyEDAA.Reports.iml @@ -2,8 +2,8 @@ - + diff --git a/doc/conf.py b/doc/conf.py index 97f24903..34518e8d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -182,7 +182,10 @@ # Sphinx.Ext.InterSphinx # ============================================================================== intersphinx_mapping = { - "python": ("https://docs.python.org/3", None), + "python": ("https://docs.python.org/3", None), + "pytooling": ("https://pytooling.github.io/pyTooling", None), + "ucis": ("https://edaa-org.github.io/pyEDAA.UCIS", None), + "ghdl": ("https://ghdl.github.io/ghdl", None), } diff --git a/pyEDAA/Reports/DocumentationCoverage/Python.py b/pyEDAA/Reports/DocumentationCoverage/Python.py new file mode 100644 index 00000000..d9e3b4d9 --- /dev/null +++ b/pyEDAA/Reports/DocumentationCoverage/Python.py @@ -0,0 +1,394 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# +""" +**Abstract documentation coverage data model for Python code.** +""" +from pathlib import Path +from typing import Optional as Nullable, Iterable, Dict, Union, Tuple + +from pyTooling.Decorators import export, readonly +from pyTooling.MetaClasses import ExtendedType + +from pyEDAA.Reports.DocumentationCoverage import Class, Module, Package, CoverageState + + +@export +class Coverage(metaclass=ExtendedType, mixin=True): + _total: int + _excluded: int + _ignored: int + _expected: int + _covered: int + _uncovered: int + + _coverage: float + + def __init__(self) -> None: + self._total = 0 + self._excluded = 0 + self._ignored = 0 + self._expected = 0 + self._covered = 0 + self._uncovered = 0 + + self._coverage = -1.0 + + @readonly + def Total(self) -> int: + return self._total + + @readonly + def Excluded(self) -> int: + return self._excluded + + @readonly + def Ignored(self) -> int: + return self._ignored + + @readonly + def Expected(self) -> int: + return self._expected + + @readonly + def Covered(self) -> int: + return self._covered + + @readonly + def Uncovered(self) -> int: + return self._uncovered + + @readonly + def Coverage(self) -> float: + return self._coverage + + def CalculateCoverage(self) -> None: + self._uncovered = self._expected - self._covered + if self._expected != 0: + self._coverage = self._covered / self._expected + else: + self._coverage = 1.0 + + def _CountCoverage(self, iterator: Iterable[CoverageState]) -> Tuple[int, int, int, int, int]: + total = 0 + excluded = 0 + ignored = 0 + expected = 0 + covered = 0 + for coverageState in iterator: + if coverageState is CoverageState.Unknown: + raise Exception(f"") + + total += 1 + + if CoverageState.Excluded in coverageState: + excluded += 1 + elif CoverageState.Ignored in coverageState: + ignored += 1 + + expected += 1 + if CoverageState.Covered in coverageState: + covered += 1 + + return total, excluded, ignored, expected, covered + + +@export +class AggregatedCoverage(Coverage, mixin=True): + _file: Path + + _aggregatedTotal: int + _aggregatedExcluded: int + _aggregatedIgnored: int + _aggregatedExpected: int + _aggregatedCovered: int + _aggregatedUncovered: int + + _aggregatedCoverage: float + + def __init__(self, file: Path) -> None: + super().__init__() + self._file = file + + @readonly + def File(self) -> Path: + return self._file + + @readonly + def AggregatedTotal(self) -> int: + return self._aggregatedTotal + + @readonly + def AggregatedExcluded(self) -> int: + return self._aggregatedExcluded + + @readonly + def AggregatedIgnored(self) -> int: + return self._aggregatedIgnored + + @readonly + def AggregatedExpected(self) -> int: + return self._aggregatedExpected + + @readonly + def AggregatedCovered(self) -> int: + return self._aggregatedCovered + + @readonly + def AggregatedUncovered(self) -> int: + return self._aggregatedUncovered + + @readonly + def AggregatedCoverage(self) -> float: + return self._aggregatedCoverage + + def Aggregate(self) -> None: + assert self._aggregatedUncovered == self._aggregatedExpected - self._aggregatedCovered + + if self._aggregatedExpected != 0: + self._aggregatedCoverage = self._aggregatedCovered / self._aggregatedExpected + else: + self._aggregatedCoverage = 1.0 + + +@export +class ClassCoverage(Class, Coverage): + _fields: Dict[str, CoverageState] + _methods: Dict[str, CoverageState] + _classes: Dict[str, "ClassCoverage"] + + def __init__(self, name: str, parent: Union["PackageCoverage", "ClassCoverage", None] = None) -> None: + super().__init__(name, parent) + Coverage.__init__(self) + + if parent is not None: + parent._classes[name] = self + + self._fields = {} + self._methods = {} + self._classes = {} + + @readonly + def Fields(self) -> Dict[str, CoverageState]: + return self._fields + + @readonly + def Methods(self) -> Dict[str, CoverageState]: + return self._methods + + @readonly + def Classes(self) -> Dict[str, "ClassCoverage"]: + return self._classes + + def CalculateCoverage(self) -> None: + for cls in self._classes.values(): + cls.CalculateCoverage() + + self._total, self._excluded, self._ignored, self._expected, self._covered = \ + self._CountCoverage(zip( + self._fields.values(), + self._methods.values() + )) + + super().CalculateCoverage() + + def __str__(self) -> str: + return f" {self._coverage:.1%}>" + + +@export +class ModuleCoverage(Module, AggregatedCoverage): + _variables: Dict[str, CoverageState] + _functions: Dict[str, CoverageState] + _classes: Dict[str, ClassCoverage] + + def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None: + super().__init__(name, parent) + AggregatedCoverage.__init__(self, file) + + if parent is not None: + parent._modules[name] = self + + self._file = file + self._variables = {} + self._functions = {} + self._classes = {} + + @readonly + def Variables(self) -> Dict[str, CoverageState]: + return self._variables + + @readonly + def Functions(self) -> Dict[str, CoverageState]: + return self._functions + + @readonly + def Classes(self) -> Dict[str, ClassCoverage]: + return self._classes + + def CalculateCoverage(self) -> None: + for cls in self._classes.values(): + cls.CalculateCoverage() + + self._total, self._excluded, self._ignored, self._expected, self._covered = \ + self._CountCoverage(zip( + self._variables.values(), + self._functions.values() + )) + + super().CalculateCoverage() + + def Aggregate(self) -> None: + self._aggregatedTotal = self._total + self._aggregatedExcluded = self._excluded + self._aggregatedIgnored = self._ignored + self._aggregatedExpected = self._expected + self._aggregatedCovered = self._covered + self._aggregatedUncovered = self._uncovered + + for cls in self._classes.values(): + self._aggregatedTotal += cls._total + self._aggregatedExcluded += cls._excluded + self._aggregatedIgnored += cls._ignored + self._aggregatedExpected += cls._expected + self._aggregatedCovered += cls._covered + self._aggregatedUncovered += cls._uncovered + + super().Aggregate() + + def __str__(self) -> str: + return f" {self._coverage:.1%}|{self._aggregatedCoverage:.1%}>" + + +@export +class PackageCoverage(Package, AggregatedCoverage): + _fileCount: int + _variables: Dict[str, CoverageState] + _functions: Dict[str, CoverageState] + _classes: Dict[str, ClassCoverage] + _modules: Dict[str, ModuleCoverage] + _packages: Dict[str, "PackageCoverage"] + + def __init__(self, name: str, file: Path, parent: Nullable["PackageCoverage"] = None) -> None: + super().__init__(name, parent) + AggregatedCoverage.__init__(self, file) + + if parent is not None: + parent._packages[name] = self + + self._file = file + self._fileCount = 1 + self._variables = {} + self._functions = {} + self._classes = {} + self._modules = {} + self._packages = {} + + @readonly + def FileCount(self) -> int: + return self._fileCount + + @readonly + def Variables(self) -> Dict[str, CoverageState]: + return self._variables + + @readonly + def Functions(self) -> Dict[str, CoverageState]: + return self._functions + + @readonly + def Classes(self) -> Dict[str, ClassCoverage]: + return self._classes + + @readonly + def Modules(self) -> Dict[str, ModuleCoverage]: + return self._modules + + @readonly + def Packages(self) -> Dict[str, "PackageCoverage"]: + return self._packages + + def __getitem__(self, key: str) -> Union["PackageCoverage", ModuleCoverage]: + try: + return self._modules[key] + except KeyError: + return self._packages[key] + + def CalculateCoverage(self) -> None: + for cls in self._classes.values(): + cls.CalculateCoverage() + + for mod in self._modules.values(): + mod.CalculateCoverage() + + for pkg in self._packages.values(): + pkg.CalculateCoverage() + + self._total, self._excluded, self._ignored, self._expected, self._covered = \ + self._CountCoverage(zip( + self._variables.values(), + self._functions.values() + )) + + super().CalculateCoverage() + + def Aggregate(self) -> None: + self._fileCount = len(self._modules) + 1 + self._aggregatedTotal = self._total + self._aggregatedExcluded = self._excluded + self._aggregatedIgnored = self._ignored + self._aggregatedExpected = self._expected + self._aggregatedCovered = self._covered + self._aggregatedUncovered = self._uncovered + + for pkg in self._packages.values(): + pkg.Aggregate() + self._fileCount += pkg._fileCount + self._aggregatedTotal += pkg._total + self._aggregatedExcluded += pkg._excluded + self._aggregatedIgnored += pkg._ignored + self._aggregatedExpected += pkg._expected + self._aggregatedCovered += pkg._covered + self._aggregatedUncovered += pkg._uncovered + + for mod in self._modules.values(): + mod.Aggregate() + self._aggregatedTotal += mod._total + self._aggregatedExcluded += mod._excluded + self._aggregatedIgnored += mod._ignored + self._aggregatedExpected += mod._expected + self._aggregatedCovered += mod._covered + self._aggregatedUncovered += mod._uncovered + + super().Aggregate() + + def __str__(self) -> str: + return f" {self._coverage:.1%}|{self._aggregatedCoverage:.1%}>" diff --git a/pyEDAA/Reports/DocumentationCoverage/__init__.py b/pyEDAA/Reports/DocumentationCoverage/__init__.py index 936793ce..0f3604e8 100644 --- a/pyEDAA/Reports/DocumentationCoverage/__init__.py +++ b/pyEDAA/Reports/DocumentationCoverage/__init__.py @@ -29,18 +29,85 @@ # ==================================================================================================================== # # """Abstraction of code documentation coverage.""" -from enum import Flag +from enum import Flag +from typing import Dict, Iterator, Optional as Nullable -from pyTooling.Decorators import export +from pyTooling.Decorators import export +from pyTooling.MetaClasses import ExtendedType @export -class Status(Flag): +class CoverageState(Flag): Unknown = 0 - Ignored = 1 - Undocumented = 2 - Documented = 4 - Inherited = 12 + Excluded = 1 + Ignored = 2 + Empty = 4 + Covered = 8 + + Weak = 16 + Incomplete = 32 + Inherited = 64 + Detailed = 128 + + Undocumented = 256 + Documented = 512 + + Parameters = 1024 + ReturnValue = 2048 + Exceptions = 8192 + Types = 16384 # unrequiredButDocumented # wrongly documented + + +@export +class Base(metaclass=ExtendedType, slots=True): + _parent: Nullable["Base"] + _name: str + _status: CoverageState + + def __init__(self, name: str, parent: Nullable["Base"] = None): + if name is None: + raise TypeError(f"Parameter 'name' must not be None.") + + self._parent = parent + self._name = name + self._status = CoverageState.Unknown + + @property + def Parent(self) -> Nullable["Base"]: + return self._parent + + @property + def Name(self) -> str: + return self._name + + @property + def Status(self) -> CoverageState: + return self._status + + +@export +class _Type(Base): + pass + + +@export +class Class(_Type): + pass + + +@export +class _Unit(Base): + pass + + +@export +class Module(_Unit): + pass + + +@export +class Package(_Unit): + pass diff --git a/pyEDAA/Reports/Unittesting/JUnit.py b/pyEDAA/Reports/Unittesting/JUnit.py index 344ba66d..8fe63845 100644 --- a/pyEDAA/Reports/Unittesting/JUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit.py @@ -11,7 +11,8 @@ # # # License: # # ==================================================================================================================== # -# Copyright 2021-2024 Electronic Design Automation Abstraction (EDA²) # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# Copyright 2023-2023 Patrick Lehmann - Bötzingen, Germany # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # diff --git a/pyEDAA/Reports/Unittesting/__init__.py b/pyEDAA/Reports/Unittesting/__init__.py index 58a51b56..93904108 100644 --- a/pyEDAA/Reports/Unittesting/__init__.py +++ b/pyEDAA/Reports/Unittesting/__init__.py @@ -11,7 +11,7 @@ # # # License: # # ==================================================================================================================== # -# Copyright 2021-2024 Electronic Design Automation Abstraction (EDA²) # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # diff --git a/tests/unit/DocumentationCoverage/DataModel.py b/tests/unit/DocumentationCoverage/DataModel.py new file mode 100644 index 00000000..0ddc0705 --- /dev/null +++ b/tests/unit/DocumentationCoverage/DataModel.py @@ -0,0 +1,61 @@ +from pathlib import Path +from unittest import TestCase + +from pyEDAA.Reports.DocumentationCoverage.Python import CoverageState, ClassCoverage, ModuleCoverage, PackageCoverage + + +class ClassCoverageInstantiation(TestCase): + def test_ClassCoverage_NoName(self) -> None: + with self.assertRaises(TypeError): + _ = ClassCoverage(None) + + def test_ClassCoverage_Name(self) -> None: + cc = ClassCoverage("class") + + self.assertIsNone(cc.Parent) + self.assertEqual("class", cc.Name) + # self.assertEqual(CoverageState.Unknown, cc.Status) + self.assertEqual(0, len(cc.Fields)) + self.assertEqual(0, len(cc.Methods)) + self.assertEqual(0, len(cc.Classes)) + + +class ModuleCoverageInstantiation(TestCase): + def test_ModuleCoverage_NoName(self) -> None: + with self.assertRaises(TypeError): + _ = ModuleCoverage(None, Path("module.py")) + + def test_ModuleCoverage_Name(self) -> None: + mc = ModuleCoverage("module", Path("module.py")) + + self.assertIsNone(mc.Parent) + self.assertEqual("module", mc.Name) + # self.assertEqual(CoverageState.Unknown, mc.Status) + + +class PackageCoverageInstantiation(TestCase): + def test_PackageCoverage_NoName(self) -> None: + with self.assertRaises(TypeError): + _ = PackageCoverage(None, Path("package")) + + def test_PackageCoverage_Name(self) -> None: + pc = PackageCoverage("package", Path("package")) + + self.assertIsNone(pc.Parent) + self.assertEqual("package", pc.Name) + # self.assertEqual(CoverageState.Unknown, pc.Status) + + +class Hierarchy(TestCase): + def test_Hierarchy1(self) -> None: + pc = PackageCoverage("package", Path("package")) + mc1 = ModuleCoverage("module1", Path("module1.py"), parent=pc) + mc2 = ModuleCoverage("module2", Path("module2.py"), parent=pc) + cc11 = ClassCoverage("class11", parent=mc1) + cc12 = ClassCoverage("class12", parent=mc1) + cc21 = ClassCoverage("class21", parent=mc2) + cc22 = ClassCoverage("class22", parent=mc2) + + pc.Aggregate() + + diff --git a/tests/unit/DocumentationCoverage/__init__.py b/tests/unit/DocumentationCoverage/__init__.py new file mode 100644 index 00000000..e69de29b From 7add2492b44dc0ae0a155d0677a30c1fd07aebb2 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Thu, 22 Feb 2024 12:25:10 +0100 Subject: [PATCH 2/3] Added analysis with DocStr_Coverage and unit tests. --- doc/prolog.inc | 3 + .../Reports/DocumentationCoverage/Python.py | 140 +++++++++++++++++- .../Reports/DocumentationCoverage/__init__.py | 9 +- tests/unit/DocumentationCoverage/DataModel.py | 8 +- .../DocumentationCoverage/DocStrCoverage.py | 16 ++ tests/unit/requirements.txt | 2 +- 6 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 tests/unit/DocumentationCoverage/DocStrCoverage.py diff --git a/doc/prolog.inc b/doc/prolog.inc index 75463a87..fed9710a 100644 --- a/doc/prolog.inc +++ b/doc/prolog.inc @@ -14,6 +14,9 @@
+.. |%| unicode:: U+2009 % + :trim: + .. # define additional CSS based styles and ReST roles for HTML .. raw:: html diff --git a/pyEDAA/Reports/DocumentationCoverage/Python.py b/pyEDAA/Reports/DocumentationCoverage/Python.py index d9e3b4d9..cb538737 100644 --- a/pyEDAA/Reports/DocumentationCoverage/Python.py +++ b/pyEDAA/Reports/DocumentationCoverage/Python.py @@ -32,17 +32,33 @@ """ **Abstract documentation coverage data model for Python code.** """ -from pathlib import Path -from typing import Optional as Nullable, Iterable, Dict, Union, Tuple +from pathlib import Path +from typing import Optional as Nullable, Iterable, Dict, Union, Tuple, List -from pyTooling.Decorators import export, readonly -from pyTooling.MetaClasses import ExtendedType +from pyTooling.Decorators import export, readonly +from pyTooling.MetaClasses import ExtendedType -from pyEDAA.Reports.DocumentationCoverage import Class, Module, Package, CoverageState +from pyEDAA.Reports.DocumentationCoverage import Class, Module, Package, CoverageState, DocCoverageException @export class Coverage(metaclass=ExtendedType, mixin=True): + """ + This base-class for :class:`ClassCoverage` and :class:`AggregatedCoverage` represents a basic set of documentation coverage metrics. + + Besides the *total* number of coverable items, it distinguishes items as *excluded*, *ignored*, and *expected*. |br| + Expected items are further distinguished into *covered* and *uncovered* items. |br| + If no item is expected, then *coverage* is always 100 |%|. + + All coverable items + total = excluded + ignored + expected + + All expected items + expected = covered + uncovered + + Coverage [0.00..1.00] + coverage = covered / expected + """ _total: int _excluded: int _ignored: int @@ -123,6 +139,25 @@ def _CountCoverage(self, iterator: Iterable[CoverageState]) -> Tuple[int, int, i @export class AggregatedCoverage(Coverage, mixin=True): + """ + This base-class for :class:`ModuleCoverage` and :class:`PackageCoverage` represents an extended set of documentation coverage metrics, especially with aggregated metrics. + + As inherited from :class:`~Coverage`, it provides the *total* number of coverable items, which are distinguished into + *excluded*, *ignored*, and *expected* items. |br| + Expected items are further distinguished into *covered* and *uncovered* items. |br| + If no item is expected, then *coverage* and *aggregated coverage* are always 100 |%|. + + In addition, all previously mentioned metrics are collected as *aggregated...*, too. |br| + + All coverable items + total = excluded + ignored + expected + + All expected items + expected = covered + uncovered + + Coverage [0.00..1.00] + coverage = covered / expected + """ _file: Path _aggregatedTotal: int @@ -181,6 +216,9 @@ def Aggregate(self) -> None: @export class ClassCoverage(Class, Coverage): + """ + This class represents the class documentation coverage for Python classes. + """ _fields: Dict[str, CoverageState] _methods: Dict[str, CoverageState] _classes: Dict[str, "ClassCoverage"] @@ -226,6 +264,9 @@ def __str__(self) -> str: @export class ModuleCoverage(Module, AggregatedCoverage): + """ + This class represents the module documentation coverage for Python modules. + """ _variables: Dict[str, CoverageState] _functions: Dict[str, CoverageState] _classes: Dict[str, ClassCoverage] @@ -290,6 +331,9 @@ def __str__(self) -> str: @export class PackageCoverage(Package, AggregatedCoverage): + """ + This class represents the package documentation coverage for Python packages. + """ _fileCount: int _variables: Dict[str, CoverageState] _functions: Dict[str, CoverageState] @@ -392,3 +436,89 @@ def Aggregate(self) -> None: def __str__(self) -> str: return f" {self._coverage:.1%}|{self._aggregatedCoverage:.1%}>" + + +@export +class DocStrCoverageError(DocCoverageException): + pass + + +@export +class DocStrCoverage(metaclass=ExtendedType): + """ + A wrapper class for the docstr_coverage package and it's analyzer producing a documentation coverage model. + """ + from docstr_coverage import ResultCollection + + _packageName: str + _searchDirectory: Path + _moduleFiles: List[Path] + _coverageReport: ResultCollection + + def __init__(self, packageName: str, directory: Path) -> None: + if not directory.exists(): + raise DocStrCoverageError(f"Package source directory '{directory}' does not exist.") from FileNotFoundError(f"Directory '{directory}' does not exist.") + + self._searchDirectory = directory + self._packageName = packageName + self._moduleFiles = [file for file in directory.glob("**/*.py")] + + @readonly + def SearchDirectories(self) -> Path: + return self._searchDirectory + + @readonly + def PackageName(self) -> str: + return self._packageName + + @readonly + def ModuleFiles(self) -> List[Path]: + return self._moduleFiles + + @readonly + def CoverageReport(self) -> ResultCollection: + return self._coverageReport + + def Analyze(self) -> ResultCollection: + from docstr_coverage import analyze, ResultCollection + + self._coverageReport: ResultCollection = analyze(self._moduleFiles, show_progress=False) + return self._coverageReport + + def Convert(self) -> PackageCoverage: + from docstr_coverage.result_collection import FileCount + + rootPackageCoverage = PackageCoverage(self._packageName, self._searchDirectory / "__init__.py") + + for key, value in self._coverageReport.files(): + path: Path = key.relative_to(self._searchDirectory) + perFileResult: FileCount = value.count_aggregate() + + moduleName = path.stem + modulePath = path.parent.parts + + currentCoverageObject: AggregatedCoverage = rootPackageCoverage + for packageName in modulePath: + try: + currentCoverageObject = currentCoverageObject[packageName] + except KeyError: + currentCoverageObject = PackageCoverage(packageName, path, currentCoverageObject) + + if moduleName != "__init__": + currentCoverageObject = ModuleCoverage(moduleName, path, currentCoverageObject) + + currentCoverageObject._expected = perFileResult.needed + currentCoverageObject._covered = perFileResult.found + currentCoverageObject._uncovered = perFileResult.missing + + if currentCoverageObject._expected != 0: + currentCoverageObject._coverage = currentCoverageObject._covered / currentCoverageObject._expected + else: + currentCoverageObject._coverage = 1.0 + + if currentCoverageObject._uncovered != currentCoverageObject._expected - currentCoverageObject._covered: + currentCoverageObject._coverage = -2.0 + + return rootPackageCoverage + + del ResultCollection diff --git a/pyEDAA/Reports/DocumentationCoverage/__init__.py b/pyEDAA/Reports/DocumentationCoverage/__init__.py index 0f3604e8..8d515ce7 100644 --- a/pyEDAA/Reports/DocumentationCoverage/__init__.py +++ b/pyEDAA/Reports/DocumentationCoverage/__init__.py @@ -30,11 +30,18 @@ # """Abstraction of code documentation coverage.""" from enum import Flag -from typing import Dict, Iterator, Optional as Nullable +from typing import Optional as Nullable from pyTooling.Decorators import export from pyTooling.MetaClasses import ExtendedType +from pyEDAA.Reports import ReportException + + +@export +class DocCoverageException(ReportException): + pass + @export class CoverageState(Flag): diff --git a/tests/unit/DocumentationCoverage/DataModel.py b/tests/unit/DocumentationCoverage/DataModel.py index 0ddc0705..fa9321da 100644 --- a/tests/unit/DocumentationCoverage/DataModel.py +++ b/tests/unit/DocumentationCoverage/DataModel.py @@ -14,7 +14,7 @@ def test_ClassCoverage_Name(self) -> None: self.assertIsNone(cc.Parent) self.assertEqual("class", cc.Name) - # self.assertEqual(CoverageState.Unknown, cc.Status) + self.assertEqual(CoverageState.Unknown, cc.Status) self.assertEqual(0, len(cc.Fields)) self.assertEqual(0, len(cc.Methods)) self.assertEqual(0, len(cc.Classes)) @@ -30,7 +30,7 @@ def test_ModuleCoverage_Name(self) -> None: self.assertIsNone(mc.Parent) self.assertEqual("module", mc.Name) - # self.assertEqual(CoverageState.Unknown, mc.Status) + self.assertEqual(CoverageState.Unknown, mc.Status) class PackageCoverageInstantiation(TestCase): @@ -43,7 +43,7 @@ def test_PackageCoverage_Name(self) -> None: self.assertIsNone(pc.Parent) self.assertEqual("package", pc.Name) - # self.assertEqual(CoverageState.Unknown, pc.Status) + self.assertEqual(CoverageState.Unknown, pc.Status) class Hierarchy(TestCase): @@ -57,5 +57,3 @@ def test_Hierarchy1(self) -> None: cc22 = ClassCoverage("class22", parent=mc2) pc.Aggregate() - - diff --git a/tests/unit/DocumentationCoverage/DocStrCoverage.py b/tests/unit/DocumentationCoverage/DocStrCoverage.py new file mode 100644 index 00000000..6346534a --- /dev/null +++ b/tests/unit/DocumentationCoverage/DocStrCoverage.py @@ -0,0 +1,16 @@ +from pathlib import Path +from unittest import TestCase + +from pyEDAA.Reports.DocumentationCoverage import CoverageState +from pyEDAA.Reports.DocumentationCoverage.Python import DocStrCoverage + + +class Analyze(TestCase): + def test_Analyze(self) -> None: + docStrCov = DocStrCoverage("pyEDAA.Reports", Path("pyEDAA")) + docStrCov.Analyze() + cov = docStrCov.Convert() + cov.Aggregate() + + self.assertEqual(CoverageState.Unknown, cov.Status) + self.assertGreaterEqual(cov.AggregatedCoverage, 0.20) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index a9fd31a8..e15f58e1 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -1,3 +1,3 @@ -r ../requirements.txt -setuptools >= 69.0.0 +docstr_coverage ~= 2.3 From eda36b99e412e66f6faed3f6296ad9e6ba96866c Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Thu, 22 Feb 2024 13:25:27 +0100 Subject: [PATCH 3/3] Updated file headers. --- .../Reports/DocumentationCoverage/Python.py | 2 +- .../Reports/DocumentationCoverage/__init__.py | 2 +- pyEDAA/Reports/Unittesting/JUnit.py | 18 +++++----- pyEDAA/Reports/Unittesting/OSVVM.py | 8 ++--- pyEDAA/Reports/__init__.py | 13 ++++---- tests/__init__.py | 30 +++++++++++++++++ tests/unit/DocumentationCoverage/DataModel.py | 32 +++++++++++++++++- .../DocumentationCoverage/DocStrCoverage.py | 30 +++++++++++++++++ tests/unit/DocumentationCoverage/__init__.py | 30 +++++++++++++++++ tests/unit/Unittesting/DataModel.py | 33 +++++++++++++++++-- tests/unit/Unittesting/JUnit.py | 33 +++++++++++++++++-- tests/unit/Unittesting/__init__.py | 30 +++++++++++++++++ tests/unit/__init__.py | 30 +++++++++++++++++ 13 files changed, 265 insertions(+), 26 deletions(-) diff --git a/pyEDAA/Reports/DocumentationCoverage/Python.py b/pyEDAA/Reports/DocumentationCoverage/Python.py index cb538737..a6898eb1 100644 --- a/pyEDAA/Reports/DocumentationCoverage/Python.py +++ b/pyEDAA/Reports/DocumentationCoverage/Python.py @@ -30,7 +30,7 @@ # ==================================================================================================================== # # """ -**Abstract documentation coverage data model for Python code.** +**Abstract code documentation coverage data model for Python code.** """ from pathlib import Path from typing import Optional as Nullable, Iterable, Dict, Union, Tuple, List diff --git a/pyEDAA/Reports/DocumentationCoverage/__init__.py b/pyEDAA/Reports/DocumentationCoverage/__init__.py index 8d515ce7..2bb411be 100644 --- a/pyEDAA/Reports/DocumentationCoverage/__init__.py +++ b/pyEDAA/Reports/DocumentationCoverage/__init__.py @@ -28,7 +28,7 @@ # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # -"""Abstraction of code documentation coverage.""" +"""Abstraction of code documentation coverage data model.""" from enum import Flag from typing import Optional as Nullable diff --git a/pyEDAA/Reports/Unittesting/JUnit.py b/pyEDAA/Reports/Unittesting/JUnit.py index 8fe63845..8f9cd872 100644 --- a/pyEDAA/Reports/Unittesting/JUnit.py +++ b/pyEDAA/Reports/Unittesting/JUnit.py @@ -29,16 +29,18 @@ # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # -from enum import Flag - -from datetime import datetime, timedelta -from pathlib import Path -from time import perf_counter_ns -from typing import Tuple, List, Dict, Optional as Nullable, Union -from xml.dom import minidom, Node +""" +Reader for JUnit unit testing summary files in XML format. +""" +from enum import Flag +from datetime import datetime, timedelta +from pathlib import Path +from time import perf_counter_ns +from typing import Tuple, Dict, Optional as Nullable, Union +from xml.dom import minidom, Node from xml.dom.minidom import Element -from pyTooling.Decorators import export, readonly +from pyTooling.Decorators import export, readonly from pyTooling.MetaClasses import ExtendedType from pyEDAA.Reports.Unittesting import UnittestException, DuplicateTestsuiteException, DuplicateTestcaseException diff --git a/pyEDAA/Reports/Unittesting/OSVVM.py b/pyEDAA/Reports/Unittesting/OSVVM.py index f2a6865a..20a5cea0 100644 --- a/pyEDAA/Reports/Unittesting/OSVVM.py +++ b/pyEDAA/Reports/Unittesting/OSVVM.py @@ -29,13 +29,13 @@ # ==================================================================================================================== # # """Reader for OSVVM test report summary files in YAML format.""" -from pathlib import Path -from typing import Dict +from pathlib import Path +from typing import Dict +from ruamel.yaml import YAML from pyTooling.Decorators import export -from ruamel.yaml import YAML -from . import Testsuite as Abstract_Testsuite, Testcase as Abstract_Testcase, Status +from pyEDAA.Reports.Unittesting import Testsuite as Abstract_Testsuite, Testcase as Abstract_Testcase, Status @export diff --git a/pyEDAA/Reports/__init__.py b/pyEDAA/Reports/__init__.py index cb104e44..0253eca5 100644 --- a/pyEDAA/Reports/__init__.py +++ b/pyEDAA/Reports/__init__.py @@ -28,26 +28,25 @@ # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # -"""Various report abstract data models and report format converters.""" +""" +Various report abstract data models and report format converters. +""" __author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2021-2024, Electronic Design Automation Abstraction (EDA²)" __license__ = "Apache License, Version 2.0" -__version__ = "0.1.0" +__version__ = "0.2.0" __keywords__ = ["Reports", "Abstract Model", "Data Model", "Test Case", "Test Suite", "OSVVM", "YAML", "XML"] -from sys import version_info - +from enum import Enum +from sys import version_info from typing import List -from enum import Enum - from pyTooling.Decorators import export @export class ReportException(Exception): - # WORKAROUND: for Python <3.11 # Implementing a dummy method for Python versions before __notes__: List[str] diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..244e0b6a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,30 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# diff --git a/tests/unit/DocumentationCoverage/DataModel.py b/tests/unit/DocumentationCoverage/DataModel.py index fa9321da..9a379b21 100644 --- a/tests/unit/DocumentationCoverage/DataModel.py +++ b/tests/unit/DocumentationCoverage/DataModel.py @@ -1,4 +1,34 @@ -from pathlib import Path +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# +from pathlib import Path from unittest import TestCase from pyEDAA.Reports.DocumentationCoverage.Python import CoverageState, ClassCoverage, ModuleCoverage, PackageCoverage diff --git a/tests/unit/DocumentationCoverage/DocStrCoverage.py b/tests/unit/DocumentationCoverage/DocStrCoverage.py index 6346534a..fc4caf7a 100644 --- a/tests/unit/DocumentationCoverage/DocStrCoverage.py +++ b/tests/unit/DocumentationCoverage/DocStrCoverage.py @@ -1,3 +1,33 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# from pathlib import Path from unittest import TestCase diff --git a/tests/unit/DocumentationCoverage/__init__.py b/tests/unit/DocumentationCoverage/__init__.py index e69de29b..972a21f9 100644 --- a/tests/unit/DocumentationCoverage/__init__.py +++ b/tests/unit/DocumentationCoverage/__init__.py @@ -0,0 +1,30 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2021-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# diff --git a/tests/unit/Unittesting/DataModel.py b/tests/unit/Unittesting/DataModel.py index 4dfdce8d..81778c00 100644 --- a/tests/unit/Unittesting/DataModel.py +++ b/tests/unit/Unittesting/DataModel.py @@ -1,7 +1,36 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# from unittest import TestCase as ut_TestCase -from pyEDAA.Reports.Unittesting import Testcase, Status, Testsuite, DuplicateTestsuiteException, \ - DuplicateTestcaseException +from pyEDAA.Reports.Unittesting import Testcase, Status, Testsuite, DuplicateTestsuiteException, DuplicateTestcaseException class TestcaseInstantiation(ut_TestCase): diff --git a/tests/unit/Unittesting/JUnit.py b/tests/unit/Unittesting/JUnit.py index 904e895b..4380d561 100644 --- a/tests/unit/Unittesting/JUnit.py +++ b/tests/unit/Unittesting/JUnit.py @@ -1,6 +1,35 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# from datetime import timedelta - -from pathlib import Path +from pathlib import Path from unittest import TestCase as ut_TestCase from pyEDAA.Reports.Unittesting.JUnit import Document, Testcase, TestcaseState, Testsuite, TestsuiteSummary diff --git a/tests/unit/Unittesting/__init__.py b/tests/unit/Unittesting/__init__.py index e69de29b..244e0b6a 100644 --- a/tests/unit/Unittesting/__init__.py +++ b/tests/unit/Unittesting/__init__.py @@ -0,0 +1,30 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +# diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index e69de29b..244e0b6a 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -0,0 +1,30 @@ +# ==================================================================================================================== # +# _____ ____ _ _ ____ _ # +# _ __ _ _| ____| _ \ / \ / \ | _ \ ___ _ __ ___ _ __| |_ ___ # +# | '_ \| | | | _| | | | |/ _ \ / _ \ | |_) / _ \ '_ \ / _ \| '__| __/ __| # +# | |_) | |_| | |___| |_| / ___ \ / ___ \ _| _ < __/ |_) | (_) | | | |_\__ \ # +# | .__/ \__, |_____|____/_/ \_\/_/ \_(_)_| \_\___| .__/ \___/|_| \__|___/ # +# |_| |___/ |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2024-2024 Electronic Design Automation Abstraction (EDA²) # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +# # +# SPDX-License-Identifier: Apache-2.0 # +# ==================================================================================================================== # +#