File doxyrun.py¶
File List > mkdoxy > doxyrun.py
Go to the documentation of this file
import hashlib
import logging
import os
import shutil
from pathlib import Path, PurePath
from subprocess import PIPE, Popen
from typing import Optional
log: logging.Logger = logging.getLogger("mkdocs")
class DoxygenRun:
"""! Class for running Doxygen.
@details This class is used to run Doxygen and parse the XML output.
"""
def __init__(
self,
doxygenBinPath: str,
doxygenSource: str,
tempDoxyFolder: str,
doxyCfgNew,
doxyConfigFile: Optional[str] = None,
):
"""! Constructor.
Default Doxygen config options:
- INPUT: <doxygenSource>
- OUTPUT_DIRECTORY: <tempDoxyFolder>
- DOXYFILE_ENCODING: UTF-8
- GENERATE_XML: YES
- RECURSIVE: YES
- EXAMPLE_PATH: examples
- SHOW_NAMESPACES: YES
- GENERATE_HTML: NO
- GENERATE_LATEX: NO
@details
@param doxygenBinPath: (str) Path to the Doxygen binary.
@param doxygenSource: (str) Source files for Doxygen.
@param tempDoxyFolder: (str) Temporary folder for Doxygen.
@param doxyConfigFile: (str) Path to a Doxygen config file.
@param doxyCfgNew: (dict) New Doxygen config options that will be added to the default config (new options will overwrite default options)
""" # noqa: E501
if not self.is_doxygen_valid_pathis_doxygen_valid_path(doxygenBinPath):
raise DoxygenBinPathNotValid(
f"Invalid Doxygen binary path: {doxygenBinPath}\n"
f"Make sure Doxygen is installed and the path is correct.\n"
f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-binary."
)
self.doxygenBinPath: str = doxygenBinPath
self.doxygenSource: str = doxygenSource
self.tempDoxyFolder: str = tempDoxyFolder
self.doxyConfigFile: Optional[str] = doxyConfigFile
self.hashFileName: str = "hashChanges.yaml"
self.hashFilePath: PurePath = PurePath.joinpath(Path(self.tempDoxyFolder), Path(self.hashFileName))
self.doxyCfg: dict = self.setDoxyCfgsetDoxyCfg(doxyCfgNew)
def setDoxyCfg(self, doxyCfgNew: dict) -> dict:
"""! Set the Doxygen configuration.
@details If a custom Doxygen config file is provided, it will be used. Otherwise, default options will be used.
@details Order of application of parameters:
@details 1. Custom Doxygen config file
@details 2. If not provided, default options - in documentation
@details 3. New Doxygen config options from mkdocs.yml
@details 3. Overwrite INPUT and OUTPUT_DIRECTORY with the provided values for correct plugin operation.
@details Overwrite options description:
@details - INPUT: <doxygenSource>
@details - OUTPUT_DIRECTORY: <tempDoxyFolder>
@details Default Doxygen config options:
@details - DOXYFILE_ENCODING: UTF-8
@details - GENERATE_XML: YES
@details - RECURSIVE: YES
@details - EXAMPLE_PATH: examples
@details - SHOW_NAMESPACES: YES
@details - GENERATE_HTML: NO
@details - GENERATE_LATEX: NO
@param doxyCfgNew: (dict) New Doxygen config options that will be
added to the default config (new options will overwrite default options)
@return: (dict) Doxygen configuration.
"""
doxyCfg = {}
if self.doxyConfigFile is not None and self.doxyConfigFile != "":
try:
with open(self.doxyConfigFile, "r") as file:
doxyCfg.update(self.str2dox_dictstr2dox_dict(file.read()))
except FileNotFoundError as e:
raise DoxygenCustomConfigNotFound(
f"Custom Doxygen config file not found: {self.doxyConfigFile}\n"
f"Make sure the path is correct."
f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file."
) from e
else:
doxyCfg = {
"DOXYFILE_ENCODING": "UTF-8",
"GENERATE_XML": "YES",
"RECURSIVE": "YES",
"EXAMPLE_PATH": "examples",
"SHOW_NAMESPACES": "YES",
"GENERATE_HTML": "NO",
"GENERATE_LATEX": "NO",
}
doxyCfg.update(doxyCfgNew)
doxyCfg["INPUT"] = self.doxygenSource
doxyCfg["OUTPUT_DIRECTORY"] = self.tempDoxyFolder
return doxyCfg
def is_doxygen_valid_path(self, doxygen_bin_path: str) -> bool:
"""! Check if the Doxygen binary path is valid.
@details Accepts a full path or just 'doxygen' if it exists in the system's PATH.
@param doxygen_bin_path: (str) The path to the Doxygen binary or just 'doxygen'.
@return: (bool) True if the Doxygen binary path is valid, False otherwise.
"""
# If the path is just 'doxygen', search for it in the system's PATH
if doxygen_bin_path.lower() == "doxygen":
return shutil.which("doxygen") is not None
# Use pathlib to check if the provided full path is a file and executable
path = Path(doxygen_bin_path)
return path.is_file() and os.access(path, os.X_OK)
# Source of dox_dict2str: https://xdress-fabio.readthedocs.io/en/latest/_modules/xdress/doxygen.html#XDressPlugin
def dox_dict2str(self, dox_dict: dict) -> str:
"""! Convert a dictionary to a string that can be written to a doxygen config file.
@details
@param dox_dict: (dict) Dictionary to convert.
@return: (str) String that can be written to a doxygen config file.
"""
s = ""
new_line = "{option} = {value}\n"
for key, value in dox_dict.items():
if value is True:
_value = "YES"
elif value is False:
_value = "NO"
else:
_value = value
s += new_line.format(option=key.upper(), value=_value)
# Don't need an empty line at the end
return s.strip()
def str2dox_dict(self, dox_str: str) -> dict:
"""! Convert a string from a doxygen config file to a dictionary.
@details
@param dox_str: (str) String from a doxygen config file.
@return: (dict) Dictionary.
"""
dox_dict = {}
try:
for line in dox_str.split("\n"):
if line.strip() == "":
continue
key, value = line.split(" = ")
if value == "YES":
dox_dict[key] = True
elif value == "NO":
dox_dict[key] = False
else:
dox_dict[key] = value
except ValueError as e:
raise DoxygenCustomConfigNotValid(
f"Invalid custom Doxygen config file: {self.doxyConfigFile}\n"
f"Make sure the file is in standard Doxygen format."
f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/."
) from e
return dox_dict
def hasChanged(self) -> bool:
"""! Check if the source files have changed since the last run.
@details
@return: (bool) True if the source files have changed since the last run.
"""
def hashWrite(filename: PurePath, hash: str):
with open(filename, "w") as file:
file.write(hash)
def hashRead(filename: PurePath) -> str:
with open(filename, "r") as file:
return str(file.read())
sha1 = hashlib.sha1()
srcs = self.doxygenSource.split(" ")
for src in srcs:
for path in Path(src).rglob("*.*"):
# # Code from https://stackoverflow.com/a/22058673/15411117
# # BUF_SIZE is totally arbitrary, change for your app!
BUF_SIZE = 65536 # let's read stuff in 64kb chunks!
if path.is_file():
with open(path, "rb") as f:
while True:
data = f.read(BUF_SIZE)
if not data:
break
sha1.update(data)
# print(f"{path}: {sha1.hexdigest()}")
hashNew = sha1.hexdigest()
if Path(self.hashFilePath).is_file():
hashOld = hashRead(self.hashFilePath)
if hashNew == hashOld:
return False
hashWrite(self.hashFilePath, hashNew)
return True
def run(self):
"""! Run Doxygen with the current configuration using the Popen class.
@details
"""
doxyBuilder = Popen(
[self.doxygenBinPath, "-"],
stdout=PIPE,
stdin=PIPE,
stderr=PIPE,
)
(doxyBuilder.communicate(self.dox_dict2strdox_dict2str(self.doxyCfg).encode("utf-8"))[0].decode().strip())
# log.info(self.destinationDir)
# log.info(stdout_data)
def checkAndRun(self):
"""! Check if the source files have changed since the last run and run Doxygen if they have.
@details
@return: (bool) True if Doxygen was run.
"""
if self.hasChangedhasChanged():
self.runrun()
return True
else:
return False
def getOutputFolder(self) -> PurePath:
"""! Get the path to the XML output folder.
@details
@return: (PurePath) Path to the XML output folder.
"""
return Path.joinpath(Path(self.tempDoxyFolder), Path("xml"))
# not valid path exception
class DoxygenBinPathNotValid(Exception):
pass
class DoxygenCustomConfigNotFound(Exception):
pass
class DoxygenCustomConfigNotValid(Exception):
pass