From 5cf27e6b71897c5acba9c73ec4ae4ab3de00ff4a Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Sun, 28 Jan 2024 01:10:30 +0100 Subject: [PATCH] Refactored code and separated directives to create legends. --- doc/CodeCov/index.rst | 17 +- doc/DocCov/index.rst | 14 +- doc/conf.py | 10 +- sphinx_reports/Adapter/Coverage.py | 25 +- sphinx_reports/CodeCoverage.py | 362 +++++++++++++++++++++-------- sphinx_reports/Common.py | 14 +- sphinx_reports/DocCoverage.py | 314 +++++++++++++++++-------- sphinx_reports/Sphinx.py | 22 +- sphinx_reports/Unittest.py | 73 ++++-- sphinx_reports/__init__.py | 68 ++++-- sphinx_reports/static/__init__.py | 6 +- 11 files changed, 654 insertions(+), 271 deletions(-) diff --git a/doc/CodeCov/index.rst b/doc/CodeCov/index.rst index 7b4998c0..2b398ade 100644 --- a/doc/CodeCov/index.rst +++ b/doc/CodeCov/index.rst @@ -118,14 +118,23 @@ Directives An identifier referencing a dictionary entry in the configuration variable ``report_codecov_packages`` defined in :file:`conf.py`. - .. rst:directive:option:: legend - - Describes if and where to add a legend. Possible values: ``no_legend``, ``top``, ``bottom``, ``both``. - .. rst:directive:option:: no-branch-coverage If flag is present, no branch coverage columns are shown. Only statement coverage columns are present. +.. rst:directive:: code-coverage-legend + + .. rst:directive:option:: style + + Specifies the legend style. Default is ``horizontal-table``. + + Possible values: + + * ``default`` + * ``horizontal-table`` + * ``vertical-table`` + + .. _CODECOV/Roles: diff --git a/doc/DocCov/index.rst b/doc/DocCov/index.rst index 1ea75258..f3451b74 100644 --- a/doc/DocCov/index.rst +++ b/doc/DocCov/index.rst @@ -117,7 +117,7 @@ bar. Directives ********** -.. rst:directive:: doc-coverage +.. rst:directive:: report:doc-coverage Add a table summarizing the documentation coverage per Python source code file (packages and/or modules). @@ -130,6 +130,18 @@ Directives Describes if and where to add a legend. Possible values: ``no_legend``, ``top``, ``bottom``, ``both``. +.. rst:directive:: report:doc-coverage-legend + + .. rst:directive:option:: style + + Specifies the legend style. Default is ``horizontal-table``. + + Possible values: + + * ``default`` + * ``horizontal-table`` + * ``vertical-table`` + .. _DOCCOV/Roles: diff --git a/doc/conf.py b/doc/conf.py index 6ce0f43d..5098ad52 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -278,7 +278,7 @@ "name": "sphinx_reports", "json_report": "../report/coverage/coverage.json", "fail_below": 80, - "levels": _codeCovLevels + "levels": "default" } } @@ -295,25 +295,25 @@ "name": "sphinx_reports", "directory": "../sphinx_reports", "fail_below": 80, - "levels": _docCovLevels + "levels": "default" }, "undocumented": { "name": "MyPackage", "directory": "../tests/packages/undocumented", "fail_below": 80, - "levels": _docCovLevels + "levels": "default" }, "partially": { "name": "MyPackage", "directory": "../tests/packages/partially", "fail_below": 80, - "levels": _docCovLevels + "levels": "default" }, "documented": { "name": "MyPackage", "directory": "../tests/packages/documented", "fail_below": 80, - "levels": _docCovLevels + "levels": "default" } } diff --git a/sphinx_reports/Adapter/Coverage.py b/sphinx_reports/Adapter/Coverage.py index d4db4480..8c1a736c 100644 --- a/sphinx_reports/Adapter/Coverage.py +++ b/sphinx_reports/Adapter/Coverage.py @@ -33,7 +33,6 @@ """ from pathlib import Path -from coverage.results import Numbers from pyTooling.Configuration.JSON import Configuration from pyTooling.Decorators import export, readonly @@ -48,10 +47,24 @@ class CodeCoverageError(ReportExtensionError): @export class Analyzer: + """ + An analyzer to read and transform code coverage data from JSON format to a generic data model. + + Coverage.py can provide collected statement and branch coverage metrics as JSON data, which can be converted to a + generic code coverage model. + """ + _packageName: str _coverageReport: Configuration def __init__(self, packageName: str, jsonCoverageFile: Path) -> None: + """ + Read a JSON file containing code coverage metrics generated by Coverage.py. + + :param packageName: Name of the Python package that was analyzed. + :param jsonCoverageFile: JSON file containing statement and/or branch coverage. + :raises CodeCoverageError: If JSON file doesn't exist. + """ if not jsonCoverageFile.exists(): raise CodeCoverageError(f"JSON coverage report '{jsonCoverageFile}' not found.") from FileNotFoundError(jsonCoverageFile) @@ -63,10 +76,20 @@ def __init__(self, packageName: str, jsonCoverageFile: Path) -> None: @readonly def PackageName(self) -> str: + """ + Read-only property to access the analyzed package's name. + + :return: Name of the analyzed package. + """ return self._packageName @readonly def JSONCoverageFile(self) -> Path: + """ + Read-only property to access the parsed JSON file. + + :return: Path to the parsed JSON file. + """ return self._coverageReport.ConfigFile @readonly diff --git a/sphinx_reports/CodeCoverage.py b/sphinx_reports/CodeCoverage.py index f09608cd..9bc82a9d 100644 --- a/sphinx_reports/CodeCoverage.py +++ b/sphinx_reports/CodeCoverage.py @@ -32,14 +32,17 @@ **Report code coverage as Sphinx documentation page(s).** """ from pathlib import Path -from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union, Optional as Nullable +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union, Optional as Nullable, \ + ClassVar -from docutils import nodes -from docutils.parsers.rst.directives import flag -from pyTooling.Decorators import export -from sphinx.util.docutils import new_document +from docutils import nodes +from docutils.parsers.rst.directives import flag +from sphinx.application import Sphinx +from sphinx.config import Config +from sphinx.util.docutils import new_document +from pyTooling.Decorators import export -from sphinx_reports.Common import ReportExtensionError, LegendPosition +from sphinx_reports.Common import ReportExtensionError, LegendStyle from sphinx_reports.Sphinx import strip, BaseDirective from sphinx_reports.DataModel.CodeCoverage import PackageCoverage, AggregatedCoverage, ModuleCoverage from sphinx_reports.Adapter.Coverage import Analyzer @@ -47,119 +50,175 @@ class package_DictType(TypedDict): name: str - json_report: str + json_report: Path fail_below: int - levels: Dict[Union[int, str], Dict[str, str]] + levels: Union[str, Dict[Union[int, str], Dict[str, str]]] @export -class CodeCoverage(BaseDirective): - """ - This directive will be replaced by a table representing code coverage. - """ - has_content = False - required_arguments = 0 - optional_arguments = 2 - +class CodeCoverageBase(BaseDirective): option_spec = { - "packageid": strip, - "legend": strip, - "no-branch-coverage": flag + "packageid": strip + } + + defaultCoverageDefinitions = { + "default": { + 30: {"class": "report-cov-below30", "desc": "almost unused"}, + 50: {"class": "report-cov-below50", "desc": "poorly used"}, + 80: {"class": "report-cov-below80", "desc": "somehow used"}, + 90: {"class": "report-cov-below90", "desc": "well used"}, + 100: {"class": "report-cov-below100", "desc": "excellently used"}, + "error": {"class": "report-cov-error", "desc": "internal error"}, + } } - directiveName: str = "code-coverage" configPrefix: str = "codecov" 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)) + f"{configPrefix}_packages": ({}, "env", Dict), + f"{configPrefix}_levels": (defaultCoverageDefinitions, "env", Dict), + } #: A dictionary of all configuration values used by code coverage directives. + + _coverageLevelDefinitions: ClassVar[Dict[str, Dict[Union[int, str], Dict[str, str]]]] = {} + _packageConfigurations: ClassVar[Dict[str, package_DictType]] = {} _packageID: str - _legend: LegendPosition - _noBranchCoverage: bool - _packageName: str - _jsonReport: Path - _failBelow: float _levels: Dict[Union[int, str], Dict[str, str]] - _coverage: PackageCoverage def _CheckOptions(self) -> None: # Parse all directive options or use default values self._packageID = self._ParseStringOption("packageid") - self._legend = self._ParseLegendOption("legend", LegendPosition.bottom) - self._noBranchCoverage = "no-branch-coverage" in self.options - - def _CheckConfiguration(self) -> None: - from sphinx_reports import ReportDomain - - # Check configuration fields and load necessary values - try: - allPackages: Dict[str, package_DictType] = self.config[f"{ReportDomain.name}_{self.configPrefix}_packages"] - except (KeyError, AttributeError) as 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 ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex + @classmethod + def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None: + """ + Check configuration fields and load necessary values. - try: - self._packageName = packageConfiguration["name"] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex + :param sphinxApplication: Sphinx application instance. + :param sphinxConfiguration: Sphinx configuration instance. + """ + cls._CheckLevelsConfiguration(sphinxConfiguration) + cls._CheckPackagesConfiguration(sphinxConfiguration) - try: - self._jsonReport = Path(packageConfiguration["json_report"]) - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.json_report: Configuration is missing.") from ex + @classmethod + def _CheckLevelsConfiguration(cls, sphinxConfiguration: Config) -> None: + from sphinx_reports import ReportDomain - if not self._jsonReport.exists(): - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.json_report: Coverage report file '{self._jsonReport}' doesn't exist.") from FileNotFoundError(self._jsonReport) + variableName = f"{ReportDomain.name}_{cls.configPrefix}_levels" try: - self._failBelow = int(packageConfiguration["fail_below"]) / 100 - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex - except ValueError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex + coverageLevelDefinitions: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_levels"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex + + if "default" not in coverageLevelDefinitions: + cls._coverageLevelDefinitions["default"] = cls.defaultCoverageDefinitions["default"] + + for key, coverageLevelDefinition in coverageLevelDefinitions.items(): + configurationName = f"conf.py: {variableName}:[{key}]" + + if 100 not in coverageLevelDefinition: + raise ReportExtensionError(f"{configurationName}[100]: Configuration is missing.") + elif "error" not in coverageLevelDefinition: + raise ReportExtensionError(f"{configurationName}[error]: Configuration is missing.") + + cls._coverageLevelDefinitions[key] = {} + + for level, levelConfig in coverageLevelDefinition.items(): + try: + if isinstance(level, str): + if level != "error": + raise ReportExtensionError(f"{configurationName}[{level}]: Level is a keyword, but not 'error'.") + elif not (0.0 <= int(level) <= 100.0): + raise ReportExtensionError(f"{configurationName}[{level}]: Level is out of range 0..100.") + except ValueError as ex: + raise ReportExtensionError(f"{configurationName}[{level}]: Level is not a keyword or an integer in range 0..100.") from ex + + try: + cssClass = levelConfig["class"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}[{level}].class: CSS class is missing.") from ex + + try: + description = levelConfig["desc"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}[{level}].desc: Description is missing.") from ex + + cls._coverageLevelDefinitions[key][level] = { + "class": cssClass, + "desc": description + } + + @classmethod + def _CheckPackagesConfiguration(cls, sphinxConfiguration: Config) -> None: + from sphinx_reports import ReportDomain - if not (0.0 <= self._failBelow <= 100.0): - raise ReportExtensionError( - f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") + variableName = f"{ReportDomain.name}_{cls.configPrefix}_packages" try: - levels = packageConfiguration["levels"] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Configuration is missing.") from ex + allPackages: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_packages"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex - if 100 not in packageConfiguration["levels"]: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[100]: Configuration is missing.") + # try: + # packageConfiguration = allPackages[self._packageID] + # except KeyError as ex: + # raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{cls.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex - if "error" not in packageConfiguration["levels"]: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[error]: Configuration is missing.") + for packageID, packageConfiguration in allPackages.items(): + configurationName = f"conf.py: {variableName}:[{packageID}]" - self._levels = {} - for level, levelConfig in levels.items(): try: - if isinstance(level, str): - if level != "error": - raise ReportExtensionError( - f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is a keyword, but not 'error'.") - elif not (0.0 <= int(level) <= 100.0): - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is out of range 0..100.") - except ValueError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is not a keyword or an integer in range 0..100.") from ex + packageName = packageConfiguration["name"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.name: Configuration is missing.") from ex try: - cssClass = levelConfig["class"] + jsonReport = Path(packageConfiguration["json_report"]) except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[level].class: CSS class is missing.") from ex + raise ReportExtensionError(f"{configurationName}.json_report: Configuration is missing.") from ex + + if not jsonReport.exists(): + raise ReportExtensionError(f"{configurationName}.json_report: Coverage report file '{jsonReport}' doesn't exist.") from FileNotFoundError(jsonReport) try: - description = levelConfig["desc"] + failBelow = int(packageConfiguration["fail_below"]) / 100 except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[level].desc: Description is missing.") from ex + raise ReportExtensionError(f"{configurationName}.fail_below: Configuration is missing.") from ex + except ValueError as ex: + raise ReportExtensionError(f"{configurationName}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex + + if not (0.0 <= failBelow <= 100.0): + raise ReportExtensionError(f"{configurationName}.fail_below: Is out of range 0..100.") - self._levels[level] = {"class": cssClass, "desc": description} + try: + levels = packageConfiguration["levels"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.levels: Configuration is missing.") from ex + + if isinstance(levels, str): + try: + levelDefinition = cls._coverageLevelDefinitions[levels] + except KeyError as ex: + raise ReportExtensionError( + f"{configurationName}.levels: Referenced coverage levels '{levels}' are not defined in conf.py variable '{variableName}'.") from ex + elif isinstance(levels, dict): + if 100 not in packageConfiguration["levels"]: + raise ReportExtensionError(f"{configurationName}.levels[100]: Configuration is missing.") + elif "error" not in packageConfiguration["levels"]: + raise ReportExtensionError(f"{configurationName}.levels[error]: Configuration is missing.") + + levelDefinition = {} + for x, y in packageConfiguration["levels"].items(): + pass + else: + raise ReportExtensionError(f"") + + cls._packageConfigurations[packageID] = { + "name": packageName, + "json_report": jsonReport, + "fail_below": failBelow, + "levels": levelDefinition + } def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: if currentLevel < 0.0: @@ -171,19 +230,55 @@ def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: return self._levels[100][configKey] + +@export +class CodeCoverage(CodeCoverageBase): + """ + This directive will be replaced by a table representing code coverage. + """ + directiveName: str = "code-coverage" + + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = CodeCoverageBase.option_spec | { + "no-branch-coverage": flag + } + + _noBranchCoverage: bool + _packageName: str + _jsonReport: Path + _failBelow: float + _coverage: PackageCoverage + + def _CheckOptions(self) -> None: + """ + Parse all directive options or use default values. + """ + super()._CheckOptions() + + self._noBranchCoverage = "no-branch-coverage" in self.options + + packageConfiguration = self._packageConfigurations[self._packageID] + self._packageName = packageConfiguration["name"] + self._jsonReport = packageConfiguration["json_report"] + self._failBelow = packageConfiguration["fail_below"] + self._levels = packageConfiguration["levels"] + def _GenerateCoverageTable(self) -> nodes.table: # Create a table and table header with 10 columns columns = [ - ("Package", [(" Module", 500)], None), + ("Package", [(" Module", 500)], None), ("Statments", [("Total", 100), ("Excluded", 100), ("Covered", 100), ("Missing", 100)], None), - ("Branches", [("Total", 100), ("Covered", 100), ("Partial", 100), ("Missing", 100)], None), - ("Coverage", [("in %", 100)], None) + ("Branches" , [("Total", 100), ("Covered", 100), ("Partial", 100), ("Missing", 100)], None), + ("Coverage", [("in %", 100)], None) ] if self._noBranchCoverage: columns.pop(2) - table, tableGroup = self._PrepareTable( + table, tableGroup = self._CreateTableHeader( identifier=self._packageID, columns=columns, classes=["report-codecov-table"] @@ -262,6 +357,76 @@ def renderlevel(tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: return table + def _CreatePages(self) -> None: + def handlePackage(package: PackageCoverage) -> None: + for pack in package._packages.values(): + if handlePackage(pack): + return True + + for module in package._modules.values(): + if handleModule(module): + return True + + def handleModule(module: ModuleCoverage) -> None: + doc = new_document("dummy") + + rootSection = nodes.section(ids=["foo"]) + doc += rootSection + + title = nodes.title(text=f"{module.Name}") + rootSection += title + rootSection += nodes.paragraph(text="some text") + + docname = f"coverage/{module.Name}" + self.env.titles[docname] = title + self.env.longtitles[docname] = title + + return True + + handlePackage(self._coverage) + + def run(self) -> List[nodes.Node]: + self._CheckOptions() + + # Assemble a list of Python source files + analyzer = Analyzer(self._packageName, self._jsonReport) + self._coverage = analyzer.Convert() + # self._coverage.Aggregate() + + self._CreatePages() + + container = nodes.container() + container += self._GenerateCoverageTable() + + return [container] + + +@export +class CodeCoverageLegend(CodeCoverageBase): + """ + This directive will be replaced by a legend table representing coverage levels. + """ + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = CodeCoverageBase.option_spec | { + "style": strip + } + + directiveName: str = "code-coverage-legend" + + _style: LegendStyle + + def _CheckOptions(self) -> None: + # Parse all directive options or use default values + super()._CheckOptions() + + self._style = self._ParseLegendStyle("style", LegendStyle.Horizontal) + + packageConfiguration = self._packageConfigurations[self._packageID] + self._levels = packageConfiguration["levels"] + def _CreateLegend(self, identifier: str, classes: Iterable[str]) -> List[nodes.Element]: rubric = nodes.rubric("", text="Legend") @@ -293,21 +458,16 @@ def _CreateLegend(self, identifier: str, classes: Iterable[str]) -> List[nodes.E def run(self) -> List[nodes.Node]: self._CheckOptions() - self._CheckConfiguration() - - # Assemble a list of Python source files - analyzer = Analyzer(self._packageName, self._jsonReport) - self._coverage = analyzer.Convert() - # self._coverage.Aggregate() container = nodes.container() - - if LegendPosition.top in self._legend: - container += self._CreateLegend(identifier=f"{self._packageID}-top-legend", classes=["report-codecov-legend"]) - - container += self._GenerateCoverageTable() - - if LegendPosition.bottom in self._legend: - container += self._CreateLegend(identifier=f"{self._packageID}-bottom-legend", classes=["report-codecov-legend"]) + if LegendStyle.Table in self._style: + if LegendStyle.Horizontal in self._style: + container += self._CreateLegend(identifier=f"{self._packageID}-legend", classes=["report-codecov-legend"]) + elif LegendStyle.Vertical in self._style: + container += self._CreateLegend(identifier=f"{self._packageID}-legend", classes=["report-codecov-legend"]) + else: + container += nodes.paragraph(text=f"Unsupported legend style.") + else: + container += nodes.paragraph(text=f"Unsupported legend style.") return [container] diff --git a/sphinx_reports/Common.py b/sphinx_reports/Common.py index 2491d3b0..8cad0604 100644 --- a/sphinx_reports/Common.py +++ b/sphinx_reports/Common.py @@ -55,11 +55,15 @@ def add_note(self, message: str) -> None: @export -class LegendPosition(Flag): - no_legend = 0 - top = 1 - bottom = 2 - both = 3 +class LegendStyle(Flag): + Default = 0 + Table = 1 + + Horizontal = 1024 + Vertical = 2048 + + horizontal_table = Table | Horizontal + vertical_table = Table | Vertical @export diff --git a/sphinx_reports/DocCoverage.py b/sphinx_reports/DocCoverage.py index 5a638375..408cc09f 100644 --- a/sphinx_reports/DocCoverage.py +++ b/sphinx_reports/DocCoverage.py @@ -31,130 +31,185 @@ """ **Report documentation coverage as Sphinx documentation page(s).** """ -from pathlib import Path -from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union +from pathlib import Path +from typing import Dict, Tuple, Any, List, Iterable, Mapping, Generator, TypedDict, Union, ClassVar from docutils import nodes +from sphinx.application import Sphinx +from sphinx.config import Config from pyTooling.Decorators import export -from sphinx_reports.Common import ReportExtensionError, LegendPosition +from sphinx_reports.Common import ReportExtensionError, LegendStyle from sphinx_reports.Sphinx import strip, BaseDirective from sphinx_reports.DataModel.DocumentationCoverage import PackageCoverage, AggregatedCoverage from sphinx_reports.Adapter.DocStrCoverage import Analyzer class package_DictType(TypedDict): - name: str - directory: str + name: str + directory: Path fail_below: int - levels: Dict[Union[int, str], Dict[str, str]] + levels: Union[str, Dict[Union[int, str], Dict[str, str]]] @export -class DocCoverage(BaseDirective): - """ - This directive will be replaced by a table representing documentation coverage. - """ - has_content = False - required_arguments = 0 - optional_arguments = 2 - +class DocCoverageBase(BaseDirective): option_spec = { "packageid": strip, - "legend": strip, } - directiveName: str = "docstr-coverage" + defaultCoverageDefinitions = { + "default": { + 30: {"class": "report-cov-below30", "desc": "almost undocumented"}, + 50: {"class": "report-cov-below50", "desc": "poorly documented"}, + 80: {"class": "report-cov-below80", "desc": "roughly documented"}, + 90: {"class": "report-cov-below90", "desc": "well documented"}, + 100: {"class": "report-cov-below100", "desc": "excellent documented"}, + "error": {"class": "report-cov-error", "desc": "internal error"}, + } + } + 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)) + f"{configPrefix}_packages": ({}, "env", Dict), + f"{configPrefix}_levels": (defaultCoverageDefinitions, "env", Dict), + } #: A dictionary of all configuration values used by documentation coverage directives. + + _coverageLevelDefinitions: ClassVar[Dict[str, Dict[Union[int, str], Dict[str, str]]]] = {} + _packageConfigurations: ClassVar[Dict[str, package_DictType]] = {} _packageID: str - _legend: LegendPosition - _packageName: str - _directory: Path - _failBelow: float _levels: Dict[Union[int, str], Dict[str, str]] - _coverage: PackageCoverage def _CheckOptions(self) -> None: # Parse all directive options or use default values self._packageID = self._ParseStringOption("packageid") - self._legend = self._ParseLegendOption("legend", LegendPosition.bottom) - def _CheckConfiguration(self) -> None: - from sphinx_reports import ReportDomain + @classmethod + def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None: + """ + Check configuration fields and load necessary values. - # Check configuration fields and load necessary values - try: - allPackages: Dict[str, package_DictType] = self.config[f"{ReportDomain.name}_{self.configPrefix}_packages"] - except (KeyError, AttributeError) as ex: - raise ReportExtensionError(f"Configuration option '{ReportDomain.name}_{self.configPrefix}_packages' is not configured.") from ex + :param sphinxApplication: Sphinx application instance. + :param sphinxConfiguration: Sphinx configuration instance. + """ + cls._CheckLevelsConfiguration(sphinxConfiguration) + cls._CheckPackagesConfiguration(sphinxConfiguration) - try: - packageConfiguration = allPackages[self._packageID] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages: No configuration found for '{self._packageID}'.") from ex + @classmethod + def _CheckLevelsConfiguration(cls, sphinxConfiguration: Config) -> None: + from sphinx_reports import ReportDomain - try: - self._packageName = packageConfiguration["name"] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.name: Configuration is missing.") from ex + variableName = f"{ReportDomain.name}_{cls.configPrefix}_levels" try: - self._directory = Path(packageConfiguration["directory"]) - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.directory: Configuration is missing.") from ex + coverageLevelDefinitions: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_levels"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex + + if "default" not in coverageLevelDefinitions: + cls._coverageLevelDefinitions["default"] = cls.defaultCoverageDefinitions["default"] + + for key, coverageLevelDefinition in coverageLevelDefinitions.items(): + configurationName = f"conf.py: {variableName}:[{key}]" + + if 100 not in coverageLevelDefinition: + raise ReportExtensionError(f"{configurationName}[100]: Configuration is missing.") + elif "error" not in coverageLevelDefinition: + raise ReportExtensionError(f"{configurationName}[error]: Configuration is missing.") + + cls._coverageLevelDefinitions[key] = {} + + for level, levelConfig in coverageLevelDefinition.items(): + try: + if isinstance(level, str): + if level != "error": + raise ReportExtensionError(f"{configurationName}[{level}]: Level is a keyword, but not 'error'.") + elif not (0.0 <= int(level) <= 100.0): + raise ReportExtensionError(f"{configurationName}[{level}]: Level is out of range 0..100.") + except ValueError as ex: + raise ReportExtensionError(f"{configurationName}[{level}]: Level is not a keyword or an integer in range 0..100.") from ex + + try: + cssClass = levelConfig["class"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}[{level}].class: CSS class is missing.") from ex + + try: + description = levelConfig["desc"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}[{level}].desc: Description is missing.") from ex + + cls._coverageLevelDefinitions[key][level] = { + "class": cssClass, + "desc": description + } + + @classmethod + def _CheckPackagesConfiguration(cls, sphinxConfiguration: Config) -> None: + from sphinx_reports import ReportDomain - if not self._directory.exists(): - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.directory: Directory '{self._directory}' doesn't exist.") from FileNotFoundError(self._directory) + variableName = f"{ReportDomain.name}_{cls.configPrefix}_packages" try: - self._failBelow = int(packageConfiguration["fail_below"]) / 100 - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Configuration is missing.") from ex - except ValueError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex + allPackages: Dict[str, package_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_packages"] + except (KeyError, AttributeError) as ex: + raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex - if not (0.0 <= self._failBelow <= 100.0): - raise ReportExtensionError( - f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.fail_below: Is out of range 0..100.") + for packageID, packageConfiguration in allPackages.items(): + configurationName = f"conf.py: {variableName}:[{packageID}]" - try: - levels = packageConfiguration["levels"] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Configuration is missing.") from ex + try: + packageName = packageConfiguration["name"] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.name: Configuration is missing.") from ex - if 100 not in packageConfiguration["levels"]: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[100]: Configuration is missing.") + try: + directory = Path(packageConfiguration["directory"]) + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.directory: Configuration is missing.") from ex - if "error" not in packageConfiguration["levels"]: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[error]: Configuration is missing.") + if not directory.exists(): + raise ReportExtensionError(f"{configurationName}.directory: Directory '{directory}' doesn't exist.") from FileNotFoundError(directory) - self._levels = {} - for level, levelConfig in levels.items(): try: - if isinstance(level, str): - if level != "error": - raise ReportExtensionError( - f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is a keyword, but not 'error'.") - elif not (0.0 <= int(level) <= 100.0): - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is out of range 0..100.") + failBelow = int(packageConfiguration["fail_below"]) / 100 + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.fail_below: Configuration is missing.") from ex except ValueError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels: Level is not a keyword or an integer in range 0..100.") from ex + raise ReportExtensionError(f"{configurationName}.fail_below: '{packageConfiguration['fail_below']}' is not an integer in range 0..100.") from ex - try: - cssClass = levelConfig["class"] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[level].class: CSS class is missing.") from ex + if not (0.0 <= failBelow <= 100.0): + raise ReportExtensionError(f"{configurationName}.fail_below: Is out of range 0..100.") try: - description = levelConfig["desc"] + levels = packageConfiguration["levels"] except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_packages:{self._packageID}.levels[level].desc: Description is missing.") from ex - - self._levels[level] = {"class": cssClass, "desc": description} + raise ReportExtensionError(f"{configurationName}.levels: Configuration is missing.") from ex + + if isinstance(levels, str): + try: + levelDefinition = cls._coverageLevelDefinitions[levels] + except KeyError as ex: + raise ReportExtensionError(f"{configurationName}.levels: Referenced coverage levels '{levels}' are not defined in conf.py variable '{variableName}'.") from ex + elif isinstance(levels, dict): + if 100 not in packageConfiguration["levels"]: + raise ReportExtensionError(f"{configurationName}.levels[100]: Configuration is missing.") + elif "error" not in packageConfiguration["levels"]: + raise ReportExtensionError(f"{configurationName}.levels[error]: Configuration is missing.") + + levelDefinition = {} + for x, y in packageConfiguration["levels"].items(): + pass + else: + raise ReportExtensionError(f"") + + cls._packageConfigurations[packageID] = { + "name": packageName, + "directory": directory, + "fail_below": failBelow, + "levels": levelDefinition + } def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: if currentLevel < 0.0: @@ -166,9 +221,40 @@ def _ConvertToColor(self, currentLevel: float, configKey: str) -> str: return self._levels[100][configKey] + +@export +class DocCoverage(DocCoverageBase): + """ + This directive will be replaced by a table representing documentation coverage. + """ + directiveName: str = "docstr-coverage" + + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = DocCoverageBase.option_spec + + _packageName: str + _directory: Path + _failBelow: float + _coverage: PackageCoverage + + def _CheckOptions(self) -> None: + """ + Parse all directive options or use default values. + """ + super()._CheckOptions() + + packageConfiguration = self._packageConfigurations[self._packageID] + self._packageName = packageConfiguration["name"] + self._directory = packageConfiguration["directory"] + self._failBelow = packageConfiguration["fail_below"] + self._levels = packageConfiguration["levels"] + def _GenerateCoverageTable(self) -> nodes.table: # Create a table and table header with 5 columns - table, tableGroup = self._PrepareTable( + table, tableGroup = self._CreateTableHeader( identifier=self._packageID, columns=[ ("Filename", None, 500), @@ -240,6 +326,50 @@ def renderlevel(tableBody: nodes.tbody, packageCoverage: PackageCoverage, level: return table + +@export +class DocStrCoverage(DocCoverage): + def run(self) -> List[nodes.Node]: + self._CheckOptions() + + # Assemble a list of Python source files + analyzer = Analyzer(self._packageName, self._directory) + analyzer.Analyze() + self._coverage = analyzer.Convert() + # self._coverage.CalculateCoverage() + self._coverage.Aggregate() + + container = nodes.container() + container += self._GenerateCoverageTable() + + return [container] + +@export +class DocCoverageLegend(DocCoverageBase): + """ + This directive will be replaced by a legend table representing coverage levels. + """ + has_content = False + required_arguments = 0 + optional_arguments = 2 + + option_spec = DocCoverageBase.option_spec | { + "style": strip + } + + directiveName: str = "doc-coverage-legend" + + _style: LegendStyle + + def _CheckOptions(self) -> None: + # Parse all directive options or use default values + super()._CheckOptions() + + self._style = self._ParseLegendStyle("style", LegendStyle.Horizontal) + + packageConfiguration = self._packageConfigurations[self._packageID] + self._levels = packageConfiguration["levels"] + def _CreateLegend(self, identifier: str, classes: Iterable[str]) -> List[nodes.Element]: rubric = nodes.rubric("", text="Legend") @@ -269,28 +399,18 @@ def _CreateLegend(self, identifier: str, classes: Iterable[str]) -> List[nodes.E return [rubric, table] - -@export -class DocStrCoverage(DocCoverage): def run(self) -> List[nodes.Node]: self._CheckOptions() - self._CheckConfiguration() - - # Assemble a list of Python source files - analyzer = Analyzer(self._packageName, self._directory) - analyzer.Analyze() - self._coverage = analyzer.Convert() - # self._coverage.CalculateCoverage() - self._coverage.Aggregate() container = nodes.container() - - if LegendPosition.top in self._legend: - container += self._CreateLegend(identifier=f"{self._packageID}-top-legend", classes=["report-doccov-legend"]) - - container += self._GenerateCoverageTable() - - if LegendPosition.bottom in self._legend: - container += self._CreateLegend(identifier=f"{self._packageID}-bottom-legend", classes=["report-doccov-legend"]) + if LegendStyle.Table in self._style: + if LegendStyle.Horizontal in self._style: + container += self._CreateLegend(identifier=f"{self._packageID}-legend", classes=["report-doccov-legend"]) + elif LegendStyle.Vertical in self._style: + container += self._CreateLegend(identifier=f"{self._packageID}-legend", classes=["report-doccov-legend"]) + else: + container += nodes.paragraph(text=f"Unsupported legend style.") + else: + container += nodes.paragraph(text=f"Unsupported legend style.") return [container] diff --git a/sphinx_reports/Sphinx.py b/sphinx_reports/Sphinx.py index 84fc8339..eb81d9a1 100644 --- a/sphinx_reports/Sphinx.py +++ b/sphinx_reports/Sphinx.py @@ -32,13 +32,13 @@ **Helper functions and derived classes from Sphinx.** """ from re import match as re_match -from typing import Optional as Nullable, Tuple, List, Dict +from typing import Optional as Nullable, Tuple, List -from docutils import nodes -from pyTooling.Decorators import export -from sphinx.directives import ObjectDescription +from docutils import nodes +from sphinx.directives import ObjectDescription +from pyTooling.Decorators import export -from sphinx_reports.Common import ReportExtensionError, LegendPosition +from sphinx_reports.Common import ReportExtensionError, LegendStyle @export @@ -108,21 +108,23 @@ def _ParseStringOption(self, optionName: str, default: Nullable[str] = None, reg 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: + def _ParseLegendStyle(self, optionName: str, default: Nullable[LegendStyle] = None) -> LegendStyle: try: - option = self.options[optionName].lower() + option: str = 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 + identifier = option.lower().replace("-", "_") + try: - return LegendPosition[option] + return LegendStyle[identifier] except KeyError as ex: - raise ReportExtensionError(f"{self.directiveName}::{optionName}: Value '{option}' is not a valid member of 'LegendPosition'.") from ex + raise ReportExtensionError(f"{self.directiveName}::{optionName}: Value '{option}' (transformed: '{identifier}') is not a valid member of 'LegendStyle'.") from ex - def _PrepareTable(self, columns: List[Tuple[str, Nullable[List[Tuple[str, int]]], Nullable[int]]], identifier: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: + def _CreateTableHeader(self, columns: List[Tuple[str, Nullable[List[Tuple[str, int]]], Nullable[int]]], identifier: str, classes: List[str]) -> Tuple[nodes.table, nodes.tgroup]: table = nodes.table("", identifier=identifier, classes=classes) hasSecondHeaderRow = False diff --git a/sphinx_reports/Unittest.py b/sphinx_reports/Unittest.py index c74d65da..1173ac2f 100644 --- a/sphinx_reports/Unittest.py +++ b/sphinx_reports/Unittest.py @@ -33,11 +33,13 @@ """ from datetime import timedelta from pathlib import Path -from typing import Dict, Tuple, Any, List, Mapping, Generator, TypedDict +from typing import Dict, Tuple, Any, List, Mapping, Generator, TypedDict, ClassVar -from docutils import nodes -from docutils.parsers.rst.directives import flag -from pyTooling.Decorators import export +from docutils import nodes +from docutils.parsers.rst.directives import flag +from pyTooling.Decorators import export +from sphinx.application import Sphinx +from sphinx.config import Config from sphinx_reports.Common import ReportExtensionError from sphinx_reports.Sphinx import strip, BaseDirective @@ -46,7 +48,7 @@ class report_DictType(TypedDict): - xml_report: str + xml_report: Path @export @@ -67,7 +69,9 @@ class UnittestSummary(BaseDirective): configPrefix: str = "unittest" configValues: Dict[str, Tuple[Any, str, Any]] = { f"{configPrefix}_testsuites": ({}, "env", Dict) - } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) + } #: A dictionary of all configuration values used by unittest directives. + + _testSummaries: ClassVar[Dict[str, report_DictType]] = {} _reportID: str _noAssertions: bool @@ -75,31 +79,55 @@ class UnittestSummary(BaseDirective): _testsuite: TestsuiteSummary def _CheckOptions(self) -> None: - # Parse all directive options or use default values + """ + Parse all directive options or use default values. + """ self._reportID = self._ParseStringOption("reportid") self._noAssertions = "without-assertions" in self.options - def _CheckConfiguration(self) -> None: + testSummary = self._testSummaries[self._reportID] + self._xmlReport = testSummary["xml_report"] + + @classmethod + def CheckConfiguration(cls, sphinxApplication: Sphinx, sphinxConfiguration: Config) -> None: + """ + Check configuration fields and load necessary values. + + :param sphinxApplication: Sphinx application instance. + :param sphinxConfiguration: Sphinx configuration instance. + """ + cls._CheckConfiguration(sphinxConfiguration) + + @classmethod + def _CheckConfiguration(cls, sphinxConfiguration: Config) -> None: from sphinx_reports import ReportDomain - # Check configuration fields and load necessary values + variableName = f"{ReportDomain.name}_{cls.configPrefix}_testsuites" + try: - allTestsuites: Dict[str, report_DictType] = self.config[f"{ReportDomain.name}_{self.configPrefix}_testsuites"] + allTestsuites: Dict[str, report_DictType] = sphinxConfiguration[f"{ReportDomain.name}_{cls.configPrefix}_testsuites"] except (KeyError, AttributeError) as ex: - raise ReportExtensionError(f"Configuration option '{ReportDomain.name}_{self.configPrefix}_testsuites' is not configured.") from ex + raise ReportExtensionError(f"Configuration option '{variableName}' is not configured.") from ex - try: - testsuiteConfiguration = allTestsuites[self._reportID] - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites: No configuration found for '{self._reportID}'.") from ex + # try: + # testsuiteConfiguration = allTestsuites[self._reportID] + # except KeyError as ex: + # raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites: No configuration found for '{self._reportID}'.") from ex - try: - self._xmlReport = Path(testsuiteConfiguration["xml_report"]) - except KeyError as ex: - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites:{self._reportID}.xml_report: Configuration is missing.") from ex + for reportID, testSummary in allTestsuites.items(): + summaryName = f"conf.py: {variableName}:[{reportID}]" + + try: + xmlReport = Path(testSummary["xml_report"]) + except KeyError as ex: + raise ReportExtensionError(f"{summaryName}.xml_report: Configuration is missing.") from ex + + if not xmlReport.exists(): + raise ReportExtensionError(f"{summaryName}.xml_report: Unittest report file '{xmlReport}' doesn't exist.") from FileNotFoundError(xmlReport) - if not self._xmlReport.exists(): - raise ReportExtensionError(f"conf.py: {ReportDomain.name}_{self.configPrefix}_testsuites:{self._reportID}.xml_report: Unittest report file '{self._xmlReport}' doesn't exist.") from FileNotFoundError(self._xmlReport) + cls._testSummaries[reportID] = { + "xml_report": xmlReport + } def _GenerateTestSummaryTable(self) -> nodes.table: # Create a table and table header with 8 columns @@ -118,7 +146,7 @@ def _GenerateTestSummaryTable(self) -> nodes.table: if self._noAssertions: columns.pop(6) - table, tableGroup = self._PrepareTable( + table, tableGroup = self._CreateTableHeader( identifier=self._reportID, columns=columns, classes=["report-unittest-table"] @@ -212,7 +240,6 @@ def renderTestcase(tableBody: nodes.tbody, testcase: Testcase, level: int) -> No def run(self) -> List[nodes.Node]: self._CheckOptions() - self._CheckConfiguration() # Assemble a list of Python source files analyzer = Analyzer(self._xmlReport) diff --git a/sphinx_reports/__init__.py b/sphinx_reports/__init__.py index 861e3a62..bf21b595 100644 --- a/sphinx_reports/__init__.py +++ b/sphinx_reports/__init__.py @@ -46,20 +46,21 @@ __version__ = "0.7.0" __keywords__ = ["Python3", "Sphinx", "Extension", "Report", "doc-string", "interrogate"] -from hashlib import md5 -from pathlib import Path -from typing import Any, Tuple, Dict, Optional as Nullable, TypedDict, List, Callable +from hashlib import md5 +from pathlib import Path +from typing import Any, Tuple, Dict, Optional as Nullable, TypedDict, List, Callable -from docutils import nodes -from sphinx.addnodes import pending_xref -from sphinx.application import Sphinx -from sphinx.builders import Builder -from sphinx.domains import Domain -from sphinx.environment import BuildEnvironment -from pyTooling.Decorators import export +from docutils import nodes +from sphinx.addnodes import pending_xref +from sphinx.application import Sphinx +from sphinx.builders import Builder +from sphinx.config import Config +from sphinx.domains import Domain +from sphinx.environment import BuildEnvironment +from pyTooling.Decorators import export -from . import static as ResourcePackage -from .Common import ReadResourceFile +from sphinx_reports import static as ResourcePackage +from sphinx_reports.Common import ReadResourceFile @export @@ -69,7 +70,12 @@ class ReportDomain(Domain): .. rubric:: New directives: + * :rst:dir:`code-coverage` + * :rst:dir:`code-coverage-legend` * :rst:dir:`doc-coverage` + * :rst:dir:`doc-coverage-legend` + * :rst:dir:`dependency` + * :rst:dir:`unittest-summary` .. rubric:: New roles: @@ -95,15 +101,17 @@ class ReportDomain(Domain): dependencies: List[str] = [ ] #: A list of other extensions this domain depends on. - from sphinx_reports.CodeCoverage import CodeCoverage - from sphinx_reports.DocCoverage import DocStrCoverage + from sphinx_reports.CodeCoverage import CodeCoverage, CodeCoverageLegend + from sphinx_reports.DocCoverage import DocStrCoverage, DocCoverageLegend from sphinx_reports.Unittest import UnittestSummary directives = { - "code-coverage": CodeCoverage, - "dependecy": DocStrCoverage, - "doc-coverage": DocStrCoverage, - "unittest-summary": UnittestSummary, + "code-coverage": CodeCoverage, + "code-coverage-legend": CodeCoverageLegend, + "doc-coverage": DocStrCoverage, + "doc-coverage-legend": DocCoverageLegend, + "dependency": DocStrCoverage, + "unittest-summary": UnittestSummary, } #: A dictionary of all directives in this domain. roles = { @@ -114,9 +122,13 @@ class ReportDomain(Domain): # LibraryIndex, ] #: A list of all indices in this domain. + from sphinx_reports.CodeCoverage import CodeCoverageBase + from sphinx_reports.DocCoverage import DocCoverageBase + from sphinx_reports.Unittest import UnittestSummary + configValues: Dict[str, Tuple[Any, str, Any]] = { - **DocStrCoverage.configValues, - **CodeCoverage.configValues, + **CodeCoverageBase.configValues, + **DocCoverageBase.configValues, **UnittestSummary.configValues, } #: A dictionary of all configuration values used by this domain. (name: (default, rebuilt, type)) @@ -128,6 +140,16 @@ class ReportDomain(Domain): def Reports(self) -> Dict[str, Any]: return self.data["reports"] + @staticmethod + def CheckConfigurationVariables(sphinxApplication: Sphinx, config: Config) -> None: + from sphinx_reports.CodeCoverage import CodeCoverageBase + from sphinx_reports.DocCoverage import DocCoverageBase + from sphinx_reports.Unittest import UnittestSummary + + CodeCoverageBase.CheckConfiguration(sphinxApplication, config) + DocCoverageBase.CheckConfiguration(sphinxApplication, config) + UnittestSummary.CheckConfiguration(sphinxApplication, config) + @staticmethod def AddCSSFiles(sphinxApplication: Sphinx) -> None: """ @@ -183,10 +205,10 @@ def ReadReports(sphinxApplication: Sphinx) -> None: print(f"Callback: builder-inited -> ReadReports") print(f"[REPORT] Reading reports ...") - callbacks: Dict[str, List[Callable]] = { - "builder-inited": [AddCSSFiles, ReadReports], - } #: A dictionary of all `events/callbacks `__ used by this domain. + "config-inited": [CheckConfigurationVariables], # (app, config) + "builder-inited": [AddCSSFiles, ReadReports], # (app) + } #: A dictionary of all events/callbacks `__ used by this domain. def resolve_xref( self, diff --git a/sphinx_reports/static/__init__.py b/sphinx_reports/static/__init__.py index 2e659c50..0440a7e7 100644 --- a/sphinx_reports/static/__init__.py +++ b/sphinx_reports/static/__init__.py @@ -29,5 +29,9 @@ # ==================================================================================================================== # # """ -**Package for static resources.** +Package for static resources. + +**Resources:** + +* ``sphinx_reports.css`` """