diff --git a/.gitignore b/.gitignore index a8670b9f..af3e2100 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,8 @@ report/ # Sphinx doc/_build/ -doc/sphinx-reports/**/*.* -!doc/sphinx-reports/index.rst +doc/sphinx_reports/**/*.* +!doc/sphinx_reports/index.rst # BuildTheDocs doc/_theme/**/*.* diff --git a/doc/CodeCov/index.rst b/doc/CodeCov/index.rst new file mode 100644 index 00000000..899bfc4a --- /dev/null +++ b/doc/CodeCov/index.rst @@ -0,0 +1,5 @@ +.. _CODECOV: + +Code Coverage +############# + diff --git a/doc/Dependency.rst b/doc/Dependency.rst index 8d87ecb6..21ecc384 100644 --- a/doc/Dependency.rst +++ b/doc/Dependency.rst @@ -53,8 +53,8 @@ When installed as ``pyTooling``: +-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ | **Package** | **Version** | **License** | **Dependencies** | +=================================================================+=============+===========================================================================================+========================================================================================================================================================+ -| `pyTooling `__ | ≥5.0.0 | `Apache License, 2.0 `__ | *None* | -+-----------------------------------------------------------------+-------------+------------------------------------------------------------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------+ +| `pyTooling `__ | ≥5.0.0 | `Apache License, 2.0 `__ | *None* | ++-----------------------------------------------------------------+-------------+-------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------+ .. TODO:: document the dependency to diff --git a/doc/DocCov/index.rst b/doc/DocCov/index.rst new file mode 100644 index 00000000..49867857 --- /dev/null +++ b/doc/DocCov/index.rst @@ -0,0 +1,5 @@ +.. _DOCCOV: + +Documentation Coverage +###################### + diff --git a/doc/DocCoverage.rst b/doc/DocCoverage.rst index a83cfc1d..c1e75266 100644 --- a/doc/DocCoverage.rst +++ b/doc/DocCoverage.rst @@ -3,5 +3,5 @@ Documentation Coverage Documentation coverage generated by `docstr-coverage `__. -.. doc-coverage:: +.. report:doc-coverage:: :packageid: src diff --git a/doc/Unittest/index.rst b/doc/Unittest/index.rst new file mode 100644 index 00000000..a2202d13 --- /dev/null +++ b/doc/Unittest/index.rst @@ -0,0 +1,5 @@ +.. _UNITTEST: + +Unit Test Summary +################# + diff --git a/doc/conf.py b/doc/conf.py index 74657cc9..a496691c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -182,8 +182,8 @@ "sphinx_copybutton", "sphinx_autodoc_typehints", "autoapi.sphinx", + "sphinx_reports", # User defined extensions - "CoverageReports", ] @@ -261,7 +261,7 @@ # ============================================================================== # DocCov # ============================================================================== -doccov_packages = { +report_doccov_packages = { "src": { "name": "sphinx_reports", "directory": "../sphinx_reports", diff --git a/doc/index.rst b/doc/index.rst index 50c352a7..91eefd17 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -122,8 +122,9 @@ License :caption: Details :hidden: - sphinx-reports - + DocCov/index + CodeCov/index + Unittest/index .. raw:: latex @@ -133,7 +134,7 @@ License :caption: References and Reports :hidden: - sphinx-reports/sphinx-reports + sphinx_reports/sphinx_reports Unittest Report ➚ Code Coverage Report ➚ Doc. Coverage Report diff --git a/run.ps1 b/run.ps1 index 072a2117..cb1c6c70 100644 --- a/run.ps1 +++ b/run.ps1 @@ -97,7 +97,7 @@ if ($install) { Write-Host -ForegroundColor Cyan "[ADMIN][UNINSTALL] Uninstalling $PackageName ..." py -3.12 -m pip uninstall -y $PackageName Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Installing $PackageName from wheel ..." - py -3.12 -m pip install .\dist\$PackageName-6.0.0-py3-none-any.whl + py -3.12 -m pip install .\dist\$PackageName-0.1.0-py3-none-any.whl Write-Host -ForegroundColor Cyan "[ADMIN][INSTALL] Closing window in 5 seconds ..." Start-Sleep -Seconds 5 diff --git a/sphinx_reports/DocStrCoverage.py b/sphinx_reports/Adapter/DocStrCoverage.py similarity index 96% rename from sphinx_reports/DocStrCoverage.py rename to sphinx_reports/Adapter/DocStrCoverage.py index 9a5452c6..195a06a5 100644 --- a/sphinx_reports/DocStrCoverage.py +++ b/sphinx_reports/Adapter/DocStrCoverage.py @@ -39,11 +39,12 @@ from docstr_coverage.result_collection import FileCount from pyTooling.Decorators import export, readonly -from sphinx_reports.DataModel import ModuleCoverage, PackageCoverage +from sphinx_reports.Common import ReportExtensionError +from sphinx_reports.DataModel.DocumentationCoverage import ModuleCoverage, PackageCoverage @export -class DocStrCoverageError(Exception): +class DocStrCoverageError(ReportExtensionError): # WORKAROUND: for Python <3.11 # Implementing a dummy method for Python versions before __notes__: List[str] diff --git a/sphinx_reports/Adapter/__init__.py b/sphinx_reports/Adapter/__init__.py new file mode 100644 index 00000000..f74aaac7 --- /dev/null +++ b/sphinx_reports/Adapter/__init__.py @@ -0,0 +1,33 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 # +# ==================================================================================================================== # +# +""" +**Adapters between report generators and data models.** +""" diff --git a/sphinx_reports/CodeCoverage.py b/sphinx_reports/CodeCoverage.py new file mode 100644 index 00000000..9c69f8bf --- /dev/null +++ b/sphinx_reports/CodeCoverage.py @@ -0,0 +1,241 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 # +# ==================================================================================================================== # +# +""" +**Report code coverage as Sphinx documentation page(s).** +""" +from pathlib import Path +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator + +from docutils import nodes +from pyTooling.Decorators import export + +from sphinx_reports.Common import ReportExtensionError +from sphinx_reports.Sphinx import strip, LegendPosition, BaseDirective +from sphinx_reports.DataModel.DocumentationCoverage import PackageCoverage +from sphinx_reports.Adapter.DocStrCoverage import Analyzer + + +@export +class CodeCoverage(BaseDirective): + """ + This directive will be replaced by a table representing code coverage. + """ + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = { + "packageid": strip, + "legend": strip, + } + + directiveName: str = "code-coverage" + configPrefix: str = "codecov" + configValues: Dict[str, Tuple[Any, str, Any]] = { + "packages": ({}, "env", Dict) + } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) + + _packageID: str + _legend: LegendPosition + _packageName: str + _directory: Path + _failBelow: float + _levels: Dict[int, Dict[str, str]] + _coverage: PackageCoverage + + def _CheckOptions(self): + # Parse all directive options or use default values + self._packageID = self._ParseStringOption("packageid") + self._legend = self._ParseLegendOption("legend", LegendPosition.Bottom) + + def _CheckConfiguration(self): + # Check configuration fields and load necessary values + try: + allPackages = self.config[f"{self.configPrefix}_packages"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{self.configPrefix}_packages' is not configured.") from ex + + try: + packageConfiguration = allPackages[self._packageID] + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex + + try: + self._packageName = packageConfiguration["name"] + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex + + try: + self._directory = Path(packageConfiguration["directory"]) + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Configuration is missing.") from ex + + if not self._directory.exists(): + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Directory doesn't exist.") from FileNotFoundError(self._directory) + + try: + self._failBelow = int(packageConfiguration["fail_below"]) / 100 + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex + + if not (0.0 <= self._failBelow <= 100.0): + raise ReportExtensionError( + f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") + + self._levels = { + 30: {"class": "doccov-below30", "background": "rgba(101, 31, 255, .2)", "desc": "almost undocumented"}, + 50: {"class": "doccov-below50", "background": "rgba(255, 82, 82, .2)", "desc": "poorly documented"}, + 80: {"class": "doccov-below80", "background": "rgba(255, 145, 0, .2)", "desc": "roughly documented"}, + 90: {"class": "doccov-below90", "background": "rgba( 0, 200, 82, .2)", "desc": "well documented"}, + 100: {"class": "doccov-below100", "background": "rgba( 0, 200, 82, .2)", "desc": "excellent documented"}, + } + + def _ConvertToColor(self, currentLevel, configKey): + for levelLimit, levelConfig in self._levels.items(): + if (currentLevel * 100) < levelLimit: + return levelConfig[configKey] + else: + return self._levels[100][configKey] + + def _GenerateCoverageTable(self) -> nodes.table: + # Create a table and table header with 5 columns + table, tableGroup = self._PrepareTable( + id=self._packageID, + columns={ + "Filename": 500, + "Total": 100, + "Covered": 100, + "Missing": 100, + "Coverage in %": 100 + }, + classes=["doccov-table"] + ) + tableBody = nodes.tbody() + tableGroup += tableBody + + def sortedValues(d: Mapping) -> Generator[Any, None, None]: + for key in sorted(d.keys()): + yield d[key] + + def renderlevel(tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: int = 0): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"{' '*level}{packageCoverage.Name} ({packageCoverage.File})")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Coverage:.1%}")), + classes=["doccov-table-row", self._ConvertToColor(packageCoverage.Coverage, "class")], + # style="background: rgba( 0, 200, 82, .2);" + ) + + for package in sortedValues(packageCoverage._packages): + renderlevel(tableBody, package, level + 1) + + for module in sortedValues(packageCoverage._modules): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"{' '*level}{module.Name} ({module.File})")), + nodes.entry("", nodes.paragraph(text=f"{module.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{module.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{module.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{module.Coverage :.1%}")), + classes=["doccov-table-row", self._ConvertToColor(module.Coverage, "class")], + # style="background: rgba( 0, 200, 82, .2);" + ) + + renderlevel(tableBody, self._coverage) + + # Add a summary row + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"Overall ({self._coverage.FileCount} files):")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Coverage:.1%}"), + # classes=[self._ConvertToColor(self._coverage.coverage(), "class")] + ), + classes=["doccov-summary-row", self._ConvertToColor(self._coverage.AggregatedCoverage, "class")] + ) + + return table + + def _CreateLegend(self, id: str, classes: Iterable[str]) -> List[nodes.Element]: + rubric = nodes.rubric("", text="Legend") + + table = nodes.table("", id=id, classes=classes) + + tableGroup = nodes.tgroup(cols=2) + table += tableGroup + + tableRow = nodes.row() + tableGroup += nodes.colspec(colwidth=300) + tableRow += nodes.entry("", nodes.paragraph(text="%")) + tableGroup += nodes.colspec(colwidth=300) + tableRow += nodes.entry("", nodes.paragraph(text="Coverage Level")) + tableGroup += nodes.thead("", tableRow) + + tableBody = nodes.tbody() + tableGroup += tableBody + + for level, config in self._levels.items(): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"≤{level}%")), + nodes.entry("", nodes.paragraph(text=config["desc"])), + classes=["doccov-legend-row", self._ConvertToColor((level - 1) / 100, "class")] + ) + + return [rubric, table] + + def run(self): + self._CheckOptions() + self._CheckConfiguration() + + # Assemble a list of Python source files + analyzer = Analyzer(self._directory, self._packageName) + analyzer.Analyze() + self._coverage = analyzer.Convert() + # self._coverage.CalculateCoverage() + self._coverage.Aggregate() + + container = nodes.container() + + if LegendPosition.Top in self._legend: + container += self._CreateLegend(id="legend1", classes=["doccov-legend"]) + + container += self._GenerateCoverageTable() + + if LegendPosition.Bottom in self._legend: + container += self._CreateLegend(id="legend2", classes=["doccov-legend"]) + + return [container] diff --git a/sphinx_reports/Common.py b/sphinx_reports/Common.py new file mode 100644 index 00000000..b5395b3e --- /dev/null +++ b/sphinx_reports/Common.py @@ -0,0 +1,60 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 # +# ==================================================================================================================== # +# +""" +**Common exceptions, classes and helper functions..** +""" +from enum import Flag +from sys import version_info +from typing import List + +from pyTooling.Decorators import export +from sphinx.errors import ExtensionError + + +@export +class ReportExtensionError(ExtensionError): + # WORKAROUND: for Python <3.11 + # Implementing a dummy method for Python versions before + __notes__: List[str] + if version_info < (3, 11): # pragma: no cover + def add_note(self, message: str): + try: + self.__notes__.append(message) + except AttributeError: + self.__notes__ = [message] + + +@export +class LegendPosition(Flag): + NoLegend = 0 + Top = 1 + Bottom = 2 + Both = 3 diff --git a/sphinx_reports/DataModel.py b/sphinx_reports/DataModel/DocumentationCoverage.py similarity index 98% rename from sphinx_reports/DataModel.py rename to sphinx_reports/DataModel/DocumentationCoverage.py index 93acf520..0ab1ea8b 100644 --- a/sphinx_reports/DataModel.py +++ b/sphinx_reports/DataModel/DocumentationCoverage.py @@ -29,11 +29,12 @@ # ==================================================================================================================== # # """ -**A Sphinx extension providing coverage details embedded in documentation pages.** +**Abstract documentation coverage data model for Python code.** """ + from enum import Flag from pathlib import Path -from typing import Iterable, Dict, Optional as Nullable, Union +from typing import Optional as Nullable, Iterable, Dict, Union from pyTooling.Decorators import export, readonly diff --git a/sphinx_reports/DataModel/__init__.py b/sphinx_reports/DataModel/__init__.py new file mode 100644 index 00000000..7f039f75 --- /dev/null +++ b/sphinx_reports/DataModel/__init__.py @@ -0,0 +1,33 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 data models of reports.** +""" diff --git a/sphinx_reports/CoverageReports.py b/sphinx_reports/DocCoverage.py similarity index 60% rename from sphinx_reports/CoverageReports.py rename to sphinx_reports/DocCoverage.py index 200d69f4..a4876d4c 100644 --- a/sphinx_reports/CoverageReports.py +++ b/sphinx_reports/DocCoverage.py @@ -29,140 +29,18 @@ # ==================================================================================================================== # # """ -**A Sphinx extension providing coverage details embedded in documentation pages.** +**Report documentation coverage as Sphinx documentation page(s).** """ -from enum import Flag -from re import match from pathlib import Path -from sys import version_info -from typing import Dict, Tuple, Any, List, Optional as Nullable, Iterable, Mapping, Generator +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator from docutils import nodes from pyTooling.Decorators import export -from sphinx.application import Sphinx -from sphinx.directives import ObjectDescription -from sphinx_reports import __version__ -from sphinx_reports.DataModel import PackageCoverage -from sphinx_reports.DocStrCoverage import Analyzer - - -@export -class ExtensionError(Exception): - # WORKAROUND: for Python <3.11 - # Implementing a dummy method for Python versions before - __notes__: List[str] - if version_info < (3, 11): # pragma: no cover - def add_note(self, message: str): - try: - self.__notes__.append(message) - except AttributeError: - self.__notes__ = [message] - - -@export -def strip(option: str): - return option.strip().lower() - - -@export -class LegendPosition(Flag): - NoLegend = 0 - Top = 1 - Bottom = 2 - Both = 3 - - -@export -class BaseDirective(ObjectDescription): - has_content: bool = False - """ - A boolean; ``True`` if content is allowed. - - Client code must handle the case where content is required but not supplied (an empty content list will be supplied). - """ - - required_arguments = 0 - """Number of required directive arguments.""" - - optional_arguments = 0 - """Number of optional arguments after the required arguments.""" - - final_argument_whitespace = False - """A boolean, indicating if the final argument may contain whitespace.""" - - option_spec = None - """ - Mapping of option names to validator functions. - - A dictionary, mapping known option names to conversion functions such as :class:`int` or :class:`float` - (default: {}, no options). Several conversion functions are defined in the ``directives/__init__.py`` module. - - Option conversion functions take a single parameter, the option argument (a string or :class:`None`), validate it - and/or convert it to the appropriate form. Conversion functions may raise :exc:`ValueError` and - :exc:`TypeError` exceptions. - """ - - directiveName: str - - def _ParseBooleanOption(self, optionName: str, default: Nullable[bool] = None) -> bool: - try: - option = self.options[optionName] - except KeyError as ex: - if default is not None: - return default - else: - raise ExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex - - if option in ("yes", "true"): - return True - elif option in ("no", "false"): - return False - else: - raise ExtensionError(f"{self.directiveName}::{optionName}: '{option}' not supported for a boolean value (yes/true, no/false).") - - def _ParseStringOption(self, optionName: str, default: Nullable[str] = None, regexp: str = "\\w+") -> str: - try: - option = self.options[optionName] - except KeyError as ex: - if default is not None: - return default - else: - raise ExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex - - if match(regexp, option): - return option - else: - raise ExtensionError(f"{self.directiveName}::{optionName}: '{option}' not an accepted value for regexp '{regexp}'.") - - def _ParseLegendOption(self, optionName: str, default: Nullable[LegendPosition] = None) -> LegendPosition: - try: - option = self.options[optionName] - except KeyError as ex: - if default is not None: - return default - else: - raise ExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex - - try: - return LegendPosition[option] - except KeyError as ex: - raise ExtensionError(f"{self.directiveName}::{optionName}: Value '{option}' is not a valid member of 'LegendPosition'.") from ex - - def _PrepareTable(self, columns: Dict[str, int], id: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: - table = nodes.table("", id=id, classes=classes) - - tableGroup = nodes.tgroup(cols=(len(columns))) - table += tableGroup - - tableRow = nodes.row() - for columnTitle, width in columns.items(): - tableGroup += nodes.colspec(colwidth=width) - tableRow += nodes.entry("", nodes.paragraph(text=columnTitle)) - - tableGroup += nodes.thead("", tableRow) - - return table, tableGroup +from sphinx_reports.Common import ReportExtensionError +from sphinx_reports.Sphinx import strip, LegendPosition, BaseDirective +from sphinx_reports.DataModel.DocumentationCoverage import PackageCoverage +from sphinx_reports.Adapter.DocStrCoverage import Analyzer @export @@ -176,17 +54,17 @@ class DocCoverage(BaseDirective): option_spec = { "packageid": strip, - "legend": strip, + "legend": strip, } directiveName: str = "docstr-coverage" - configPrefix: str = "doccov" - configValues: Dict[str, Tuple[Any, str, Any]] = { - "packages": ({}, "env", Dict) + configPrefix: str = "doccov" + configValues: Dict[str, Tuple[Any, str, Any]] = { + f"{configPrefix}_packages": ({}, "env", Dict) } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) - _packageID: str - _legend: LegendPosition + _packageID: str + _legend: LegendPosition _packageName: str _directory: Path _failBelow: float @@ -199,38 +77,40 @@ def _CheckOptions(self): self._legend = self._ParseLegendOption("legend", LegendPosition.Bottom) def _CheckConfiguration(self): + from sphinx_reports import ReportDomain + # Check configuration fields and load necessary values try: - allPackages = self.config[f"{self.configPrefix}_packages"] + allPackages = self.config[f"{ReportDomain.name}_{self.configPrefix}_packages"] except (KeyError, AttributeError) as ex: - raise ExtensionError(f"Configuration option '{self.configPrefix}_packages' is not configured.") from ex + raise ReportExtensionError(f"Configuration option '{ReportDomain.name}_{self.configPrefix}_packages' is not configured.") from ex try: packageConfiguration = allPackages[self._packageID] except KeyError as ex: - raise ExtensionError(f"conf.py: {self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex + raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex try: self._packageName = packageConfiguration["name"] except KeyError as ex: - raise ExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex + raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex try: self._directory = Path(packageConfiguration["directory"]) except KeyError as ex: - raise ExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Configuration is missing.") from ex + raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.directory: Configuration is missing.") from ex if not self._directory.exists(): - raise ExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Directory doesn't exist.") from FileNotFoundError(self._directory) + raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.directory: Directory doesn't exist.") from FileNotFoundError(self._directory) try: self._failBelow = int(packageConfiguration["fail_below"]) / 100 except KeyError as ex: - raise ExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex + raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex if not (0.0 <= self._failBelow <= 100.0): - raise ExtensionError( - f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") + raise ReportExtensionError( + f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") self._levels = { 30: {"class": "doccov-below30", "background": "rgba(101, 31, 255, .2)", "desc": "almost undocumented"}, @@ -289,7 +169,7 @@ def renderlevel(tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: nodes.entry("", nodes.paragraph(text=f"{module.Expected}")), nodes.entry("", nodes.paragraph(text=f"{module.Covered}")), nodes.entry("", nodes.paragraph(text=f"{module.Uncovered}")), - nodes.entry("", nodes.paragraph(text=f"{module.Coverage:.1%}")), + nodes.entry("", nodes.paragraph(text=f"{module.Coverage :.1%}")), classes=["doccov-table-row", self._ConvertToColor(module.Coverage, "class")], # style="background: rgba( 0, 200, 82, .2);" ) @@ -364,24 +244,3 @@ def run(self): container += self._CreateLegend(id="legend2", classes=["doccov-legend"]) return [container] - - -@export -def setup(sphinxApplication: Sphinx): - """ - Extension setup function registering the VHDL domain in Sphinx. - - :param sphinxApplication: The Sphinx application. - :return: Dictionary containing the extension version and some properties. - """ - sphinxApplication.add_directive("doc-coverage", DocStrCoverage) - - for configName, (configDefault, configRebuilt, configTypes) in DocStrCoverage.configValues.items(): - sphinxApplication.add_config_value(f"{DocStrCoverage.configPrefix}_{configName}", configDefault, configRebuilt, configTypes) - - return { - "version": __version__, # version of the extension - "env_version": int(__version__.split(".")[0]), # version of the data structure stored in the environment - 'parallel_read_safe': False, # Not yet evaluated, thus false - 'parallel_write_safe': True, # Internal data structure is used read-only, thus no problems will occur by parallel writing. - } diff --git a/sphinx_reports/Sphinx.py b/sphinx_reports/Sphinx.py new file mode 100644 index 00000000..a4524a4c --- /dev/null +++ b/sphinx_reports/Sphinx.py @@ -0,0 +1,138 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 # +# ==================================================================================================================== # +# +""" +**Helper functions and derived classes from Sphinx.** +""" +from re import match as re_match +from typing import Optional as Nullable, Tuple, List, Dict + +from docutils import nodes +from pyTooling.Decorators import export +from sphinx.directives import ObjectDescription + +from sphinx_reports.Common import ReportExtensionError, LegendPosition + + +@export +def strip(option: str): + return option.strip().lower() + + +@export +class BaseDirective(ObjectDescription): + has_content: bool = False + """ + A boolean; ``True`` if content is allowed. + + Client code must handle the case where content is required but not supplied (an empty content list will be supplied). + """ + + required_arguments = 0 + """Number of required directive arguments.""" + + optional_arguments = 0 + """Number of optional arguments after the required arguments.""" + + final_argument_whitespace = False + """A boolean, indicating if the final argument may contain whitespace.""" + + option_spec = None + """ + Mapping of option names to validator functions. + + A dictionary, mapping known option names to conversion functions such as :class:`int` or :class:`float` + (default: {}, no options). Several conversion functions are defined in the ``directives/__init__.py`` module. + + Option conversion functions take a single parameter, the option argument (a string or :class:`None`), validate it + and/or convert it to the appropriate form. Conversion functions may raise :exc:`ValueError` and + :exc:`TypeError` exceptions. + """ + + directiveName: str + + def _ParseBooleanOption(self, optionName: str, default: Nullable[bool] = None) -> bool: + try: + option = self.options[optionName] + except KeyError as ex: + if default is not None: + return default + else: + raise ReportExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex + + if option in ("yes", "true"): + return True + elif option in ("no", "false"): + return False + else: + raise ReportExtensionError(f"{self.directiveName}::{optionName}: '{option}' not supported for a boolean value (yes/true, no/false).") + + def _ParseStringOption(self, optionName: str, default: Nullable[str] = None, regexp: str = "\\w+") -> str: + try: + option = self.options[optionName] + except KeyError as ex: + if default is not None: + return default + else: + raise ReportExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex + + if re_match(regexp, option): + return option + else: + raise ReportExtensionError(f"{self.directiveName}::{optionName}: '{option}' not an accepted value for regexp '{regexp}'.") + + def _ParseLegendOption(self, optionName: str, default: Nullable[LegendPosition] = None) -> LegendPosition: + try: + option = self.options[optionName] + except KeyError as ex: + if default is not None: + return default + else: + raise ReportExtensionError(f"{self.directiveName}: Required option '{optionName}' not found for directive.") from ex + + try: + return LegendPosition[option] + except KeyError as ex: + raise ReportExtensionError(f"{self.directiveName}::{optionName}: Value '{option}' is not a valid member of 'LegendPosition'.") from ex + + def _PrepareTable(self, columns: Dict[str, int], id: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: + table = nodes.table("", id=id, classes=classes) + + tableGroup = nodes.tgroup(cols=(len(columns))) + table += tableGroup + + tableRow = nodes.row() + for columnTitle, width in columns.items(): + tableGroup += nodes.colspec(colwidth=width) + tableRow += nodes.entry("", nodes.paragraph(text=columnTitle)) + + tableGroup += nodes.thead("", tableRow) + + return table, tableGroup diff --git a/sphinx_reports/Unittest.py b/sphinx_reports/Unittest.py new file mode 100644 index 00000000..09e15df5 --- /dev/null +++ b/sphinx_reports/Unittest.py @@ -0,0 +1,241 @@ +# ==================================================================================================================== # +# _ _ _ # +# ___ _ __ | |__ (_)_ __ __ __ _ __ ___ _ __ ___ _ __| |_ ___ # +# / __| '_ \| '_ \| | '_ \\ \/ /____| '__/ _ \ '_ \ / _ \| '__| __/ __| # +# \__ \ |_) | | | | | | | |> <_____| | | __/ |_) | (_) | | | |_\__ \ # +# |___/ .__/|_| |_|_|_| |_/_/\_\ |_| \___| .__/ \___/|_| \__|___/ # +# |_| |_| # +# ==================================================================================================================== # +# Authors: # +# Patrick Lehmann # +# # +# License: # +# ==================================================================================================================== # +# Copyright 2023-2024 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 # +# ==================================================================================================================== # +# +""" +**Report unit test results as Sphinx documentation page(s).** +""" +from pathlib import Path +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator + +from docutils import nodes +from pyTooling.Decorators import export + +from sphinx_reports.Common import ReportExtensionError +from sphinx_reports.Sphinx import strip, LegendPosition, BaseDirective +from sphinx_reports.DataModel.DocumentationCoverage import PackageCoverage +from sphinx_reports.Adapter.DocStrCoverage import Analyzer + + +@export +class UnittestSummary(BaseDirective): + """ + This directive will be replaced by a table representing unit test results. + """ + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = { + "packageid": strip, + "legend": strip, + } + + directiveName: str = "unittest-summary" + configPrefix: str = "unittest" + configValues: Dict[str, Tuple[Any, str, Any]] = { + "packages": ({}, "env", Dict) + } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) + + _packageID: str + _legend: LegendPosition + _packageName: str + _directory: Path + _failBelow: float + _levels: Dict[int, Dict[str, str]] + _coverage: PackageCoverage + + def _CheckOptions(self): + # Parse all directive options or use default values + self._packageID = self._ParseStringOption("packageid") + self._legend = self._ParseLegendOption("legend", LegendPosition.Bottom) + + def _CheckConfiguration(self): + # Check configuration fields and load necessary values + try: + allPackages = self.config[f"{self.configPrefix}_packages"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{self.configPrefix}_packages' is not configured.") from ex + + try: + packageConfiguration = allPackages[self._packageID] + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex + + try: + self._packageName = packageConfiguration["name"] + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex + + try: + self._directory = Path(packageConfiguration["directory"]) + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Configuration is missing.") from ex + + if not self._directory.exists(): + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.directory: Directory doesn't exist.") from FileNotFoundError(self._directory) + + try: + self._failBelow = int(packageConfiguration["fail_below"]) / 100 + except KeyError as ex: + raise ReportExtensionError(f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex + + if not (0.0 <= self._failBelow <= 100.0): + raise ReportExtensionError( + f"conf.py: {self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") + + self._levels = { + 30: {"class": "doccov-below30", "background": "rgba(101, 31, 255, .2)", "desc": "almost undocumented"}, + 50: {"class": "doccov-below50", "background": "rgba(255, 82, 82, .2)", "desc": "poorly documented"}, + 80: {"class": "doccov-below80", "background": "rgba(255, 145, 0, .2)", "desc": "roughly documented"}, + 90: {"class": "doccov-below90", "background": "rgba( 0, 200, 82, .2)", "desc": "well documented"}, + 100: {"class": "doccov-below100", "background": "rgba( 0, 200, 82, .2)", "desc": "excellent documented"}, + } + + def _ConvertToColor(self, currentLevel, configKey): + for levelLimit, levelConfig in self._levels.items(): + if (currentLevel * 100) < levelLimit: + return levelConfig[configKey] + else: + return self._levels[100][configKey] + + def _GenerateCoverageTable(self) -> nodes.table: + # Create a table and table header with 5 columns + table, tableGroup = self._PrepareTable( + id=self._packageID, + columns={ + "Filename": 500, + "Total": 100, + "Covered": 100, + "Missing": 100, + "Coverage in %": 100 + }, + classes=["doccov-table"] + ) + tableBody = nodes.tbody() + tableGroup += tableBody + + def sortedValues(d: Mapping) -> Generator[Any, None, None]: + for key in sorted(d.keys()): + yield d[key] + + def renderlevel(tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: int = 0): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"{' '*level}{packageCoverage.Name} ({packageCoverage.File})")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{packageCoverage.Coverage:.1%}")), + classes=["doccov-table-row", self._ConvertToColor(packageCoverage.Coverage, "class")], + # style="background: rgba( 0, 200, 82, .2);" + ) + + for package in sortedValues(packageCoverage._packages): + renderlevel(tableBody, package, level + 1) + + for module in sortedValues(packageCoverage._modules): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"{' '*level}{module.Name} ({module.File})")), + nodes.entry("", nodes.paragraph(text=f"{module.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{module.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{module.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{module.Coverage :.1%}")), + classes=["doccov-table-row", self._ConvertToColor(module.Coverage, "class")], + # style="background: rgba( 0, 200, 82, .2);" + ) + + renderlevel(tableBody, self._coverage) + + # Add a summary row + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"Overall ({self._coverage.FileCount} files):")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Expected}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Covered}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Uncovered}")), + nodes.entry("", nodes.paragraph(text=f"{self._coverage.Coverage:.1%}"), + # classes=[self._ConvertToColor(self._coverage.coverage(), "class")] + ), + classes=["doccov-summary-row", self._ConvertToColor(self._coverage.AggregatedCoverage, "class")] + ) + + return table + + def _CreateLegend(self, id: str, classes: Iterable[str]) -> List[nodes.Element]: + rubric = nodes.rubric("", text="Legend") + + table = nodes.table("", id=id, classes=classes) + + tableGroup = nodes.tgroup(cols=2) + table += tableGroup + + tableRow = nodes.row() + tableGroup += nodes.colspec(colwidth=300) + tableRow += nodes.entry("", nodes.paragraph(text="%")) + tableGroup += nodes.colspec(colwidth=300) + tableRow += nodes.entry("", nodes.paragraph(text="Coverage Level")) + tableGroup += nodes.thead("", tableRow) + + tableBody = nodes.tbody() + tableGroup += tableBody + + for level, config in self._levels.items(): + tableBody += nodes.row( + "", + nodes.entry("", nodes.paragraph(text=f"≤{level}%")), + nodes.entry("", nodes.paragraph(text=config["desc"])), + classes=["doccov-legend-row", self._ConvertToColor((level - 1) / 100, "class")] + ) + + return [rubric, table] + + def run(self): + self._CheckOptions() + self._CheckConfiguration() + + # Assemble a list of Python source files + analyzer = Analyzer(self._directory, self._packageName) + analyzer.Analyze() + self._coverage = analyzer.Convert() + # self._coverage.CalculateCoverage() + self._coverage.Aggregate() + + container = nodes.container() + + if LegendPosition.Top in self._legend: + container += self._CreateLegend(id="legend1", classes=["doccov-legend"]) + + container += self._GenerateCoverageTable() + + if LegendPosition.Bottom in self._legend: + container += self._CreateLegend(id="legend2", classes=["doccov-legend"]) + + return [container] diff --git a/sphinx_reports/__init__.py b/sphinx_reports/__init__.py index 345334ff..1530c755 100644 --- a/sphinx_reports/__init__.py +++ b/sphinx_reports/__init__.py @@ -28,9 +28,127 @@ # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # # +""" +**A Sphinx domain providing directives to add reports to the Sphinx-based documentation.** + +Supported reports: +* :ref:`Documentation coverage reports ` +""" __author__ = "Patrick Lehmann" __email__ = "Paebbels@gmail.com" __copyright__ = "2023-2024, Patrick Lehmann" __license__ = "Apache License, Version 2.0" __version__ = "0.1.0" __keywords__ = ["Python3", "Sphinx", "Extension", "Report", "doc-string", "interrogate"] + +from typing import Any, Tuple, Dict, Optional as Nullable + +from pyTooling.Decorators import export +from sphinx.application import Sphinx +from sphinx.domains import Domain + + +@export +class ReportDomain(Domain): + name = "report" #: The name of this domain + label = "rpt" #: The label of this domain + + dependencies = [ + ] #: A list of other extensions this domain depends on. + + from sphinx_reports.CodeCoverage import CodeCoverage + from sphinx_reports.DocCoverage import DocStrCoverage + from sphinx_reports.Unittest import UnittestSummary + + directives = { + "code-coverage": CodeCoverage, + "dependecy": DocStrCoverage, + "doc-coverage": DocStrCoverage, + "unittest": UnittestSummary, + } #: A dictionary of all directives in this domain. + + roles = { + # "design": DesignRole, + } #: A dictionary of all roles in this domain. + + indices = { + # LibraryIndex, + } #: A dictionary of all indices in this domain. + + configValues: Dict[str, Tuple[Any, str, Any]] = { + "designs": ({}, "env", Dict), + "defaults": ({}, "env", Dict), + **DocStrCoverage.configValues + } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) + + initial_data = { + "reports": {} + } #: A dictionary of all global data fields used by this domain. + + @property + def Reports(self) -> Dict[str, Any]: + return self.data["reports"] + + @staticmethod + def ReadReports(sphinxApplication: Sphinx) -> None: + """ + Call back for Sphinx ``builder-inited`` event. + + This callback will read the configuration variable ``vhdl_designs`` and parse the found VHDL source files. + + .. seealso:: + + Sphinx *builder-inited* event + See http://sphinx-doc.org/extdev/appapi.html#event-builder-inited + + :param sphinxApplication: The Sphinx application. + :return: + """ + print(f"Callback: builder-inited -> ReadReports") + print(f"[REPORT] Reading reports ...") + + + callbacks = { + "builder-inited": ReadReports, + # "source-read": ReadDesigns + } #: A dictionary of all callbacks used by this domain. + + # def resolve_xref( + # self, + # env: BuildEnvironment, + # fromdocname: str, + # builder: Builder, + # typ: str, + # target: str, + # node: pending_xref, + # contnode: nodes.Element + # ) -> Nullable[nodes.Element]: + # raise NotImplementedError() + + +@export +def setup(sphinxApplication: Sphinx): + """ + Extension setup function registering the VHDL domain in Sphinx. + + :param sphinxApplication: The Sphinx application. + :return: Dictionary containing the extension version and some properties. + """ + sphinxApplication.add_domain(ReportDomain) + + # Register callbacks + for eventName, callback in ReportDomain.callbacks.items(): + sphinxApplication.connect(eventName, callback) + + # Register configuration options supported/needed in Sphinx's 'conf.py' + for configName, (configDefault, configRebuilt, configTypes) in ReportDomain.configValues.items(): + sphinxApplication.add_config_value(f"{ReportDomain.name}_{configName}", configDefault, configRebuilt, configTypes) + + return { + "version": __version__, # version of the extension + "env_version": int(__version__.split(".")[0]), # version of the data structure stored in the environment + 'parallel_read_safe': False, # Not yet evaluated, thus false + 'parallel_write_safe': True, # Internal data structure is used read-only, thus no problems will occur by parallel writing. + } + + diff --git a/tests/unit/DataModel.py b/tests/unit/DataModel.py index 7eeebe83..be8ad609 100644 --- a/tests/unit/DataModel.py +++ b/tests/unit/DataModel.py @@ -32,8 +32,7 @@ from pathlib import Path from unittest import TestCase -from sphinx_reports.DataModel import PackageCoverage, ModuleCoverage, ClassCoverage - +from sphinx_reports.DataModel.DocumentationCoverage import ClassCoverage, ModuleCoverage, PackageCoverage if __name__ == "__main__": print("ERROR: you called a testcase declaration file as an executable module.")