diff --git a/.github/workflows/mkdoxy-test-demos.yaml b/.github/workflows/mkdoxy-test-demos.yaml index 8d09ba83..9fedfc55 100644 --- a/.github/workflows/mkdoxy-test-demos.yaml +++ b/.github/workflows/mkdoxy-test-demos.yaml @@ -24,7 +24,7 @@ jobs: python -m pip install -e ".[dev]" sudo apt-get install doxygen - name: Clone test repo - run: git clone https://github.com/JakubAndrysek/MkDoxy-demo.git demo + run: git clone --branch core-update https://github.com/JakubAndrysek/MkDoxy-demo.git demo - name: Build docs run: | cd demo diff --git a/.gitignore b/.gitignore index 0aad63ce..0dde6add 100644 --- a/.gitignore +++ b/.gitignore @@ -289,3 +289,5 @@ tests/files/.mkdoxy .mkdoxy/ temp/ local/ + +.idea/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 1a0176ac..00000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index d678e147..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index cc5462da..00000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/.idea/main-mkdoxy.iml b/.idea/main-mkdoxy.iml deleted file mode 100644 index 76f7618c..00000000 --- a/.idea/main-mkdoxy.iml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 97b30fe4..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index a3ab9e8d..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 5ace414d..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 6a4636cb..00000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - "associatedIndex": 3 -} - - - - - { - "keyToString": { - "Python tests.pytest in /.executor": "Run", - "Python.mkdocs-serve-vPy3.11.executor": "Run", - "Python.mkdoxy-demo-vPy3.11.executor": "Debug", - "RunOnceActivity.OpenProjectViewOnStart": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "SHARE_PROJECT_CONFIGURATION_FILES": "true", - "WebServerToolWindowFactoryState": "false", - "git-widget-placeholder": "main", - "last_opened_file_path": "/Users/kuba/Documents/git/kuba/mkdoxy/mkdoxy-base/mkdoxy-main", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", - "vue.rearranger.settings.migration": "true" - } -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1636831708250 - - - 1674630349699 - - - 1674631616905 - - - 1679404123954 - - - 1679405696796 - - - 1679406849683 - - - 1679412445947 - - - 1679412916914 - - - 1679412944638 - - - 1679475560538 - - - 1680348700460 - - - 1680349313071 - - - 1680349715098 - - - 1681656417972 - - - 1681656572465 - - - 1681657852137 - - - 1681663034138 - - - 1683158225350 - - - 1683158771920 - - - 1683158793244 - - - 1683242202337 - - - 1686746284046 - - - 1686746715966 - - - 1686746897657 - - - 1689593119646 - - - 1689777297262 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - file://$PROJECT_DIR$/mkdoxy/generatorAuto.py - 235 - - - file://$PROJECT_DIR$/mkdoxy/node.py - 221 - - - file://$PROJECT_DIR$/mkdoxy/node.py - 224 - - - file://$PROJECT_DIR$/tests-old/snippetsTest-examples.py - 49 - - - file://$PROJECT_DIR$/mkdoxy/node.py - 839 - - - file://$PROJECT_DIR$/mkdoxy/DoxyTagParser.py - 41 - - - file://$PROJECT_DIR$/mkdoxy/DoxyTagParser.py - 21 - - - file://$PROJECT_DIR$/mkdoxy/DoxyTagParser.py - 34 - - - file://$PROJECT_DIR$/local/test_generatorSnippets.py - 22 - - - file://$PROJECT_DIR$/mkdoxy/doxyrun.py - 94 - - - file://$PROJECT_DIR$/mkdoxy/doxyrun.py - 111 - - - file://$PROJECT_DIR$/tests/test_doxyrun.py - 14 - - - file://$PROJECT_DIR$/mkdoxy/generatorBase.py - 67 - - - file://$PROJECT_DIR$/mkdoxy/templates/relatedPages.jinja2 - 5 - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1d7c8fe..9592a6cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,15 @@ --- repos: + - repo: https://github.com/sourcery-ai/sourcery + rev: v1.19.0 + hooks: + - id: sourcery + # The best way to use Sourcery in a pre-commit hook: + # * review only changed lines: + # * omit the summary + args: [--diff=git diff HEAD, --no-summary] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -9,8 +17,12 @@ repos: rev: 23.9.1 hooks: - id: black - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.292' + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.9.6 hooks: + # Run the linter. - id: ruff - args: [--fix, --exit-non-zero-on-fix] + args: [ --fix, --exit-non-zero-on-fix ] + # Run the formatter. + - id: ruff-format diff --git a/Makefile b/Makefile index 312f42b4..aaea103f 100755 --- a/Makefile +++ b/Makefile @@ -3,20 +3,20 @@ # Packaging package: rm -f dist/* - python3 -m build --wheel # Updated to use python3 build tool for wheel - python3 setup.py sdist + hatch build install: package - python3 -m pip install --no-deps --force dist/*.whl + hatch install release: package - twine upload --repository pypi dist/* + hatch publish release-test: package - twine upload --repository testpypi dist/* + hatch publish --index test clean: rm -rf dist build + hatch clean # Testing @@ -24,7 +24,7 @@ reviewCode: sourcery review mkdoxy --in-place install-dev: - python3 -m pip install --force --editable . + hatch env create # Documentation docs-serve: @@ -35,3 +35,13 @@ docs-build: # results in site directory pre-commit: pre-commit run --show-diff-on-failure --color=always --all-files + +# Linting +lint: + ruff check mkdoxy + +format: + ruff format mkdoxy + +lint-fix: + ruff check --fix mkdoxy diff --git a/README.md b/README.md index 70cbe63e..fa21878e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ pip install mkdoxy ``` Development version with all dependencies: ```bash +python -m venv .venv python -m pip install mkdoxy ".[dev]" ``` diff --git a/devdeps.txt b/devdeps.txt deleted file mode 100644 index 43234dad..00000000 --- a/devdeps.txt +++ /dev/null @@ -1,9 +0,0 @@ -mkdocs-material~=9.5.18 -mkdocs-open-in-new-tab~=1.0.2 -pathlib~=1.0.1 -isort~=5.13.2 -pytest~=8.2.2 -pre-commit~=3.7.0 -setuptools~=70.0.0 -build~=1.2.2 -twine~=5.1.1 diff --git a/mkdocs.yml b/mkdocs.yml index dc5a3e34..0e7b75c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,9 +25,23 @@ theme: repo: fontawesome/brands/github palette: - - scheme: slate + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate primary: orange accent: orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: orange + accent: orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode extra: social: @@ -64,11 +78,11 @@ plugins: enabled: !ENV [ENABLE_MKDOXY, True] projects: mkdoxyApi: - src-dirs: mkdoxy - full-doc: True - template-dir: templates-custom - doxy-cfg-file: demo-projects/animal/Doxyfile - doxy-cfg: + src_dirs: mkdoxy + full_doc: True + template_dir: templates-custom +# doxy_config_file: demo-projects/animal/Doxyfile + doxy_config_dict: FILE_PATTERNS: "*.py" EXAMPLE_PATH: "" RECURSIVE: True @@ -76,17 +90,44 @@ plugins: JAVADOC_AUTOBRIEF: True EXTRACT_ALL: True animal: - src-dirs: demo-projects/animal - full-doc: True - doxy-cfg: + src_dirs: demo-projects/animal + full_doc: True + doxy_config_dict: FILE_PATTERNS: "*.cpp *.h*" EXAMPLE_PATH: examples RECURSIVE: True - save-api: .mkdoxy - full-doc: True +# save_api: .mkdoxy + full_doc: True debug: False - ignore-errors: False - emojis-enabled: True + ignore_errors: False + +# - mkdoxy: +# enabled: !ENV [ENABLE_MKDOXY, True] +# projects: +# mkdoxyApi: +# src-dirs: mkdoxy +# full-doc: True +# template-dir: templates-custom +# doxy-cfg-file: demo-projects/animal/Doxyfile +# doxy-cfg: +# FILE_PATTERNS: "*.py" +# EXAMPLE_PATH: "" +# RECURSIVE: True +# OPTIMIZE_OUTPUT_JAVA: True +# JAVADOC_AUTOBRIEF: True +# EXTRACT_ALL: True +# animal: +# src-dirs: demo-projects/animal +# full-doc: True +# doxy-cfg: +# FILE_PATTERNS: "*.cpp *.h*" +# EXAMPLE_PATH: examples +# RECURSIVE: True +# save-api: .mkdoxy +# full-doc: True +# debug: False +# ignore-errors: False +# emojis-enabled: True markdown_extensions: - pymdownx.highlight diff --git a/mkdoxy/DoxyTagParser.py b/mkdoxy/DoxyTagParser.py index db450f56..8d35d4a1 100644 --- a/mkdoxy/DoxyTagParser.py +++ b/mkdoxy/DoxyTagParser.py @@ -2,36 +2,36 @@ class DoxyTagParser: - def __init__(self, markdown_page: str, debug: bool = False): + def __init__(self, markdown_page: str, debug: bool = False) -> None: self.markdown_page = markdown_page self.debug = debug self.doxy_key = "::: doxy" - self.indent = "(?P[\t ]*)" + self.indent = "(?P[\\t ]*)" self.project = "(?P[a-zA-Z]+)" self.key = "(?P[a-zA-Z.-_]+)" - self.dot = "\." + self.dot = "\\." self.optional_dot = "[.]?" - self.look_ahead = "(?=\n)" # it's a look ahead because we don't want to capture the newline + self.look_ahead = "(?=\\n)" # it's a look ahead because we don't want to capture the newline - def replaceMarkdown(self, start: int, end: int, replace_format: str, **kwargs): + def replace_markdown(self, start: int, end: int, replace_format: str, **kwargs) -> None: self.markdown_page = self.markdown_page.replace(self.markdown_page[start:end], replace_format.format(**kwargs)) - def returnMarkdown(self): + def return_markdown(self): return self.markdown_page - def parseEmptyTag(self, replacement: str): + def parse_empty_tag(self, replacement: str) -> None: empty_tag = ( rf"{self.indent}{self.doxy_key}{self.optional_dot}{self.look_ahead}" # https://regex101.com/r/Zh38uo/1 ) matches = re.finditer(empty_tag, self.markdown_page, re.MULTILINE) for match in reversed(list(matches)): - self.replaceMarkdown(match.start(), match.end(), replacement, indent=match.group("indent")) + self.replace_markdown(match.start(), match.end(), replacement, indent=match.group("indent")) - def parseProject(self, replacement: str): + def parse_project(self, replacement: str) -> None: project_tag = rf"{self.indent}{self.doxy_key}{self.dot}{self.project}{self.optional_dot}{self.look_ahead}" # https://regex101.com/r/TfAsmE/1 matches = re.finditer(project_tag, self.markdown_page, re.MULTILINE) for match in reversed(list(matches)): - self.replaceMarkdown( + self.replace_markdown( match.start(), match.end(), replacement, @@ -39,13 +39,13 @@ def parseProject(self, replacement: str): project=match.group("project"), ) - def parseProjectTagSingle(self, replacement: str): + def parse_project_tag_single(self, replacement: str) -> None: project_tag = ( rf"{self.indent}{self.doxy_key}{self.dot}{self.project}{self.dot}(?P[a-zA-Z-_]+){self.look_ahead}" ) matches = re.finditer(project_tag, self.markdown_page, re.MULTILINE) for match in reversed(list(matches)): - self.replaceMarkdown( + self.replace_markdown( match.start(), match.end(), replacement, @@ -53,12 +53,12 @@ def parseProjectTagSingle(self, replacement: str): key=match.group("key"), ) - def parseProjectTagMulti(self, replacement: str): - project_tag = rf"{self.indent}{self.doxy_key}{self.dot}{self.project}{self.dot}(?P[a-zA-Z-_]+)\s*\n(?:(?=\n)|(?=:::)|\Z)" # noqa: E501 + def parse_project_tag_multi(self, replacement: str) -> None: + project_tag = rf"{self.indent}{self.doxy_key}{self.dot}{self.project}{self.dot}(?P[a-zA-Z-_]+)\\s*\\n(?:(?=\\n)|(?=:::)|\\Z)" # noqa: E501 matches = re.finditer(project_tag, self.markdown_page, re.MULTILINE) for match in reversed(list(matches)): list_keys = match.group("key").split(".") # split keys by . to allow for nested keys - self.replaceMarkdown( + self.replace_markdown( match.start(), match.end(), replacement, diff --git a/mkdoxy/__main__.py b/mkdoxy/__main__.py new file mode 100644 index 00000000..0b3242c6 --- /dev/null +++ b/mkdoxy/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from mkdoxy.cli import main + +if __name__ == "__main__": + main() diff --git a/mkdoxy/cache.py b/mkdoxy/cache.py index b0c93fc6..30a98239 100644 --- a/mkdoxy/cache.py +++ b/mkdoxy/cache.py @@ -1,8 +1,8 @@ class Cache: - def __init__(self): + def __init__(self) -> None: self.cache = {} - def add(self, key: str, value): + def add(self, key: str, value) -> None: self.cache[key] = value def get(self, key: str): diff --git a/mkdoxy/cli.py b/mkdoxy/cli.py new file mode 100644 index 00000000..600e5701 --- /dev/null +++ b/mkdoxy/cli.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +from pathlib import Path + +import click + +from mkdoxy.migration import update_new_config + + +@click.group() +def main() -> None: + """mkdoxy - Command line tool for managing Doxygen configuration migration.""" + + +@click.command() +@click.argument("yaml_file", type=click.Path(exists=True)) +@click.option("--no-backup", is_flag=True, help="Do not backup old config to mkdocs.1_old.yaml") +def migrate(yaml_file, no_backup) -> None: + """ + Migrate mkdoxy configuration to a new version. + + :param yaml_file: Path to the mkdocs.yaml file. + :param no_backup: Do not backup the old config to mkdocs.1_old.yaml. + """ + backup_file_name = "mkdocs.1_old.yaml" + update_new_config(Path(yaml_file), not no_backup, backup_file_name) + click.echo("Migration completed successfully") + if not no_backup: + click.echo(f"Old config was backed up as '{backup_file_name}'") + + +@click.command() +def version() -> None: + """ + Display the version of the mkdoxy package. + """ + try: + import importlib.metadata + + package_version = importlib.metadata.version("mkdoxy") + except Exception: + package_version = "Unknown" + click.echo("MkDoxy: https://github.com/JakubAndrysek/MkDoxy") + click.echo(f"Version: {package_version}") + + +main.add_command(migrate) +main.add_command(version) + +if __name__ == "__main__": + main() diff --git a/mkdoxy/constants.py b/mkdoxy/constants.py index c9be98ea..dff41b19 100644 --- a/mkdoxy/constants.py +++ b/mkdoxy/constants.py @@ -37,6 +37,7 @@ "operator<<=", "operator>>=", "operator[]", + "operator()", "operator*", "operator&", "operator->", @@ -168,3 +169,6 @@ class Visibility(Enum): PACKAGE = "package" PROTECTED = "protected" PRIVATE = "private" + + +JINJA_EXTENSIONS = (".jinja2", ".j2", ".jinja") diff --git a/mkdoxy/doxy_config.py b/mkdoxy/doxy_config.py new file mode 100644 index 00000000..f5b316c8 --- /dev/null +++ b/mkdoxy/doxy_config.py @@ -0,0 +1,227 @@ +import logging +from pathlib import Path + +from mkdocs.config import Config +from mkdocs.config import config_options as c + +log: logging.Logger = logging.getLogger("mkdocs") + +config_scheme_legacy = { + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "save-api": "custom_api_folder", + "doxygen-bin-path": "doxygen_bin_path", +} + +config_project_legacy = { + "src-dirs": "src_dirs", + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "doxy-cfg": "doxy_config_dict", + "doxy-cfg-file": "doxy_config_file", + "template-dir": "custom_template_dir", +} + + +class MkDoxyConfigProject(Config): + """! Configuration for each project in the MkDoxy configuration file. + @details New type of configuration for each project in the MkDoxy configuration file. + It will replace the old configuration type. + + @param src_dirs: (str) Source directories for Doxygen - INPUT + @param full_doc: (bool) Generate full documentation + @param debug: (bool) Debug mode + @param ignore_errors: (bool) Ignore errors + @param doxy_config_dict: (dict) Doxygen additional configuration + @param doxy_config_default: (bool) Use default MkDoxy Doxygen configuration + @param doxy_config_file: (str) Doxygen configuration file + @param doxy_config_file_force: (bool) Do not use default MkDoxy Doxygen configuration, use only Doxygen configuration file + @param custom_template_dir: (str) Custom template directory + """ + + src_dirs = c.Type(str) + full_doc = c.Type(bool, default=True) + debug = c.Type(bool, default=False) + ignore_errors = c.Type(bool, default=False) + doxy_config_dict = c.Type(dict, default={}) + doxy_config_default = c.Type(bool, default=True) + doxy_config_file = c.Optional(c.Type(Path)) + doxy_config_file_force = c.Type(bool, default=False) + custom_template_dir = c.Optional(c.Type(str)) + + def validate(self): + failed, warnings = super().validate() + + # Add a warning for deprecated configuration keys + unused_keys = set(self.keys()) - self._schema_keys + for k in unused_keys.intersection(config_project_legacy.keys()): + warnings.append((k, f"Deprecated configuration name: {k} -> {config_project_legacy[k]}")) + + return failed, warnings + + +class MkDoxyConfig(Config): + """! Global configuration for the MkDoxy plugin. + @details New type of global configuration for the MkDoxy plugin. It will replace the old configuration type. + @param projects: (dict) Project configuration - multiple projects + @param full_doc: (bool) Generate full documentation - global (all projects) + @param debug: (bool) Debug mode + @param ignore_errors: (bool) Ignore errors + @param custom_api_folder: (str) Custom API folder for Doxygen and MD output (default in temp folder) + @param doxygen_bin_path: (str) Path to Doxygen binary - default "doxygen" + """ + + projects = c.DictOfItems(c.SubConfig(MkDoxyConfigProject), default={}) # project configuration - multiple projects + full_doc = c.Type(bool, default=True) # generate full documentation - global (all projects) + debug = c.Type(bool, default=False) # debug mode + ignore_errors = c.Type(bool, default=False) # ignore errors + custom_api_folder = c.Optional(c.Type(str)) # custom API folder for Doxygen and MD output (default in temp folder) + doxy_config_dict = c.Type( + dict, default={} + ) # Doxygen additional configuration - it is overwritten by project config + doxygen_bin_path = c.Type(Path, default=Path("doxygen")) # path to Doxygen binary (default "doxygen" + + generate_diagrams = c.Type(bool, default=False) # generate diagrams + generate_diagrams_format = c.Choice(("svg", "png", "jpg", "gif"), default="svg") # diagram format + generate_diagrams_type = c.Choice(("dot", "uml"), default="dot") # diagram type + + def validate(self): + failed, warnings = super().validate() + + # Add a warning for deprecated configuration keys + unused_keys = set(self.keys()) - self._schema_keys + for k in unused_keys.intersection(config_scheme_legacy.keys()): + warnings.append((k, f"Deprecated configuration name: {k} -> {config_scheme_legacy[k]}")) + + # Include warnings from sub-config options in mkdoxy.projects + warnings.extend(next(s.option_type.warnings for k, s in self._schema if k == "projects")) + + return failed, warnings + + +# def load_config_by_key(key: str, legacy_key: str, config: Config, legacy: list) -> any: +# """! Load the configuration value from the global configuration +# @details Legacy config option is by default None, but if it is not None, it will print a warning and return value. +# @param key: (str) The new configuration key. +# @param legacy_key: (str) The legacy configuration key. +# @param config: (Config) The global configuration object. +# @param legacy: (list) The list of legacy configuration options. +# @return: (Optional[str]) The configuration value. +# """ +# if config.get(legacy_key) is not None: +# legacy.append(f"Found legacy configuration options: '{legacy_key}' -> replace with '{key}'") +# return config.get(legacy_key) +# return config.get(key) +# +# +# def process_configuration(config: Config) -> MkDoxyConfig: +# """! Process the configuration for the MkDoxy plugin +# @details Process the configuration for the MkDoxy plugin and validate the configuration. +# It will try to load new configuration, but it will also check for legacy configuration options. +# @param config: (Config) The global configuration object. +# @return: (MkDoxyConfig) The new validated configuration object. +# @throws ConfigurationError: If the configuration is invalid. +# """ +# legacy_options = [] +# doxy_config = MkDoxyConfig() +# doxy_config.full_doc = load_config_by_key("full_doc", "full-doc", config, legacy_options) +# doxy_config.debug = config.get("debug", False) +# doxy_config.ignore_errors = load_config_by_key("ignore_errors", "ignore-errors", config, legacy_options) +# doxy_config.custom_api_folder = load_config_by_key("custom_api_folder", "save-api", config, legacy_options) +# doxy_config.doxygen_bin_path = load_config_by_key("doxygen_bin_path", "doxygen-bin-path", config, legacy_options) +# +# doxy_config.generate_diagrams = config.get("generate_diagrams") +# doxy_config.generate_diagrams_format = config.get("generate_diagrams_format") +# doxy_config.generate_diagrams_type = config.get("generate_diagrams_type") +# +# # Validate the global configuration +# validate_project_config(doxy_config, legacy_options) +# +# # Validate and load project configuration +# for project_name, project_cfg in config.get("projects", {}).items(): +# doxy_config.projects[project_name] = load_project_config(project_cfg, project_name) +# +# return doxy_config +# +# +# def validate_project_config(doxy_cfg: Config, legacy_options: list[str]) -> None: +# """! Validate the project configuration for the MkDoxy plugin +# @details Validate the project configuration for the MkDoxy plugin and check for errors and warnings. +# @param doxy_cfg: (MkDoxyConfig) The project configuration object. +# @param legacy_options: (list) The list of problems. +# @return: None +# @throws ConfigurationError: If the configuration is invalid. +# """ +# if legacy_options: +# log.warning("Found some legacy configuration options, please update your configuration!") +# log.warning("Run command 'mkdoxy migrate mkdocs.yaml' to update your configuration to the new format!") +# log.warning("More information in the documentation: https://mkdoxy.kubaandrysek.cz/") +# for problem in legacy_options: +# log.warning(f" -> {problem}") +# +# failed, warnings = doxy_cfg.validate() +# +# for config_name, warning in warnings: +# log.warning(f" -> Config value: '{config_name}'. Warning: {warning}") +# +# for config_name, error in failed: +# log.error(f" -> Config value: '{config_name}'. Error: {error}") +# raise exceptions.ConfigurationError(f"Config value: '{config_name}'. Error: {error}") +# +# +# def load_project_config_by_key(key: str, legacy_key: str, project_cfg: dict, project_name: str, problems: list) -> any: +# """! Load the project configuration value from the project configuration +# @details Legacy project config option is by default None, but if it is not None, +# it will print a warning and return the value. +# @param key: (str) The new project configuration key. +# @param legacy_key: (str) The legacy project configuration key. +# @param project_cfg: (dict) The project configuration object. +# @param project_name: (str) The project name. +# @param problems: (list) The list of problems. +# @return: (Optional[str]) The project configuration value. +# """ +# if project_cfg.get(legacy_key) is not None: +# problems.append( +# f"Found legacy configuration options: '{legacy_key}' -> replace with '{key}'" +# f" in project '{project_name}'" +# ) +# return project_cfg.get(legacy_key) +# return project_cfg.get(key) +# +# +# def load_project_config(project_cfg: dict, project_name: str) -> MkDoxyConfigProject: +# """! Load the project configuration for the MkDoxy plugin +# @details Load the project configuration for the MkDoxy plugin and validate the configuration. +# @param project_cfg: (dict) The project configuration object. +# @param project_name: (str) The project name. +# @return: (MkDoxyConfigProject) The new validated project configuration object. +# """ +# legacy_options = [] +# doxy_project_cfg = MkDoxyConfigProject() +# doxy_project_cfg.src_dirs = load_project_config_by_key( +# "src_dirs", "src-dirs", project_cfg, project_name, legacy_options +# ) +# +# doxy_project_cfg.full_doc = load_project_config_by_key( +# "full_doc", "full-doc", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.debug = project_cfg.get("debug", False) +# doxy_project_cfg.ignore_errors = load_project_config_by_key( +# "ignore_errors", "ignore-errors", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.doxy_config_dict = load_project_config_by_key( +# "doxy_config_dict", "doxy-cfg", project_cfg, project_name, legacy_options +# ) +# +# validate_config_file: Optional[str] = load_project_config_by_key( +# "doxy_config_file", "doxy-cfg-file", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.doxy_config_file = None if validate_config_file is None else Path(validate_config_file) +# +# validate_template_dir: Optional[str] = load_project_config_by_key( +# "custom_template_dir", "template-dir", project_cfg, project_name, legacy_options +# ) +# doxy_project_cfg.custom_template_dir = None if validate_template_dir is None else Path(validate_template_dir) +# +# validate_project_config(doxy_project_cfg, legacy_options) +# return doxy_project_cfg diff --git a/mkdoxy/doxygen.py b/mkdoxy/doxygen.py index 39c70c00..50c17330 100644 --- a/mkdoxy/doxygen.py +++ b/mkdoxy/doxygen.py @@ -1,6 +1,6 @@ import logging import os -from xml.etree import ElementTree +from xml.etree import ElementTree as ET from mkdoxy.cache import Cache from mkdoxy.constants import Kind, Visibility @@ -12,12 +12,12 @@ class Doxygen: - def __init__(self, index_path: str, parser: XmlParser, cache: Cache): + def __init__(self, index_path: str, parser: XmlParser, cache: Cache) -> None: self.debug = parser.debug path_xml = os.path.join(index_path, "index.xml") if self.debug: - log.info(f"Loading XML from: {path_xml}") - xml = ElementTree.parse(path_xml).getroot() + log.info("Loading XML from: %s", path_xml) + xml = ET.parse(path_xml).getroot() self.parser = parser self.ctx = ProjectContext(cache) @@ -106,7 +106,7 @@ def __init__(self, index_path: str, parser: XmlParser, cache: Cache): self._recursive_sort(self.pages) self._recursive_sort(self.examples) - def _fix_parents(self, node: Node): + def _fix_parents(self, node: Node) -> None: if node.is_dir or node.is_root: for child in node.children: if child.is_file: @@ -114,7 +114,7 @@ def _fix_parents(self, node: Node): if child.is_dir: self._fix_parents(child) - def _recursive_sort(self, node: Node): + def _recursive_sort(self, node: Node) -> None: node.sort_children() for child in node.children: self._recursive_sort(child) @@ -122,13 +122,13 @@ def _recursive_sort(self, node: Node): def _is_in_root(self, node: Node, root: Node): return any(node.refid == child.refid for child in root.children) - def _remove_from_root(self, refid: str, root: Node): + def _remove_from_root(self, refid: str, root: Node) -> None: for i, child in enumerate(root.children): if child.refid == refid: root.children.pop(i) return - def _fix_duplicates(self, node: Node, root: Node, filter: [Kind]): + def _fix_duplicates(self, node: Node, root: Node, filter: [Kind]) -> None: for child in node.children: if len(filter) > 0 and child.kind not in filter: continue @@ -136,7 +136,7 @@ def _fix_duplicates(self, node: Node, root: Node, filter: [Kind]): self._remove_from_root(child.refid, root) self._fix_duplicates(child, root, filter) - def printStructure(self): + def printStructure(self) -> None: if not self.debug: return print("\n") @@ -154,8 +154,8 @@ def printStructure(self): for node in self.files.children: self.print_node(node, "") - def print_node(self, node: Node, indent: str): + def print_node(self, node: Node, indent: str) -> None: if self.debug: - log.info(f"{indent} {node.kind} {node.name}") + log.info("%s %s %s", indent, node.kind, node.name) for child in node.children: self.print_node(child, f"{indent} ") diff --git a/mkdoxy/doxygen_generator.py b/mkdoxy/doxygen_generator.py new file mode 100644 index 00000000..f0ca72f6 --- /dev/null +++ b/mkdoxy/doxygen_generator.py @@ -0,0 +1,401 @@ +import hashlib +import logging +import os +import re +import shutil +from pathlib import Path +from subprocess import PIPE, Popen + +from mkdocs import exceptions + +from mkdoxy.doxy_config import MkDoxyConfig, MkDoxyConfigProject + +log: logging.Logger = logging.getLogger("mkdocs") + + +class DoxygenGenerator: + """! Class for running Doxygen. + @details This class is used to run Doxygen and parse the XML output. + """ + + def __init__( + self, + doxy_config: MkDoxyConfig, + project_config: MkDoxyConfigProject, + temp_doxy_folder: Path, + ) -> None: + """! Constructor. + @details + @param doxy_config: (MkDoxyConfig) Doxygen configuration. + @param project_config: (MkDoxyConfigProject) Project configuration. + @param temp_doxy_folder: (Path) Temporary Doxygen folder. + """ + self.doxy_config = doxy_config + self.project_config = project_config + self.temp_doxy_folder = temp_doxy_folder + + if not self.is_doxygen_valid_path(doxy_config.doxygen_bin_path): + raise DoxygenBinPathNotValid( + f"Invalid Doxygen binary path: {doxy_config.doxygen_bin_path}\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." + ) + + @staticmethod + def get_doxy_format_config() -> dict: + """ + @brief Get the default Doxygen format configuration. + @details Default Doxygen configuration options: + @details - GENERATE_XML: YES + @details - GENERATE_HTML: NO + @details - GENERATE_LATEX: NO + """ + return { + "GENERATE_XML": True, + "GENERATE_HTML": False, + "GENERATE_LATEX": False, + } + + @staticmethod + def get_doxy_default_config() -> dict: + """ + @brief Get the default Doxygen configuration. + @details Default Doxygen configuration options: + @details - DOXYFILE_ENCODING: UTF-8 + @details - RECURSIVE: YES + @details - EXAMPLE_PATH: examples + @details - SHOW_NAMESPACES: YES + """ + return { + "DOXYFILE_ENCODING": "UTF-8", + "RECURSIVE": True, + "EXAMPLE_PATH": "examples", + "SHOW_NAMESPACES": True, + } + + def get_doxy_diagrams_config(self) -> dict: + """ + @brief Get the Doxygen diagrams configuration. + @details Doxygen diagrams configuration options: + @details - HAVE_DOT: YES + @details - DOT_IMAGE_FORMATS: + @details - UML_LOOK: YES if is "uml", NO otherwise + @details - DOT_CLEANUP: NO + @details - GENERATE_LEGEND: NO + @details - SEARCHENGINE: NO + @details - GENERATE_HTML: YES (required for diagrams) + """ + return { + "HAVE_DOT": True, + "DOT_IMAGE_FORMATS": self.doxy_config.generate_diagrams_format, + "UML_LOOK": self.doxy_config.generate_diagrams_type == "uml", + "DOT_CLEANUP": False, + "GENERATE_LEGEND": False, + "SEARCHENGINE": False, + "GENERATE_HTML": True, + } + + # have to be tested + # doxy_config["CLASS_DIAGRAMS"] = "YES" + # doxy_config["COLLABORATION_GRAPH"] = "YES" + # doxy_config["INCLUDE_GRAPH"] = "YES" + # doxy_config["GRAPHICAL_HIERARCHY"] = "YES" + # doxy_config["CALL_GRAPH"] = "YES" + # doxy_config["CALLER_GRAPH"] = "YES" + + def get_doxy_config_file(self): + """! Get the Doxygen configuration from the provided file. + @details + @return: (dict) Doxygen configuration from the provided file. + """ + return self.str2dox_dict(self.get_doxy_config_file_raw(), self.project_config.doxy_config_file) + + def get_doxy_config_file_raw(self): + """! Get the Doxygen configuration from the provided file. + @details + @return: (str) Doxygen configuration from the provided file. + """ + try: + with open(self.project_config.doxy_config_file) as file: + return file.read() + except FileNotFoundError as e: + raise DoxygenCustomConfigNotFound( + f"Custom Doxygen config file not found\n" + f"Make sure the path is correct." + f"Loaded path: '{self.project_config.doxy_config_file}'\n" + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file.\n" + ) from e + + def get_merged_doxy_dict(self) -> dict: + """! Get the merged Doxygen configuration. + @details The merged Doxygen configuration is created by merging multiple configurations. + @details The hierarchy is as follows: + @details - If a Doxygen config file is provided, it is used. + @details - If not, the default Doxygen configuration is used. + @details - Merge the INPUT directories from the mkdocs.yml file with the Doxygen configuration. + @details - Add the OUTPUT_DIRECTORY to the temporary Doxygen folder. + @details - Update configuration with the project format configuration. + @details - Update configuration with the default configuration. + @details - Update configuration with the project configuration. + @details - Update configuration with the diagrams configuration if enabled. + @return: (dict) Merged Doxygen configuration. + """ + doxy_dict = {} + + # Update with Doxygen config file if provided + if self.project_config.doxy_config_file: + doxy_dict.update(self.get_doxy_config_file()) + else: + doxy_dict.update(self.get_doxy_default_config()) + + # Merge INPUT directories from the mkdocs.yml file with the Doxygen configuration + doxy_dict["INPUT"] = self.merge_doxygen_input( + self.project_config.src_dirs, doxy_dict.get("INPUT", ""), self.get_doxygen_run_folder() + ) + + # OUTPUT_DIRECTORY is always the temporary Doxygen folder + doxy_dict["OUTPUT_DIRECTORY"] = str(self.temp_doxy_folder) + + # Update with the project format configuration + doxy_dict.update(self.get_doxy_format_config()) + + # Update with the default configuration + doxy_dict.update(self.doxy_config.doxy_config_dict) + + # Update with the project configuration + doxy_dict.update(self.project_config.doxy_config_dict) + + if self.doxy_config.generate_diagrams: + doxy_dict.update(self.get_doxy_diagrams_config()) + + if doxy_dict["INPUT"] == "": + raise exceptions.PluginError( + "No INPUT directories provided for Doxygen.\n" + "Make sure to provide at least one source directory." + "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/#configure-custom-doxygen-configuration-file." + ) + + log.debug(f"- Doxygen INPUT: {doxy_dict['INPUT']}") + + return doxy_dict + + @staticmethod + def merge_doxygen_input(src_dirs: str, doxy_input: str, doxygen_run_folder: Path) -> str: + """! Merge the INPUT directories from the mkdocs.yml file with the Doxygen configuration. + + @details Both `src_dirs` and `doxy_input` should be space-separated strings. + Each path is resolved relative to `doxygen_run_folder`. + The function returns a space-separated string of unique relative paths. + + @param src_dirs: (str) Source directories from the mkdocs.yml file. + @param doxy_input: (str) Doxygen INPUT directories. + @param doxygen_run_folder: (Path) The folder to execute + @return: (str) Merged INPUT directories. + """ + # If either input is empty, return the other. + if not src_dirs: + return doxy_input + if not doxy_input: + return src_dirs + + base_dir = doxygen_run_folder.resolve() + + abs_paths = {(base_dir / path_str).resolve() for path_str in src_dirs.split()} + for path_str in doxy_input.split(): + abs_paths.add((base_dir / path_str).resolve()) + + # Convert absolute paths back to relative ones and sort for consistency + relative_paths = sorted(os.path.relpath(p, base_dir) for p in abs_paths) + + return " ".join(relative_paths) + + @staticmethod + def is_doxygen_valid_path(doxygen_bin_path: Path) -> 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 str(doxygen_bin_path) == "doxygen": + return shutil.which("doxygen") is not None + + # Use pathlib to check if the provided full path is a file and executable + return doxygen_bin_path.is_file() and os.access(doxygen_bin_path, os.X_OK) + + # Source of dox_dict2str: https://xdress-fabio.readthedocs.io/en/latest/_modules/xdress/doxygen.html#XDressPlugin + + @staticmethod + def str2dox_dict(dox_str: str, config_file: 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 = {} + dox_str = re.sub(r"\\\s*\n\s*", "", dox_str) + pattern = r"^\s*([^=\s]+)\s*(=|\+=)\s*(.*)$" + + try: + for line in dox_str.split("\n"): + if line.strip() == "" or line.startswith("#"): + continue + match = re.match(pattern, line) + if not match: + raise DoxygenCustomConfigNotValid( + f"Invalid line: '{line}'" + f"In custom Doxygen config file: {config_file}\n" + f"Make sure the file is in standard Doxygen format." + f"Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." + ) + key, operator, value = match.groups() + value = value.strip() + if operator == "=": + if value == "YES": + dox_dict[key] = True + elif value == "NO": + dox_dict[key] = False + else: + dox_dict[key] = value + if operator == "+=": + dox_dict[key] = f"{dox_dict[key]} {value}" + except ValueError as e: + raise DoxygenCustomConfigNotValid( + f"Invalid custom Doxygen config file: {config_file}\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 + + @staticmethod + def dox_dict2str(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. + """ + string = "" + new_line = "{option} = {value}\n" + items = sorted(dox_dict.items()) + for key, value in items: + if value is True: + value_transformed = "YES" + elif value is False: + value_transformed = "NO" + else: + value_transformed = value + + string += new_line.format(option=key.upper(), value=value_transformed) + + # Don't need an empty line at the end + return string.strip() + + @staticmethod + def hash_write(file_name: Path, hash_key: str) -> None: + """! Write the hash to the file. + @details + @param file_name: (Path) Path to the file where the hash will be saved. + @param hash_key: (str) Hash. + """ + with open(file_name, "w") as hash_file: + hash_file.write(hash_key) + + @staticmethod + def hash_read(file_name: Path) -> str: + """! Read the hash from the file. + @details + @param file_name: (Path) Path to the file with the hash. + @return: (str) Hash. + """ + with open(file_name) as hash_file: + return str(hash_file.read()) + + def has_changes(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. + """ + sha1 = hashlib.sha1() + sources = self.project_config.src_dirs.split(" ") + # Code from https://stackoverflow.com/a/22058673/15411117 + BUF_SIZE = 65536 # let's read stuff in 64kb chunks! + for source in sources: + for path in Path(source).rglob("*.*"): + if path.is_file(): + with open(path, "rb") as file: + while True: + data = file.read(BUF_SIZE) + if not data: + break + sha1.update(data) + + hash_new = sha1.hexdigest() + hash_file_name: Path = Path("mkdoxy_hash.txt") + hash_file_path = Path.joinpath(self.temp_doxy_folder, hash_file_name) + if hash_file_path.is_file(): + hash_old = self.hash_read(hash_file_path) + if hash_new == hash_old: + return False # No changes in the source files + + self.hash_write(hash_file_path, hash_new) + return True + + def run(self) -> None: + """! Run Doxygen with the current configuration using the Popen class. + @details + """ + doxy_builder = Popen( + [self.doxy_config.doxygen_bin_path, "-"], + stdout=PIPE, + stdin=PIPE, + stderr=PIPE, + ) + + if self.project_config.doxy_config_file_force: + doxy_str = self.get_doxy_config_file_raw() + else: + doxy_str = self.dox_dict2str(self.get_merged_doxy_dict()) + stdout_data, stderr_data = doxy_builder.communicate(input=doxy_str.encode("utf-8")) + if doxy_builder.returncode != 0: + error_message = ( + f"Error running Doxygen (exit code {doxy_builder.returncode}): {stderr_data.decode('utf-8')}" + ) + raise exceptions.PluginError(error_message) + + def get_output_xml_folder(self) -> Path: + """! Get the path to the XML output folder. + @details + @return: (Path) Path to the XML output folder. + """ + return Path.joinpath(self.temp_doxy_folder, Path("xml")) + + def get_output_html_folder(self) -> Path: + """! Get the path to the HTML output folder. + @details + @return: (Path) Path to the HTML output folder. + """ + return Path.joinpath(self.temp_doxy_folder, Path("html")) + + def get_doxygen_run_folder(self): + """! Get the working directory to execute Doxygen in. Important to resolve relative paths. + @details When a doxygen config file is provided, this is its containing directory. Otherwise, it's the current + working directory. + @return: (Path) Path to the folder to execute Doxygen in. + """ + if not self.project_config.doxy_config_file: + return Path.cwd() + + return Path(self.project_config.doxy_config_file).parent + + +# not valid path exception +class DoxygenBinPathNotValid(exceptions.PluginError): + pass + + +class DoxygenCustomConfigNotFound(exceptions.PluginError): + pass + + +class DoxygenCustomConfigNotValid(exceptions.PluginError): + pass diff --git a/mkdoxy/doxyrun.py b/mkdoxy/doxyrun.py deleted file mode 100644 index ffd1473a..00000000 --- a/mkdoxy/doxyrun.py +++ /dev/null @@ -1,275 +0,0 @@ -import hashlib -import logging -import os -import shutil -import re - -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: - - OUTPUT_DIRECTORY: - - 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_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.setDoxyCfg(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: - @details - OUTPUT_DIRECTORY: - - @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_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 = {} - dox_str = re.sub(r"\\\s*\n\s*", "", dox_str) - pattern = r"^\s*([^=\s]+)\s*(=|\+=)\s*(.*)$" - - try: - for line in dox_str.split("\n"): - if line.strip() == "" or line.startswith("#"): - continue - match = re.match(pattern, line) - if not match: - raise DoxygenCustomConfigNotValid( - f"Invalid line: '{line}'" - f"In 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/." - ) - key, operator, value = match.groups() - value = value.strip() - if operator == "=": - if value == "YES": - dox_dict[key] = True - elif value == "NO": - dox_dict[key] = False - else: - dox_dict[key] = value - if operator == "+=": - dox_dict[key] = f"{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_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.hasChanged(): - self.run() - 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 diff --git a/mkdoxy/finder.py b/mkdoxy/finder.py index 10621465..053d4643 100644 --- a/mkdoxy/finder.py +++ b/mkdoxy/finder.py @@ -1,76 +1,74 @@ -from typing import Dict - from mkdoxy.constants import Kind from mkdoxy.doxygen import Doxygen from mkdoxy.utils import recursive_find, recursive_find_with_parent class Finder: - def __init__(self, doxygen: Dict[str, Doxygen], debug: bool = False): + def __init__(self, doxygen: dict[str, Doxygen], debug: bool = False) -> None: self.doxygen = doxygen self.debug = debug def _normalize(self, name: str) -> str: return name.replace(" ", "") - def listToNames(self, list): + def list_to_names(self, list): return [part.name_params for part in list] - def _doxyParent(self, project, parent: str, kind: Kind): + def _doxy_parent(self, project, parent: str, kind: Kind): if not kind.is_parent(): return None parents = recursive_find(self.doxygen[project].root.children, kind) if parents: - for findParent in parents: - if findParent.name_long == parent: - return findParent - return self.listToNames(parents) + for find_parent in parents: + if find_parent.name_long == parent: + return find_parent + return self.list_to_names(parents) return None - def _doxyMemberInParent(self, project, parent: str, parentKind: Kind, memberName: str, memberKind: Kind): - findParent = self._doxyParent(project, parent, parentKind) - if findParent: - if isinstance(findParent, list): - for member in findParent: - if self._normalize(memberName) in self._normalize(member): + def _doxy_member_in_parent(self, project, parent: str, parent_kind: Kind, member_name: str, member_kind: Kind): + find_parent = self._doxy_parent(project, parent, parent_kind) + if find_parent: + if isinstance(find_parent, list): + for member in find_parent: + if self._normalize(member_name) in self._normalize(member): return member - return findParent + return find_parent else: - members = recursive_find(findParent.children, memberKind) + members = recursive_find(find_parent.children, member_kind) if members: for member in members: - if self._normalize(memberName) in self._normalize(member.name_params): + if self._normalize(member_name) in self._normalize(member.name_params): return member - return self.listToNames(members) + return self.list_to_names(members) return None return None - def doxyClass(self, project, className: str): - return self._doxyParent(project, className, Kind.CLASS) + def doxy_class(self, project, class_name: str): + return self._doxy_parent(project, class_name, Kind.CLASS) - def doxyNamespace(self, project, namespace: str): - return self._doxyParent(project, namespace, Kind.NAMESPACE) + def doxy_namespace(self, project, namespace: str): + return self._doxy_parent(project, namespace, Kind.NAMESPACE) - def doxyClassMethod(self, project, className: str, methodName: str): - return self._doxyMemberInParent(project, className, Kind.CLASS, methodName, Kind.FUNCTION) + def doxy_class_method(self, project, class_name: str, method_name: str): + return self._doxy_member_in_parent(project, class_name, Kind.CLASS, method_name, Kind.FUNCTION) - def doxyNamespaceFunction(self, project, namespace: str, functionName: str): - return self._doxyMemberInParent(project, namespace, Kind.NAMESPACE, functionName, Kind.FUNCTION) + def doxy_namespace_function(self, project, namespace: str, function_name: str): + return self._doxy_member_in_parent(project, namespace, Kind.NAMESPACE, function_name, Kind.FUNCTION) - def doxyFunction(self, project, functionName: str): + def doxy_function(self, project, function_name: str): functions = recursive_find_with_parent(self.doxygen[project].files.children, [Kind.FUNCTION], [Kind.FILE]) if functions: for function in functions: - if self._normalize(functionName) == self._normalize(function.name_params): + if self._normalize(function_name) == self._normalize(function.name_params): return function - return self.listToNames(functions) + return self.list_to_names(functions) return None - def doxyCode(self, project, fileName): + def doxy_code(self, project, file_name): files = recursive_find_with_parent(self.doxygen[project].files.children, [Kind.FILE], [Kind.DIR]) if files: for file in files: - if self._normalize(fileName) == self._normalize(file.name_long): + if self._normalize(file_name) == self._normalize(file.name_long): return file - return self.listToNames(files) + return self.list_to_names(files) return None diff --git a/mkdoxy/generatorAuto.py b/mkdoxy/generatorAuto.py index 8f5d77d6..91add8b6 100644 --- a/mkdoxy/generatorAuto.py +++ b/mkdoxy/generatorAuto.py @@ -1,5 +1,6 @@ import logging import os +from typing import Optional from mkdocs.structure import files @@ -41,40 +42,40 @@ def normalize(name): class GeneratorAuto: def __init__( self, - generatorBase: GeneratorBase, - tempDoxyDir: str, - siteDir: str, - apiPath: str, + generator_base: GeneratorBase, + temp_doxy_dir: str, + site_dir: str, + api_path: str, doxygen: Doxygen, - useDirectoryUrls: bool, - ): - self.generatorBase = generatorBase - self.tempDoxyDir = tempDoxyDir - self.siteDir = siteDir - self.apiPath = apiPath + use_directory_urls: bool, + ) -> None: + self.generator_base = generator_base + self.temp_doxy_dir = temp_doxy_dir + self.site_dir = site_dir + self.api_path = api_path self.doxygen = doxygen - self.useDirectoryUrls = useDirectoryUrls - self.fullDocFiles = [] - self.debug = generatorBase.debug - os.makedirs(os.path.join(self.tempDoxyDir, self.apiPath), exist_ok=True) - - def save(self, path: str, output: str): - pathRel = os.path.join(self.apiPath, path) - self.fullDocFiles.append(files.File(pathRel, self.tempDoxyDir, self.siteDir, self.useDirectoryUrls)) - with open(os.path.join(self.tempDoxyDir, pathRel), "w", encoding="utf-8") as file: + self.use_directory_urls = use_directory_urls + self.full_doc_files = [] + self.debug = generator_base.debug + os.makedirs(os.path.join(self.temp_doxy_dir, self.api_path), exist_ok=True) + + def save(self, path: str, output: str) -> None: + path_rel = os.path.join(self.api_path, path) + self.full_doc_files.append(files.File(path_rel, self.temp_doxy_dir, self.site_dir, self.use_directory_urls)) + with open(os.path.join(self.temp_doxy_dir, path_rel), "w", encoding="utf-8") as file: file.write(output) - def fullDoc(self, defaultTemplateConfig: dict): - self.annotated(self.doxygen.root.children, defaultTemplateConfig) - self.fileindex(self.doxygen.files.children, defaultTemplateConfig) - self.members(self.doxygen.root.children, defaultTemplateConfig) - self.members(self.doxygen.groups.children, defaultTemplateConfig) - self.files(self.doxygen.files.children, defaultTemplateConfig) - self.namespaces(self.doxygen.root.children, defaultTemplateConfig) - self.classes(self.doxygen.root.children, defaultTemplateConfig) - self.hierarchy(self.doxygen.root.children, defaultTemplateConfig) - self.modules(self.doxygen.groups.children, defaultTemplateConfig) - self.pages(self.doxygen.pages.children, defaultTemplateConfig) + def full_doc(self, default_template_config: dict) -> None: + self.annotated(self.doxygen.root.children, default_template_config) + self.fileindex(self.doxygen.files.children, default_template_config) + self.members(self.doxygen.root.children, default_template_config) + self.members(self.doxygen.groups.children, default_template_config) + self.files(self.doxygen.files.children, default_template_config) + self.namespaces(self.doxygen.root.children, default_template_config) + self.classes(self.doxygen.root.children, default_template_config) + self.hierarchy(self.doxygen.root.children, default_template_config) + self.modules(self.doxygen.groups.children, default_template_config) + self.pages(self.doxygen.pages.children, default_template_config) # self.examples(self.doxygen.examples.children) # TODO examples self.relatedpages(self.doxygen.pages.children) self.index( @@ -82,139 +83,139 @@ def fullDoc(self, defaultTemplateConfig: dict): [Kind.FUNCTION, Kind.VARIABLE, Kind.TYPEDEF, Kind.ENUM], [Kind.CLASS, Kind.STRUCT, Kind.INTERFACE], "Class Members", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.FUNCTION], [Kind.CLASS, Kind.STRUCT, Kind.INTERFACE], "Class Member Functions", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.VARIABLE], [Kind.CLASS, Kind.STRUCT, Kind.INTERFACE], "Class Member Variables", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.TYPEDEF], [Kind.CLASS, Kind.STRUCT, Kind.INTERFACE], "Class Member Typedefs", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.ENUM], [Kind.CLASS, Kind.STRUCT, Kind.INTERFACE], "Class Member Enums", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.FUNCTION, Kind.VARIABLE, Kind.TYPEDEF, Kind.ENUM], [Kind.NAMESPACE], "Namespace Members", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.FUNCTION], [Kind.NAMESPACE], "Namespace Member Functions", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.VARIABLE], [Kind.NAMESPACE], "Namespace Member Variables", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.TYPEDEF], [Kind.NAMESPACE], "Namespace Member Typedefs", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.root.children, [Kind.ENUM], [Kind.NAMESPACE], "Namespace Member Enums", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.files.children, [Kind.FUNCTION], [Kind.FILE], "Functions", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.files.children, [Kind.DEFINE], [Kind.FILE], "Macros", - defaultTemplateConfig, + default_template_config, ) self.index( self.doxygen.files.children, [Kind.VARIABLE, Kind.UNION, Kind.TYPEDEF, Kind.ENUM], [Kind.FILE], "Variables", - defaultTemplateConfig, + default_template_config, ) - def annotated(self, nodes: [Node], config: dict = None): + def annotated(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "annotated.md" - output = self.generatorBase.annotated(nodes, config) + output = self.generator_base.annotated(nodes, config) self.save(path, output) - def programlisting(self, node: [Node], config: dict = None): + def programlisting(self, node: [Node], config: Optional[dict] = None) -> None: path = f"{node.refid}_source.md" - output = self.generatorBase.programlisting(node, config) + output = self.generator_base.programlisting(node, config) self.save(path, output) - def fileindex(self, nodes: [Node], config: dict = None): + def fileindex(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "files.md" - output = self.generatorBase.fileindex(nodes, config) + output = self.generator_base.fileindex(nodes, config) self.save(path, output) - def namespaces(self, nodes: [Node], config: dict = None): + def namespaces(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "namespaces.md" - output = self.generatorBase.namespaces(nodes, config) + output = self.generator_base.namespaces(nodes, config) self.save(path, output) - def page(self, node: Node, config: dict = None): + def page(self, node: Node, config: Optional[dict] = None) -> None: path = f"{node.name}.md" - output = self.generatorBase.page(node, config) + output = self.generator_base.page(node, config) self.save(path, output) - def pages(self, nodes: [Node], config: dict = None): + def pages(self, nodes: [Node], config: Optional[dict] = None) -> None: for node in nodes: self.page(node, config) - def relatedpages(self, nodes: [Node], config: dict = None): + def relatedpages(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "pages.md" - output = self.generatorBase.relatedpages(nodes) + output = self.generator_base.relatedpages(nodes) self.save(path, output) - def example(self, node: Node, config: dict = None): + def example(self, node: Node, config: Optional[dict] = None) -> None: path = f"{node.refid}.md" - output = self.generatorBase.example(node, config) + output = self.generator_base.example(node, config) self.save(path, output) - def examples(self, nodes: [Node], config: dict = None): + def examples(self, nodes: [Node], config: Optional[dict] = None) -> None: for node in nodes: if node.is_example: if node.has_programlisting: @@ -223,40 +224,40 @@ def examples(self, nodes: [Node], config: dict = None): path = "examples.md" - output = self.generatorBase.examples(nodes, config) + output = self.generator_base.examples(nodes, config) self.save(path, output) - def classes(self, nodes: [Node], config: dict = None): + def classes(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "classes.md" - output = self.generatorBase.classes(nodes, config) + output = self.generator_base.classes(nodes, config) self.save(path, output) - def modules(self, nodes: [Node], config: dict = None): + def modules(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "modules.md" - output = self.generatorBase.modules(nodes, config) + output = self.generator_base.modules(nodes, config) self.save(path, output) - def hierarchy(self, nodes: [Node], config: dict = None): + def hierarchy(self, nodes: [Node], config: Optional[dict] = None) -> None: path = "hierarchy.md" - output = self.generatorBase.hierarchy(nodes, config) + output = self.generator_base.hierarchy(nodes, config) self.save(path, output) - def member(self, node: Node, config: dict = None): + def member(self, node: Node, config: Optional[dict] = None) -> None: path = node.filename - output = self.generatorBase.member(node, config) + output = self.generator_base.member(node, config) self.save(path, output) if node.is_language or node.is_group or node.is_file or node.is_dir: self.members(node.children, config) - def file(self, node: Node, config: dict = None): + def file(self, node: Node, config: Optional[dict] = None) -> None: path = node.filename - output = self.generatorBase.file(node, config) + output = self.generator_base.file(node, config) self.save(path, output) if node.is_file and node.has_programlisting: @@ -265,12 +266,12 @@ def file(self, node: Node, config: dict = None): if node.is_file or node.is_dir: self.files(node.children, config) - def members(self, nodes: [Node], config: dict = None): + def members(self, nodes: [Node], config: Optional[dict] = None) -> None: for node in nodes: if node.is_parent or node.is_group or node.is_file or node.is_dir: self.member(node, config) - def files(self, nodes: [Node], config: dict = None): + def files(self, nodes: [Node], config: Optional[dict] = None) -> None: for node in nodes: if node.is_file or node.is_dir: self.file(node, config) @@ -281,20 +282,22 @@ def index( kind_filters: Kind, kind_parents: [Kind], title: str, - config: dict = None, - ): + config: Optional[dict] = None, + ) -> None: path = title.lower().replace(" ", "_") + ".md" - output = self.generatorBase.index(nodes, kind_filters, kind_parents, title, config) + output = self.generator_base.index(nodes, kind_filters, kind_parents, title, config) self.save(path, output) - def _generate_recursive(self, output_summary: str, node: Node, level: int): + def _generate_recursive(self, output_summary: str, node: Node, level: int) -> None: if node.kind.is_parent(): output_summary += str(" " * level + generate_link(f"{node.kind.value} {node.name}", f"{node.refid}.md")) for child in node.children: self._generate_recursive(output_summary, child, level + 2) - def _generate_recursive_files(self, output_summary: str, node: Node, level: int, config: dict = None): + def _generate_recursive_files( + self, output_summary: str, node: Node, level: int, config: Optional[dict] = None + ) -> None: if config is None: config = [] if node.kind.is_file() or node.kind.is_dir(): @@ -308,25 +311,25 @@ def _generate_recursive_files(self, output_summary: str, node: Node, level: int, for child in node.children: self._generate_recursive_files(output_summary, child, level + 2, config) - def _generate_recursive_examples(self, output_summary: str, node: Node, level: int): + def _generate_recursive_examples(self, output_summary: str, node: Node, level: int) -> None: if node.kind.is_example(): output_summary += str(" " * level + generate_link(node.name, f"{node.refid}.md")) for child in node.children: self._generate_recursive_examples(output_summary, child, level + 2) - def _generate_recursive_groups(self, output_summary: str, node: Node, level: int): + def _generate_recursive_groups(self, output_summary: str, node: Node, level: int) -> None: if node.kind.is_group(): output_summary += str(" " * level + generate_link(node.title, f"{node.refid}.md")) for child in node.children: self._generate_recursive_groups(output_summary, child, level + 2) - def _generate_recursive_pages(self, output_summary: str, node: Node, level: int): + def _generate_recursive_pages(self, output_summary: str, node: Node, level: int) -> None: if node.kind.is_page(): output_summary += str(" " * level + generate_link(node.title, f"{node.refid}.md")) for child in node.children: self._generate_recursive_pages(output_summary, child, level + 2) - def summary(self, defaultTemplateConfig: dict): + def summary(self, default_template_config: dict) -> None: offset = 0 output_summary = "" + str(" " * (offset + 2) + generate_link("Related Pages", "pages.md")) for node in self.doxygen.pages.children: @@ -345,7 +348,7 @@ def summary(self, defaultTemplateConfig: dict): output_summary += str(" " * (offset + 2) + generate_link("Files", "files.md", end="\n")) for node in self.doxygen.files.children: - self._generate_recursive_files(output_summary, node, offset + 4, defaultTemplateConfig) + self._generate_recursive_files(output_summary, node, offset + 4, default_template_config) # output_summary += str(' ' * (offset + 2) + generate_link('Examples', 'examples.md')) # for node in self.doxygen.examples.children: diff --git a/mkdoxy/generatorBase.py b/mkdoxy/generatorBase.py index 6e2f6905..fc7c3d3e 100644 --- a/mkdoxy/generatorBase.py +++ b/mkdoxy/generatorBase.py @@ -1,19 +1,19 @@ import logging import os import string -from typing import Dict +from typing import Optional from jinja2 import BaseLoader, Environment, Template from jinja2.exceptions import TemplateError from mkdocs import exceptions import mkdoxy -from mkdoxy.constants import Kind +from mkdoxy.constants import JINJA_EXTENSIONS, Kind from mkdoxy.filters import use_code_language from mkdoxy.node import DummyNode, Node from mkdoxy.utils import ( merge_two_dicts, - parseTemplateFile, + parse_template_file, recursive_find, recursive_find_with_parent, ) @@ -21,91 +21,92 @@ log: logging.Logger = logging.getLogger("mkdocs") -LETTERS = string.ascii_lowercase + "~_@\\" +LETTERS = string.ascii_lowercase + "~_@\\\\" class GeneratorBase: - """! Base class for all generators.""" + \"\"\"! Base class for all generators.\"\"\" - def __init__(self, templateDir: str = "", ignore_errors: bool = False, debug: bool = False): - """! Constructor. + def __init__(self, template_dir: str = \"\", ignore_errors: bool = False, debug: bool = False) -> None: + \"\"\"! Constructor. @details - @param templateDir (str): Path to the directory with custom templates (default: "") + @param template_dir (str): Path to the directory with custom templates (default: \"\") @param ignore_errors (bool): If True, errors will be ignored (default: False) @param debug (bool): If True, debug messages will be printed (default: False) - """ + \"\"\" self.debug: bool = debug # if True, debug messages will be printed - self.templates: Dict[str, Template] = {} - self.metaData: Dict[str, list[str]] = {} + self.templates: dict[str, Template] = {} + self.meta_data: dict[str, list[str]] = {} environment = Environment(loader=BaseLoader()) - environment.filters["use_code_language"] = use_code_language + environment.filters[\"use_code_language\"] = use_code_language # code from https://github.com/daizutabi/mkapi/blob/master/mkapi/core/renderer.py#L29-L38 - path = os.path.join(os.path.dirname(mkdoxy.__file__), "templates") - ENDING = (".jinja2", ".j2", ".jinja") - for fileName in os.listdir(path): - filePath = os.path.join(path, fileName) + path = os.path.join(os.path.dirname(mkdoxy.__file__), \"templates\") + for file_name in os.listdir(path): + file_path = os.path.join(path, file_name) # accept any case of the file ending - if fileName.lower().endswith(ENDING): - with open(filePath, "r") as file: - name = os.path.splitext(fileName)[0] - fileTemplate, metaData = parseTemplateFile(file.read()) - self.templates[name] = environment.from_string(fileTemplate) - self.metaData[name] = metaData + if file_name.lower().endswith(JINJA_EXTENSIONS): + with open(file_path) as file: + name = os.path.splitext(file_name)[0] + file_template, meta_data = parse_template_file(file.read()) + self.templates[name] = environment.from_string(file_template) + self.meta_data[name] = meta_data else: log.error( - f"Trying to load unsupported file '{filePath}'. Supported file ends with {ENDING}." - f"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates." + \"Trying to load unsupported file '%s'. Supported file ends with %s.\" + \"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates.\", + file_path, JINJA_EXTENSIONS ) - # test if templateDir is existing - if templateDir: - if not os.path.exists(templateDir): - raise exceptions.ConfigurationError(f"Custom template directory '{templateDir}' does not exist.") + # test if template_dir is existing + if template_dir: + if not os.path.exists(template_dir): + raise exceptions.ConfigurationError(f\"Custom template directory '{template_dir}' does not exist.\") # load custom templates and overwrite default templates - if they exist - for fileName in os.listdir(templateDir): - filePath = os.path.join(templateDir, fileName) - if fileName.lower().endswith(ENDING): - with open(filePath, "r") as file: - name = os.path.splitext(fileName)[0] - fileTemplate, metaData = parseTemplateFile(file.read()) - self.templates[name] = environment.from_string(fileTemplate) - self.metaData[name] = metaData - log.info(f"Overwriting template '{name}' with custom template.") + for file_name in os.listdir(template_dir): + file_path = os.path.join(template_dir, file_name) + if file_name.lower().endswith(JINJA_EXTENSIONS): + with open(file_path) as file: + name = os.path.splitext(file_name)[0] + file_template, meta_data = parse_template_file(file.read()) + self.templates[name] = environment.from_string(file_template) + self.meta_data[name] = meta_data + log.info(\"Overwriting template '%s' with custom template.\", name) else: log.error( - f"Trying to load unsupported file '{filePath}'. Supported file ends with {ENDING}." - f"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates." + \"Trying to load unsupported file '%s'. Supported file ends with %s.\" + \"Look at documentation: https://mkdoxy.kubaandrysek.cz/usage/#custom-jinja-templates.\", + file_path, JINJA_EXTENSIONS ) @staticmethod - def shift_each_line(value: str, shift_char: str = "\t") -> str: - """! Shift each line of a given string for a given character. + def shift_each_line(value: str, shift_char: str = \"\t\") -> str: + \"\"\"! Shift each line of a given string for a given character. @details It is used to shift the content for Markdown code blocks or other content that should be shifted. @param value (str): String to shift. @param shift_char (str): Character to shift the string (default: '\t'). @return (str): Shifted string. - """ - return "\n".join(shift_char + line for line in value.split("\n")) + \"\"\" + return \"\n\".join(shift_char + line for line in value.split(\"\\n\")) - def loadConfigAndTemplate(self, name: str) -> [Template, dict]: + def load_config_and_template(self, name: str) -> [Template, dict]: template = self.templates.get(name) if not template: raise exceptions.Abort( - f"Trying to load unexciting template '{name}'. Please create a new template file with name '{name}.jinja2'" # noqa: E501 + f\"Trying to load unexciting template '{name}'. Please create a new template file with name '{name}.jinja2'\" # noqa: E501 ) - metaData = self.metaData.get(name, {}) - return template, metaData + meta_data = self.meta_data.get(name, {}) + return template, meta_data def render(self, tmpl: Template, data: dict) -> str: - """! Render a template with given data. + \"\"\"! Render a template with given data. @details @param tmpl (Template): Template to render. @param data (dict): Data to render the template. @return (str): Rendered template. - """ + \"\"\" try: # if self.debug: # print('Generating', path) # TODO: add path to data @@ -119,195 +120,195 @@ def error( config: dict, title: str, description: str, - code_header: str = "", - code: str = "", - code_language: str = "", - snippet_code: str = "", + code_header: str = \"\", + code: str = \"\", + code_language: str = \"\", + snippet_code: str = \"\", ): - """! Render an error page. + \"\"\"! Render an error page. @details @param config (dict): Config for the template. @param title (str): Title of the error. @param description (str): Description of the error. - @param code_header (str): Header of the code (default: "") - @param code (str): Code (default: "") - @param code_language (str): Language of the code (default: "") - @param snippet_code (str): Snippet code (default: "") - """ + @param code_header (str): Header of the code (default: \"\") + @param code (str): Code (default: \"\") + @param code_language (str): Language of the code (default: \"\") + @param snippet_code (str): Snippet code (default: \"\") + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("error") + template, meta_config = self.load_config_and_template(\"error\") data = { - "title": title, - "description": description, - "code": code, - "code_header": code_header, - "code_language": code_language, - "snippet_code": snippet_code, - "config": merge_two_dicts(config, metaConfig), + \"title\": title, + \"description\": description, + \"code\": code, + \"code_header\": code_header, + \"code_language\": code_language, + \"snippet_code\": snippet_code, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def annotated(self, nodes: [Node], config: dict = None): - """! Render an annotated page. + def annotated(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render an annotated page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template (default: None) @return (str): Rendered annotated page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("annotated") + template, meta_config = self.load_config_and_template(\"annotated\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) def examples(self, nodes: [Node], config=None): - """! Render an examples page. + \"\"\"! Render an examples page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template (default: None) @return (str): Rendered examples page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("examples") + template, meta_config = self.load_config_and_template(\"examples\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def programlisting(self, node: [Node], config: dict = None): - """! Render a programlisting page. + def programlisting(self, node: [Node], config: Optional[dict] = None): + \"\"\"! Render a programlisting page. @details @param node ([Node]): Node to render. @param config (dict): Config for the template (default: None) @return (str): Rendered programlisting page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("programlisting") + template, meta_config = self.load_config_and_template(\"programlisting\") data = { - "node": node, - "config": merge_two_dicts(config, metaConfig), + \"node\": node, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def code(self, node: [Node], config: dict = None, code: str = ""): - """! Render a code page. + def code(self, node: [Node], config: Optional[dict] = None, code: str = \"\"): + \"\"\"! Render a code page. @details @param node ([Node]): Node to render. @param config (dict): Config for the template (default: None) - @param code (str): Code to render (default: "") + @param code (str): Code to render (default: \"\") @return (str): Rendered code page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("code") + template, meta_config = self.load_config_and_template(\"code\") # newConfig = merge_two_dicts(CODE_CONFIG, config) data = { - "node": node, - "config": merge_two_dicts(config, metaConfig), - "code": code, + \"node\": node, + \"config\": merge_two_dicts(config, meta_config), + \"code\": code, } return self.render(template, data) - def fileindex(self, nodes: [Node], config: dict = None): - """! Render a fileindex page. + def fileindex(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a fileindex page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template (default: None) @return (str): Rendered fileindex page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("files") + template, meta_config = self.load_config_and_template(\"files\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def namespaces(self, nodes: [Node], config: dict = None): - """! Render a namespaces page. + def namespaces(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a namespaces page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered namespaces page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("namespaces") + template, meta_config = self.load_config_and_template(\"namespaces\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def page(self, node: Node, config: dict = None): - """! Render a page. + def page(self, node: Node, config: Optional[dict] = None): + \"\"\"! Render a page. @details @param node (Node): Node to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("page") + template, meta_config = self.load_config_and_template(\"page\") data = { - "node": node, - "config": merge_two_dicts(config, metaConfig), + \"node\": node, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def example(self, node: Node, config: dict = None): - """! Render an example page. + def example(self, node: Node, config: Optional[dict] = None): + \"\"\"! Render an example page. @details @param node (Node): Node to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered example page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("example") + template, meta_config = self.load_config_and_template(\"example\") data = { - "node": node, - "config": merge_two_dicts(config, metaConfig), + \"node\": node, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def relatedpages(self, nodes: [Node], config: dict = None): - """! Render a related pages page. + def relatedpages(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a related pages page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered related pages page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("relatedPages") + template, meta_config = self.load_config_and_template(\"relatedPages\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def classes(self, nodes: [Node], config: dict = None): - """! Render a classes page. + def classes(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a classes page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered classes page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("classes") + template, meta_config = self.load_config_and_template(\"classes\") classes = recursive_find(nodes, Kind.CLASS) classes.extend(recursive_find(nodes, Kind.STRUCT)) @@ -323,22 +324,22 @@ def classes(self, nodes: [Node], config: dict = None): del dictionary[letter] data = { - "dictionary": dictionary, - "config": merge_two_dicts(config, metaConfig), + \"dictionary\": dictionary, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) def _find_base_classes(self, nodes: [Node], derived: Node): - """! Find base classes of a node. + \"\"\"! Find base classes of a node. @details @param nodes ([Node]): List of nodes to search. @param derived (Node): Derived node. @return ([Node]): List of base classes. - """ + \"\"\" ret = [] for node in nodes: if isinstance(node, str): - ret.append({"refid": node, "derived": derived}) + ret.append({\"refid\": node, \"derived\": derived}) elif node.kind.is_parent() and not node.kind.is_namespace(): bases = node.base_classes if len(bases) == 0: @@ -347,32 +348,32 @@ def _find_base_classes(self, nodes: [Node], derived: Node): ret.extend(self._find_base_classes(bases, node)) return ret - def modules(self, nodes: [Node], config: dict = None): - """! Render a modules page. + def modules(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a modules page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered modules page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("modules") + template, meta_config = self.load_config_and_template(\"modules\") data = { - "nodes": nodes, - "config": merge_two_dicts(config, metaConfig), + \"nodes\": nodes, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def hierarchy(self, nodes: [Node], config: dict = None): - """! Render a hierarchy page. + def hierarchy(self, nodes: [Node], config: Optional[dict] = None): + \"\"\"! Render a hierarchy page. @details @param nodes ([Node]): List of nodes to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered hierarchy page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("hierarchy") + template, meta_config = self.load_config_and_template(\"hierarchy\") classes = recursive_find(nodes, Kind.CLASS) classes.extend(recursive_find(nodes, Kind.STRUCT)) @@ -383,92 +384,92 @@ def hierarchy(self, nodes: [Node], config: dict = None): for base in bases: if isinstance(base, dict): - if base["refid"] not in deduplicated: - deduplicated[base["refid"]] = [] - deduplicated[base["refid"]].append(base) + if base[\"refid\"] not in deduplicated: + deduplicated[base[\"refid\"]] = [] + deduplicated[base[\"refid\"]].append(base) deduplicated_arr = [] for key, children in deduplicated.items(): if isinstance(children, list): - deduplicated_arr.append(DummyNode(key, list(map(lambda x: x["derived"], children)), Kind.CLASS)) + deduplicated_arr.append(DummyNode(key, [x[\"derived\"] for x in children], Kind.CLASS)) else: found: Node = next((klass for klass in classes if klass.refid == key), None) if found: deduplicated_arr.append(found) data = { - "classes": deduplicated_arr, - "config": merge_two_dicts(config, metaConfig), + \"classes\": deduplicated_arr, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def function(self, node: Node, config: dict = None): - """! Render a function page. + def function(self, node: Node, config: Optional[dict] = None): + \"\"\"! Render a function page. @details @param node (Node): Node to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered function page. - """ + \"\"\" if config is None: config = {} - templateMemDef, metaConfigMemDef = self.loadConfigAndTemplate("memDef") - templateCode, metaConfigCode = self.loadConfigAndTemplate("code") + template_mem_def, meta_config_mem_def = self.load_config_and_template(\"memDef\") + template_code, meta_config_code = self.load_config_and_template(\"code\") data = { - "node": node, - "configMemDef": merge_two_dicts(config, metaConfigMemDef), - "templateCode": templateCode, - "configCode": metaConfigCode, - "config": merge_two_dicts(config, metaConfigMemDef), + \"node\": node, + \"config_mem_def\": merge_two_dicts(config, meta_config_mem_def), + \"template_code\": template_code, + \"config_code\": meta_config_code, + \"config\": merge_two_dicts(config, meta_config_mem_def), } - return self.render(templateMemDef, data) + return self.render(template_mem_def, data) - def member(self, node: Node, config: dict = None): - """! Render a member page. + def member(self, node: Node, config: Optional[dict] = None): + \"\"\"! Render a member page. @details @param node (Node): Node to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered member page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("member") - templateMemDef, metaConfigMemDef = self.loadConfigAndTemplate("memDef") - templateMemTab, metaConfigMemTab = self.loadConfigAndTemplate("memTab") - templateCode, metaConfigCode = self.loadConfigAndTemplate("code") + template, meta_config = self.load_config_and_template(\"member\") + template_mem_def, meta_config_mem_def = self.load_config_and_template(\"memDef\") + template_mem_tab, meta_config_mem_tab = self.load_config_and_template(\"memTab\") + template_code, meta_config_code = self.load_config_and_template(\"code\") data = { - "node": node, - "templateMemDef": templateMemDef, - "configMemDef": metaConfigMemDef, - "templateMemTab": templateMemTab, - "configMemTab": metaConfigMemTab, - "templateCode": templateCode, - "configCode": metaConfigCode, - "config": merge_two_dicts(config, metaConfig), + \"node\": node, + \"template_mem_def\": template_mem_def, + \"config_mem_def\": meta_config_mem_def, + \"template_mem_tab\": template_mem_tab, + \"config_mem_tab\": meta_config_mem_tab, + \"template_code\": template_code, + \"config_code\": meta_config_code, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) - def file(self, node: Node, config: dict = None): - """! Render a file page. + def file(self, node: Node, config: Optional[dict] = None): + \"\"\"! Render a file page. @details @param node (Node): Node to render. @param config (dict): Config for the template. (default: None) @return (str): Rendered file page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("member") - templateMemDef, metaConfigMemDef = self.loadConfigAndTemplate("memDef") - templateMemTab, metaConfigMemTab = self.loadConfigAndTemplate("memTab") + template, meta_config = self.load_config_and_template(\"member\") + template_mem_def, meta_config_mem_def = self.load_config_and_template(\"memDef\") + template_mem_tab, meta_config_mem_tab = self.load_config_and_template(\"memTab\") data = { - "node": node, - "templateMemDef": templateMemDef, - "configMemDef": metaConfigMemDef, - "templateMemTab": templateMemTab, - "configMemTab": metaConfigMemTab, - "config": merge_two_dicts(config, metaConfig), + \"node\": node, + \"template_mem_def\": template_mem_def, + \"config_mem_def\": meta_config_mem_def, + \"template_mem_tab\": template_mem_tab, + \"config_mem_tab\": meta_config_mem_tab, + \"config\": merge_two_dicts(config, meta_config), } return self.render(template, data) @@ -478,9 +479,9 @@ def index( kind_filters: Kind, kind_parents: [Kind], title: str, - config: dict = None, + config: Optional[dict] = None, ): - """! Render an index page. + \"\"\"! Render an index page. @details @param nodes ([Node]): List of nodes to render. @param kind_filters (Kind): Kind of nodes to render. @@ -488,10 +489,10 @@ def index( @param title (str): Title of the index page. @param config (dict): Config for the template. (default: None) @return (str): Rendered index page. - """ + \"\"\" if config is None: config = {} - template, metaConfig = self.loadConfigAndTemplate("index") + template, meta_config = self.load_config_and_template(\"index\") found_nodes = recursive_find_with_parent(nodes, kind_filters, kind_parents) dictionary = {letter: [] for letter in LETTERS} @@ -522,8 +523,8 @@ def index( sorted_dictionary[letter] = d data = { - "title": title, - "dictionary": sorted_dictionary, - "config": merge_two_dicts(config, metaConfig), + \"title\": title, + \"dictionary\": sorted_dictionary, + \"config\": merge_two_dicts(config, meta_config), } - return self.render(template, data) + return self.render(template, data) \ No newline at end of file diff --git a/mkdoxy/generatorSnippets.py b/mkdoxy/generatorSnippets.py index 4c8c18fb..c037de8e 100644 --- a/mkdoxy/generatorSnippets.py +++ b/mkdoxy/generatorSnippets.py @@ -1,6 +1,7 @@ import logging import pathlib import re +from typing import Optional, Union import yaml from mkdocs.structure import pages @@ -12,56 +13,56 @@ log: logging.Logger = logging.getLogger("mkdocs") -regexIncorrect = r"(?s)(?[a-zA-Z0-9_]+))?[\.]?[\s]*\n(?P.*?)\s*\n(?:(?=\n)|(?=:::)|\Z)" # https://regex101.com/r/IYl25b/2 # noqa: E501 -regexLong = r"(?s)(?[a-zA-Z0-9_]+)\.(?P[a-zA-Z0-9_.]+))\s*\n(?P.*?)(?:(?:(?:\r*\n)(?=\n))|(?=:::)|`|\Z)" # https://regex101.com/r/lIgOij/4 # noqa: E501 -regexShort = r"(?s)(?[a-zA-Z0-9_]+)\.(?P[a-zA-Z0-9_.]+))\s*\n(?:(?=\n)|(?=:::)|\Z)" # https://regex101.com/r/QnqxRc/2 # noqa: E501 +regex_incorrect = r"(?s)(?[a-zA-Z0-9_]+))?[\.]?[\s]*\n(?P.*?)\s*\n(?:(?=\n)|(?=:::)|\Z)" # https://regex101.com/r/IYl25b/2 # noqa: E501 +regex_long = r"(?s)(?[a-zA-Z0-9_]+)\.(?P[a-zA-Z0-9_.]+))\s*\n(?P.*?)(?:(?:(?:\r*\n)(?=\n))|(?=:::)|`|\Z)" # https://regex101.com/r/lIgOij/4 # noqa: E501 +regex_short = r"(?s)(?[a-zA-Z0-9_]+)\.(?P[a-zA-Z0-9_.]+))\s*\n(?:(?=\n)|(?=:::)|\Z)" # https://regex101.com/r/QnqxRc/2 # noqa: E501 class GeneratorSnippets: def __init__( self, markdown: str, - generatorBase: dict[str, GeneratorBase], + generator_base: dict[str, GeneratorBase], doxygen: dict[str, Doxygen], projects: dict[str, dict[str, any]], - useDirectoryUrls: bool, + use_directory_urls: bool, page: pages.Page, config: dict, debug: bool = False, - ): + ) -> None: self.markdown = markdown - self.generatorBase = generatorBase + self.generator_base = generator_base self.doxygen = doxygen self.projects = projects - self.useDirectoryUrls = useDirectoryUrls + self.use_directory_urls = use_directory_urls self.page = page self.config = config self.debug = debug self.finder = Finder(doxygen, debug) self.doxy_arguments = { - "code": self.doxyCode, - "function": self.doxyFunction, - "namespace.function": self.doxyNamespaceFunction, - "class": self.doxyClass, - "class.method": self.doxyClassMethod, - "class.list": self.doxyClassList, - "class.index": self.doxyClassIndex, - "class.hierarchy": self.doxyClassHierarchy, - "namespace.list": self.doxyNamespaceList, - "file.list": self.doxyFileList, + "code": self.doxy_code, + "function": self.doxy_function, + "namespace.function": self.doxy_namespace_function, + "class": self.doxy_class, + "class.method": self.doxy_class_method, + "class.list": self.doxy_class_list, + "class.index": self.doxy_class_index, + "class.hierarchy": self.doxy_class_hierarchy, + "namespace.list": self.doxy_namespace_list, + "file.list": self.doxy_file_list, } # fix absolute path path = pathlib.PurePath(self.page.url).parts - self.pageUrlPrefix = "".join("../" for _ in range(len(path) - 1)) + self.page_url_prefix = "".join("../" for _ in range(len(path) - 1)) def generate(self): if self.is_doxy_inactive(self.config): return self.markdown # doxygen is inactive return unchanged markdown try: - matches = re.finditer(regexIncorrect, self.markdown, re.MULTILINE) + matches = re.finditer(regex_incorrect, self.markdown, re.MULTILINE) for match in reversed(list(matches)): snippet = match.group() project_name = match.group("project") or "" @@ -79,7 +80,7 @@ def generate(self): ) self.replace_markdown(match.start(), match.end(), replacement) - matches = re.finditer(regexShort, self.markdown, re.MULTILINE) + matches = re.finditer(regex_short, self.markdown, re.MULTILINE) for match in reversed(list(matches)): snippet = match.group() argument = match.group("argument").lower() @@ -91,10 +92,10 @@ def generate(self): if self.is_doxy_inactive(snippet_config): continue - replaceStr = self.call_doxy_by_name(snippet, project_name, argument, snippet_config) - self.replace_markdown(match.start(), match.end(), replaceStr) + replace_str = self.call_doxy_by_name(snippet, project_name, argument, snippet_config) + self.replace_markdown(match.start(), match.end(), replace_str) - matches = re.finditer(regexLong, self.markdown, re.MULTILINE) + matches = re.finditer(regex_long, self.markdown, re.MULTILINE) for match in reversed(list(matches)): snippet = match.group() argument = match.group("argument").lower() @@ -105,8 +106,8 @@ def generate(self): snippet_config = self.config.copy() snippet_config.update(self.try_load_yaml(match.group("yaml"), project_name, snippet, self.config)) - replaceStr = self.call_doxy_by_name(snippet, project_name, argument, snippet_config) - self.replace_markdown(match.start(), match.end(), replaceStr) + replace_str = self.call_doxy_by_name(snippet, project_name, argument, snippet_config) + self.replace_markdown(match.start(), match.end(), replace_str) return self.markdown except Exception as e: basename = pathlib.Path(__file__).name @@ -119,7 +120,7 @@ def try_load_yaml(self, yaml_raw: str, project: str, snippet: str, config: dict) return yaml.safe_load(yaml_raw) except yaml.YAMLError: log.error(f"YAML error in {project} project on page {self.page.url}") - self.doxyError( + self.doxy_error( project, config, "YAML error", @@ -137,7 +138,7 @@ def incorrect_project( config: dict, snippet: str, ) -> str: - return self.doxyError( + return self.doxy_error( project, config, f"Incorrect project name: {project}", @@ -149,7 +150,7 @@ def incorrect_project( ) def incorrect_argument(self, project: str, argument: str, config: dict, snippet: str) -> str: - return self.doxyError( + return self.doxy_error( project, config, f"Incorrect argument: {argument}" if argument else f"Add argument to snippet: {project}", @@ -160,15 +161,15 @@ def incorrect_argument(self, project: str, argument: str, config: dict, snippet: snippet, ) - def replace_markdown(self, start: int, end: int, replacement: str): + def replace_markdown(self, start: int, end: int, replacement: str) -> None: self.markdown = self.markdown[:start] + replacement + "\n" + self.markdown[end:] - def _setLinkPrefixNode(self, node: Node, linkPrefix: str): - node.project.linkPrefix = linkPrefix + def _set_link_prefix_node(self, node: Node, link_prefix: str) -> None: + node.project.link_prefix = link_prefix - def _setLinkPrefixNodes(self, nodes: list[Node], linkPrefix: str): + def _set_link_prefix_nodes(self, nodes: list[Node], link_prefix: str) -> None: if nodes: - nodes[0].project.linkPrefix = linkPrefix + nodes[0].project.link_prefix = link_prefix def is_project_exist(self, project: str): return project in self.projects @@ -182,14 +183,14 @@ def call_doxy_by_name(self, snippet, project: str, argument: str, config: dict) callback = self.doxy_arguments[argument] return callback(snippet, project, config) - def checkConfig(self, snippet, project: str, config, required_params: [str]) -> bool: + def check_config(self, snippet, project: str, config, required_params: [str]) -> bool: """ returns false if config is correct return error message if project not exist or find problem in config """ return next( ( - self.doxyError( + self.doxy_error( project, config, f"Missing parameter: {param}", @@ -207,7 +208,7 @@ def checkConfig(self, snippet, project: str, config, required_params: [str]) -> ### Create documentation generator callbacks - def doxyError( + def doxy_error( self, project, config: dict, @@ -220,37 +221,37 @@ def doxyError( ) -> str: log.error(f" -> {title} -> page: {self.page.canonical_url}") if project not in self.projects: - project = list(self.projects)[0] - return self.generatorBase[project].error( + project = next(iter(self.projects)) + return self.generator_base[project].error( config, title, description, code_header, code, code_language, snippet_code ) - def doxyCode(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, ["file"]) - if errorMsg: - return errorMsg - node = self.finder.doxyCode(project, config.get("file")) + def doxy_code(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, ["file"]) + if error_msg: + return error_msg + node = self.finder.doxy_code(project, config.get("file")) if node is None: - return self.doxyNodeIsNone(project, config, snippet) + return self.doxy_node_is_none(project, config, snippet) if isinstance(node, Node): - progCode = self.codeStrip( + prog_code = self.code_strip( node.programlisting, node.code_language, config.get("start", 1), config.get("end", 0), ) - if progCode is False: - return self.doxyError( + if prog_code is False: + return self.doxy_error( project, config, f"Parameter start: {config.get('start')} is greater than end: {config.get('end')}", f"{snippet}", "yaml", ) - self._setLinkPrefixNode(node, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].code(node, config, progCode) - return self.doxyError( + self._set_link_prefix_node(node, self.page_url_prefix + project + "/") + return self.generator_base[project].code(node, config, prog_code) + return self.doxy_error( project, config, f"Did not find File: `{config.get('file')}`", @@ -261,28 +262,28 @@ def doxyCode(self, snippet, project: str, config): snippet, ) - def codeStrip(self, codeRaw, codeLanguage: str, start: int = 1, end: int = None): - lines = codeRaw.split("\n") + def code_strip(self, code_raw, code_language: str, start: int = 1, end: Optional[int] = None) -> Union[str, bool]: + lines = code_raw.split("\n") if end and start > end: return False out = "".join(line + "\n" for num, line in enumerate(lines) if num >= start and (num <= end or end == 0)) - return f"```{codeLanguage} linenums='{start}'\n{out}```" + return f"```{code_language} linenums='{start}'\n{out}```" - def doxyFunction(self, snippet, project: str, config: dict): - errorMsg = self.checkConfig(snippet, project, config, ["name"]) - if errorMsg: - return errorMsg + def doxy_function(self, snippet, project: str, config: dict): + error_msg = self.check_config(snippet, project, config, ["name"]) + if error_msg: + return error_msg - node = self.finder.doxyFunction(project, config.get("name")) + node = self.finder.doxy_function(project, config.get("name")) if node is None: - return self.doxyNodeIsNone(project, config, snippet) + return self.doxy_node_is_none(project, config, snippet) if isinstance(node, Node): - self._setLinkPrefixNode(node, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].function(node, config) - return self.doxyError( + self._set_link_prefix_node(node, self.page_url_prefix + project + "/") + return self.generator_base[project].function(node, config) + return self.doxy_error( project, config, "Incorrect function configuration", @@ -293,19 +294,19 @@ def doxyFunction(self, snippet, project: str, config: dict): snippet, ) - def doxyClass(self, snippet, project: str, config: dict): - errorMsg = self.checkConfig(snippet, project, config, ["name"]) - if errorMsg: - return errorMsg + def doxy_class(self, snippet, project: str, config: dict): + error_msg = self.check_config(snippet, project, config, ["name"]) + if error_msg: + return error_msg - node = self.finder.doxyClass(project, config.get("name")) + node = self.finder.doxy_class(project, config.get("name")) if node is None: - return self.doxyNodeIsNone(project, config, snippet) + return self.doxy_node_is_none(project, config, snippet) if isinstance(node, Node): - self._setLinkPrefixNode(node, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].member(node, config) - return self.doxyError( + self._set_link_prefix_node(node, self.page_url_prefix + project + "/") + return self.generator_base[project].member(node, config) + return self.doxy_error( project, config, "Incorrect class configuration", @@ -316,19 +317,19 @@ def doxyClass(self, snippet, project: str, config: dict): snippet, ) - def doxyClassMethod(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, ["name", "method"]) - if errorMsg: - return errorMsg + def doxy_class_method(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, ["name", "method"]) + if error_msg: + return error_msg - node = self.finder.doxyClassMethod(project, config.get("name"), config.get("method")) + node = self.finder.doxy_class_method(project, config.get("name"), config.get("method")) if node is None: - return self.doxyNodeIsNone(project, config, snippet) + return self.doxy_node_is_none(project, config, snippet) if isinstance(node, Node): - self._setLinkPrefixNode(node, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].function(node, config) - return self.doxyError( + self._set_link_prefix_node(node, self.page_url_prefix + project + "/") + return self.generator_base[project].function(node, config) + return self.doxy_error( project, config, "Incorrect class method configuration", @@ -339,51 +340,51 @@ def doxyClassMethod(self, snippet, project: str, config): snippet, ) - def doxyClassList(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, []) - if errorMsg: - return errorMsg + def doxy_class_list(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, []) + if error_msg: + return error_msg nodes = self.doxygen[project].root.children - self._setLinkPrefixNodes(nodes, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].annotated(nodes, config) + self._set_link_prefix_nodes(nodes, self.page_url_prefix + project + "/") + return self.generator_base[project].annotated(nodes, config) - def doxyClassIndex(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, []) - if errorMsg: - return errorMsg + def doxy_class_index(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, []) + if error_msg: + return error_msg nodes = self.doxygen[project].root.children - self._setLinkPrefixNodes(nodes, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].classes(nodes, config) + self._set_link_prefix_nodes(nodes, self.page_url_prefix + project + "/") + return self.generator_base[project].classes(nodes, config) - def doxyClassHierarchy(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, []) - if errorMsg: - return errorMsg + def doxy_class_hierarchy(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, []) + if error_msg: + return error_msg nodes = self.doxygen[project].root.children - self._setLinkPrefixNodes(nodes, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].hierarchy(nodes, config) + self._set_link_prefix_nodes(nodes, self.page_url_prefix + project + "/") + return self.generator_base[project].hierarchy(nodes, config) - def doxyNamespaceList(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, []) - if errorMsg: - return errorMsg + def doxy_namespace_list(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, []) + if error_msg: + return error_msg nodes = self.doxygen[project].root.children - self._setLinkPrefixNodes(nodes, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].namespaces(nodes, config) + self._set_link_prefix_nodes(nodes, self.page_url_prefix + project + "/") + return self.generator_base[project].namespaces(nodes, config) - def doxyNamespaceFunction(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, ["namespace", "name"]) - if errorMsg: - return errorMsg + def doxy_namespace_function(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, ["namespace", "name"]) + if error_msg: + return error_msg - node = self.finder.doxyNamespaceFunction(project, config.get("namespace"), config.get("name")) + node = self.finder.doxy_namespace_function(project, config.get("namespace"), config.get("name")) if node is None: - return self.doxyNodeIsNone(project, config, snippet) + return self.doxy_node_is_none(project, config, snippet) if isinstance(node, Node): - self._setLinkPrefixNode(node, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].function(node, config) - return self.doxyError( + self._set_link_prefix_node(node, self.page_url_prefix + project + "/") + return self.generator_base[project].function(node, config) + return self.doxy_error( project, config, "Incorrect namespace function configuration", @@ -394,16 +395,16 @@ def doxyNamespaceFunction(self, snippet, project: str, config): snippet, ) - def doxyFileList(self, snippet, project: str, config): - errorMsg = self.checkConfig(snippet, project, config, []) - if errorMsg: - return errorMsg + def doxy_file_list(self, snippet, project: str, config): + error_msg = self.check_config(snippet, project, config, []) + if error_msg: + return error_msg nodes = self.doxygen[project].files.children - self._setLinkPrefixNodes(nodes, self.pageUrlPrefix + project + "/") - return self.generatorBase[project].fileindex(nodes, config) + self._set_link_prefix_nodes(nodes, self.page_url_prefix + project + "/") + return self.generator_base[project].fileindex(nodes, config) - def doxyNodeIsNone(self, project: str, config: dict, snippet: str) -> str: - return self.doxyError( + def doxy_node_is_none(self, project: str, config: dict, snippet: str) -> str: + return self.doxy_error( project, config, f"Could not find coresponding snippet for project {project}", @@ -417,8 +418,8 @@ def doxyNodeIsNone(self, project: str, config: dict, snippet: str) -> str: class SnippetClass: - def __init__(self, config): + def __init__(self, config) -> None: self.config = config - def default(self): + def default(self) -> str: return "" diff --git a/mkdoxy/markdown.py b/mkdoxy/markdown.py index 2740f250..d5fdfe9a 100644 --- a/mkdoxy/markdown.py +++ b/mkdoxy/markdown.py @@ -1,64 +1,61 @@ -from typing import List - - def escape(s: str) -> str: ret = s.replace("*", "\\*") ret = ret.replace("_", "\\_") ret = ret.replace("<", "<") ret = ret.replace(">", ">") - return ret.replace("|", "\|") + return ret.replace("|", "\\|") class MdRenderer: - def __init__(self): + def __init__(self) -> None: self.output = "" self.eol_flag = True - def write(self, s: str): + def write(self, s: str) -> None: self.output += s self.eol_flag = False - def eol(self): + def eol(self) -> None: if not self.eol_flag: self.output += "\n" self.eol_flag = True class Md: - def __init__(self, children: List["Md"]): + def __init__(self, children: list["Md"]) -> None: self.children = children - def append(self, child: "Md"): + def append(self, child: "Md") -> None: self.children.append(child) - def extend(self, child: List["Md"]): + def extend(self, child: list["Md"]) -> None: self.children.extend(child) class Text: - def __init__(self, text: str): + def __init__(self, text: str) -> None: self.text = text - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: if self.text: f.write(escape(self.text)) class Br: - def __init__(self): + def __init__(self) -> None: pass - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("\n\n") class MdHint(Md): - def __init__(self, children: List[Md], typ: str, title: str): + def __init__(self, children: list[Md], typ: str, title: str) -> None: Md.__init__(self, children) self.title = title self.typ = typ - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write(f"::: {self.typ} {self.title}" + "\n") for child in self.children: child.render(f, "") @@ -66,10 +63,10 @@ def render(self, f: MdRenderer, indent: str): class MdBold(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("**") for child in self.children: child.render(f, "") @@ -77,29 +74,29 @@ def render(self, f: MdRenderer, indent: str): class MdImage: - def __init__(self, url: str): + def __init__(self, url: str) -> None: self.url = url - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write(f"![Image]({self.url})") class Code: - def __init__(self, text: str): + def __init__(self, text: str) -> None: self.text = text - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write(f"`{self.text}`") class MdCodeBlock: - def __init__(self, lines: List[str]): + def __init__(self, lines: list[str]) -> None: self.lines = lines - def append(self, line: str): + def append(self, line: str) -> None: self.lines.append(line) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("```\n") for line in self.lines: f.write(line) @@ -108,10 +105,10 @@ def render(self, f: MdRenderer, indent: str): class MdBlockQuote(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("\n") for child in self.children: f.write("> ") @@ -120,10 +117,10 @@ def render(self, f: MdRenderer, indent: str): class MdItalic(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("_") for child in self.children: child.render(f, "") @@ -131,21 +128,21 @@ def render(self, f: MdRenderer, indent: str): class MdParagraph(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: for child in self.children: child.render(f, indent) f.eol() class MdLink(Md): - def __init__(self, children: List[Md], url: str): + def __init__(self, children: list[Md], url: str) -> None: Md.__init__(self, children) self.url = url - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("[") for child in self.children: child.render(f, "") @@ -153,10 +150,10 @@ def render(self, f: MdRenderer, indent: str): class MdList(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.eol() for child in self.children: if not isinstance(child, MdList): @@ -165,21 +162,21 @@ def render(self, f: MdRenderer, indent: str): class MdLine: - def __init__(self): + def __init__(self) -> None: pass - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.eol() f.write("----------------------------------------") f.eol() class MdHeader(Md): - def __init__(self, level: int, children: List[Md]): + def __init__(self, level: int, children: list[Md]) -> None: Md.__init__(self, children) self.level = level - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.write("#" * self.level + " ") for child in self.children: child.render(f, f"{indent}") @@ -188,19 +185,19 @@ def render(self, f: MdRenderer, indent: str): class MdTableCell(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: for child in self.children: child.render(f, indent) class MdTableRow(Md): - def __init__(self, children: List[Md]): + def __init__(self, children: list[Md]) -> None: Md.__init__(self, children) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: f.eol() f.write("|") for child in self.children: @@ -210,10 +207,10 @@ def render(self, f: MdRenderer, indent: str): class MdTable(Md): - def __init__(self): + def __init__(self) -> None: Md.__init__(self, []) - def render(self, f: MdRenderer, indent: str): + def render(self, f: MdRenderer, indent: str) -> None: is_first = True f.eol() for child in self.children: @@ -224,3 +221,22 @@ def render(self, f: MdRenderer, indent: str): f.write("|") is_first = False f.write("\n\n") + + +class MdInlineEquation(Md): + def __init__(self, equation: str) -> None: + self.equation = equation + + def render(self, f: MdRenderer, indent: str) -> None: + if self.equation: + f.write(rf"\({self.equation}\)") + + +class MdBlockEquation(Md): + def __init__(self, equation: str) -> None: + self.equation = equation + + def render(self, f: MdRenderer, indent: str) -> None: + f.write("\n") + f.write(rf"\[{self.equation}\]") + f.write("\n") diff --git a/mkdoxy/migration.py b/mkdoxy/migration.py new file mode 100644 index 00000000..d09f17a2 --- /dev/null +++ b/mkdoxy/migration.py @@ -0,0 +1,56 @@ +import logging +import re +import shutil +from pathlib import Path + +log = logging.getLogger("mkdoxy.migration") + + +def update_new_config(yaml_file: Path, backup: bool, backup_file_name: str) -> None: + """ + Migrate MkDoxy configuration to the new version by replacing legacy keys + directly in the text file—preserving comments and structure. + + Legacy keys are replaced only on non-comment lines. + + :param yaml_file: Path to the mkdocs YAML configuration file. + :param backup: If True, a backup of the original file is created. + :param backup_file_name: The filename to use for the backup. + """ + if backup: + backup_path = yaml_file.parent / backup_file_name + shutil.copy2(yaml_file, backup_path) + log.info(f"Backup created at {backup_path}") + + text = yaml_file.read_text(encoding="utf-8") + + # Merge global and project legacy mappings. + legacy_mapping = {} + legacy_mapping.update( + { + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "save-api": "custom_api_folder", + "doxygen-bin-path": "doxygen_bin_path", + } + ) + legacy_mapping.update( + { + "src-dirs": "src_dirs", + "full-doc": "full_doc", + "ignore-errors": "ignore_errors", + "doxy-cfg": "doxy_config_dict", + "doxy-cfg-file": "doxy_config_file", + "template-dir": "custom_template_dir", + } + ) + + # Replace each legacy key only on lines that are not comments. + for old_key, new_key in legacy_mapping.items(): + # Pattern matches lines that do not start with a comment (after optional whitespace), + # then the legacy key followed by optional spaces and a colon. + pattern = re.compile(rf"(?m)^(?!\s*#)(\s*){re.escape(old_key)}(\s*:)", re.UNICODE) + text = pattern.sub(rf"\1{new_key}\2", text) + + yaml_file.write_text(text, encoding="utf-8") + log.info("Migration completed successfully") diff --git a/mkdoxy/node.py b/mkdoxy/node.py index 41323070..e7103285 100644 --- a/mkdoxy/node.py +++ b/mkdoxy/node.py @@ -1,6 +1,8 @@ import logging import os -from xml.etree import ElementTree +import re +from typing import Optional +from xml.etree import ElementTree as ET from xml.etree.ElementTree import Element as Element from mkdoxy.constants import OVERLOAD_OPERATORS, Kind, Visibility @@ -21,10 +23,10 @@ def __init__( project: ProjectContext, parser: XmlParser, parent: "Node", - refid: str = None, + refid: Optional[str] = None, debug: bool = False, - ): - self._children: ["Node"] = [] + ) -> None: + self._children: [Node] = [] self._cache = project.cache self._parser: XmlParser = parser self._parent = parent @@ -41,7 +43,7 @@ def __init__( if self.debug: log.info(f"Loading XML from: {xml_file}") self._dirname = os.path.dirname(xml_file) - self._xml = ElementTree.parse(xml_file).getroot().find("compounddef") + self._xml = ET.parse(xml_file).getroot().find("compounddef") if self._xml is None: raise Exception(f"File {xml_file} has no ") self._kind = Kind.from_str(self._xml.get("kind")) @@ -88,16 +90,16 @@ def __init__( self._definition = Property.Definition(self._xml, parser, self._kind) self._programlisting = Property.Programlisting(self._xml, parser, self._kind) - def __repr__(self): + def __repr__(self) -> str: return f"Node: {self.name} refid: {self._refid}" - def add_child(self, child: "Node"): + def add_child(self, child: "Node") -> None: self._children.append(child) - def sort_children(self): + def sort_children(self) -> None: self._children.sort(key=lambda x: x._name, reverse=False) - def _check_for_children(self): + def _check_for_children(self) -> None: for innergroup in self._xml.findall("innergroup"): refid = innergroup.get("refid") if self._kind in [Kind.GROUP, Kind.DIR, Kind.FILE]: @@ -240,7 +242,7 @@ def _check_for_children(self): # if para.find('programlisting') is not None: # self._programlisting = Property.Programlisting(para, self._parser, self._kind) - def _check_attrs(self): + def _check_attrs(self) -> None: prot = self._xml.get("prot") self._visibility = Visibility(prot) if prot is not None else Visibility.PUBLIC @@ -256,8 +258,8 @@ def _check_attrs(self): inline = self._xml.get("inline") self._inline = inline == "yes" - const = self._xml.get("inline") - self._const = const == "yes" + const_val = self._xml.get("inline") + self._const = const_val == "yes" name = self._xml.find("name") if name is not None and name.text: @@ -281,9 +283,9 @@ def _check_attrs(self): def has(self, visibility: str, kinds: [str], static: bool) -> bool: return len(self.query(visibility, kinds, static)) > 0 - def query(self, visibility: str, kinds: [str], static: bool) -> ["Node"]: + def query(self, visibility: str, kinds: [str], static: bool) -> [Node]: visibility = Visibility(visibility) - kinds = list(map(lambda kind: Kind.from_str(kind), kinds)) + kinds = [Kind.from_str(kind) for kind in kinds] return [ child for child in self._children @@ -323,11 +325,11 @@ def has_children(self) -> bool: return len(self._children) > 0 @property - def children(self) -> ["Node"]: + def children(self) -> [Node]: return self._children @property - def parent(self) -> "Node": + def parent(self) -> Node: return self._parent @property @@ -417,9 +419,9 @@ def name(self) -> str: @property def name_params(self) -> str: name = self._name - type = self._type.plain() + type_val = self._type.plain() params = self._specifiers.plain() - return f"{type} {name}{params}" if params else self.name_long + return f"{type_val} {name}{params}" if params else self.name_long @property def title(self) -> str: @@ -443,9 +445,22 @@ def operators_total(self) -> int: @property def operator_num(self) -> int: + stem = "operator" + ("-" * self._name.count("-")) total = 0 for child in self.parent.children: - if child.is_function and child.name.replace(" ", "") in OVERLOAD_OPERATORS: + child_refid = child.name.replace(" ", "") + # Check if the child is a displayed operator by ensuring: + # 1. Its identifier is in the predefined OVERLOAD_OPERATORS list. + # 2. It starts with the expected 'stem' derived from the parent's naming. + # 3. It does not start with an extra hyphen (stem+'-') to avoid excessive matching. + # 4. It is not private. + if ( + child.is_function + and child_refid in OVERLOAD_OPERATORS + and child_refid.startswith(stem) + and not child_refid.startswith(stem + "-") + and child._visibility != Visibility.PRIVATE + ): total += 1 if child.refid == self._refid: break @@ -454,16 +469,21 @@ def operator_num(self) -> int: @property def name_url_safe(self) -> str: name = self.name_tokens[-1] - return name.replace(" ", "-").replace("=", "").replace("~", "").lower() + # Strip special characters that do not appear in anchors + name = re.sub("[=~.,<>]", "", name) + return name.strip(" ").replace(" ", "-").lower() @property def anchor(self) -> str: name = "" if self._name.replace(" ", "") in OVERLOAD_OPERATORS: num = self.operator_num - name = f"operator_{str(self.operator_num - 1)}" if num > 1 else "operator" + if self._name.startswith("operator-"): + name = f"operator-_{num - 1!s}" if num > 1 else "operator-" + else: + name = f"operator_{num - 1!s}" if num > 1 else "operator" elif self.is_overloaded: - name = f"{self.name_url_safe}-{str(self.overload_num)}{str(self.overload_total)}" + name = f"{self.name_url_safe}-{self.overload_num!s}{self.overload_total!s}" else: name = self.name_url_safe @@ -515,7 +535,7 @@ def filename(self) -> str: return self.project.linkPrefix + self._refid + ".md" @property - def root(self) -> "Node": + def root(self) -> Node: return self if self._kind == Kind.ROOT else self._parent.root @property @@ -574,10 +594,10 @@ def overload_suffix(self) -> str: return "" total = self.overload_total - return f"[{str(self.overload_num)}/{str(total)}]" if total > 1 else "" + return f"[{self.overload_num!s}/{total!s}]" if total > 1 else "" @property - def parents(self) -> ["Node"]: + def parents(self) -> [Node]: ret = [] if self._parent is not None and (self._parent.is_language or self._parent.is_dir): ret.extend(self.parent.parents) @@ -695,7 +715,7 @@ def has_derived_classes(self) -> bool: return len(self._xml.findall("derivedcompoundref")) > 0 @property - def base_classes(self) -> ["Node"]: + def base_classes(self) -> [Node]: ret = [] for basecompoundref in self._xml.findall("basecompoundref"): refid = basecompoundref.get("refid") @@ -706,7 +726,7 @@ def base_classes(self) -> ["Node"]: return ret @property - def derived_classes(self) -> ["Node"]: + def derived_classes(self) -> [Node]: ret = [] for derivedcompoundref in self._xml.findall("derivedcompoundref"): refid = derivedcompoundref.get("refid") @@ -830,7 +850,7 @@ def is_resolved(self) -> bool: return True @property - def reimplements(self) -> "Node": + def reimplements(self) -> Node: reimp = self._xml.find("reimplements") return self._cache.get(reimp.get("refid")) if reimp is not None else None @@ -842,7 +862,7 @@ def print_node_recursive(self) -> str: def _print_node_recursive_md(self, node: Element, depth: int) -> str: # print as Markdown code block - indent = " " * depth + indent = "\t" * depth ret = f"{indent} * {node.tag} {node.attrib} -> Text: {node.text}\n" for child in node.findall("*"): ret += self._print_node_recursive_md(child, depth + 1) @@ -851,7 +871,7 @@ def _print_node_recursive_md(self, node: Element, depth: int) -> str: class DummyNode: - def __init__(self, name_long: str, derived_classes: [Node], kind: Kind): + def __init__(self, name_long: str, derived_classes: [Node], kind: Kind) -> None: self.name_long = name_long self.derived_classes = derived_classes self.kind = kind diff --git a/mkdoxy/plugin.py b/mkdoxy/plugin.py index 20063227..030adb23 100644 --- a/mkdoxy/plugin.py +++ b/mkdoxy/plugin.py @@ -5,53 +5,35 @@ """ import logging -from pathlib import Path, PurePath +from pathlib import Path -from mkdocs import exceptions -from mkdocs.config import Config, base, config_options +from mkdocs.config import Config from mkdocs.plugins import BasePlugin -from mkdocs.structure import files, pages +from mkdocs.structure.files import Files +from mkdocs.structure.pages import Page from mkdoxy.cache import Cache +from mkdoxy.doxy_config import MkDoxyConfig from mkdoxy.doxygen import Doxygen -from mkdoxy.doxyrun import DoxygenRun +from mkdoxy.doxygen_generator import DoxygenGenerator from mkdoxy.generatorAuto import GeneratorAuto from mkdoxy.generatorBase import GeneratorBase from mkdoxy.generatorSnippets import GeneratorSnippets from mkdoxy.xml_parser import XmlParser log: logging.Logger = logging.getLogger("mkdocs") -pluginName: str = "MkDoxy" +plugin_name: str = "MkDoxy" -class MkDoxy(BasePlugin): +class MkDoxy(BasePlugin[MkDoxyConfig]): """! MkDocs plugin for generating documentation from Doxygen XML files.""" - # Config options for the plugin - config_scheme = ( - ("projects", config_options.Type(dict, default={})), - ("full-doc", config_options.Type(bool, default=True)), - ("debug", config_options.Type(bool, default=False)), - ("ignore-errors", config_options.Type(bool, default=False)), - ("save-api", config_options.Type(str, default="")), - ("enabled", config_options.Type(bool, default=True)), - ( - "doxygen-bin-path", - config_options.Type(str, default="doxygen", required=False), - ), - ) - - # Config options for each project - config_project = ( - ("src-dirs", config_options.Type(str)), - ("full-doc", config_options.Type(bool, default=True)), - ("debug", config_options.Type(bool, default=False)), - # ('ignore-errors', config_options.Type(bool, default=False)), - ("api-path", config_options.Type(str, default=".")), - ("doxy-cfg", config_options.Type(dict, default={}, required=False)), - ("doxy-cfg-file", config_options.Type(str, default="", required=False)), - ("template-dir", config_options.Type(str, default="", required=False)), - ) + def __init__(self) -> None: + self.generator_base: dict[str, GeneratorBase] = {} + self.doxygen: dict[str, Doxygen] = {} + self.default_template_config = { + "indent_level": 0, + } def is_enabled(self) -> bool: """! Checks if the plugin is enabled @@ -60,145 +42,107 @@ def is_enabled(self) -> bool: """ return self.config.get("enabled") - def on_files(self, files: files.Files, config: base.Config) -> files.Files: + def on_files(self, files: Files, config: Config) -> Files: """! Called after files have been gathered by MkDocs. - @details + @details generate automatic documentation and append files in the list of files to be processed by mkdocs @param files: (Files) The files gathered by MkDocs. @param config: (Config) The global configuration object. @return: (Files) The files gathered by MkDocs. """ - if not self.is_enabled(): - return files - - def checkConfig(config_project, proData, strict: bool): - cfg = Config(config_project, "") - cfg.load_dict(proData) - errors, warnings = cfg.validate() - for config_name, warning in warnings: - log.warning(f" -> Config value: '{config_name}' in project '{project_name}'. Warning: {warning}") - for config_name, error in errors: - log.error(f" -> Config value: '{config_name}' in project '{project_name}'. Error: {error}") - - if len(errors) > 0: - raise exceptions.Abort(f"Aborted with {len(errors)} Configuration Errors!") - elif strict and len(warnings) > 0: - raise exceptions.Abort(f"Aborted with {len(warnings)} Configuration Warnings in 'strict' mode!") - - def tempDir(siteDir: str, tempDir: str, projectName: str) -> str: - tempDoxyDir = PurePath.joinpath(Path(siteDir), Path(tempDir), Path(projectName)) - tempDoxyDir.mkdir(parents=True, exist_ok=True) - return str(tempDoxyDir) - - self.doxygen = {} - self.generatorBase = {} - self.projects_config: dict[str, dict[str, any]] = self.config["projects"] - self.debug = self.config.get("debug", False) - - # generate automatic documentation and append files in the list of files to be processed by mkdocs - self.defaultTemplateConfig: dict = { - "indent_level": 0, - } - - log.info(f"Start plugin {pluginName}") - - for project_name, project_data in self.projects_config.items(): - log.info(f"-> Start project '{project_name}'") + for project_name, project_config in self.config.projects.items(): + log.info(f"-> Processing project '{project_name}'") - # Check project config -> raise exceptions - checkConfig(self.config_project, project_data, config["strict"]) - - if self.config.get("save-api"): - tempDirApi = tempDir("", self.config.get("save-api"), project_name) + # Generate Doxygen and MD files to user defined folder or default temp folder + if self.config.custom_api_folder: + temp_doxy_folder = Path.joinpath(Path(self.config.custom_api_folder), Path(project_name)) else: - tempDirApi = tempDir(config["site_dir"], "assets/.doxy/", project_name) + temp_doxy_folder = Path.joinpath(Path(config["site_dir"]), Path("assets/.doxy"), Path(project_name)) + + # Create temp dir for Doxygen if not exists + temp_doxy_folder.mkdir(parents=True, exist_ok=True) # Check src changes -> run Doxygen - doxygenRun = DoxygenRun( - self.config["doxygen-bin-path"], - project_data.get("src-dirs"), - tempDirApi, - project_data.get("doxy-cfg", {}), - project_data.get("doxy-cfg-file", ""), + doxygen = DoxygenGenerator( + self.config, + project_config, + temp_doxy_folder, ) - if doxygenRun.checkAndRun(): - log.info(" -> generating Doxygen files") + if doxygen.has_changes(): + log.info(" -> Generating Doxygen files started") + doxygen.run() + log.info(" -> Doxygen files generated") else: - log.info(" -> skip generating Doxygen files (nothing changes)") + log.info(" -> skip generating Doxygen files (nothing seems to have changed)") # Parse XML to basic structure cache = Cache() - parser = XmlParser(cache=cache, debug=self.debug) + parser = XmlParser(cache=cache, debug=self.config.debug) # Parse basic structure to recursive Nodes - self.doxygen[project_name] = Doxygen(doxygenRun.getOutputFolder(), parser=parser, cache=cache) + # TODO: Doxygen index_path should be Path object + self.doxygen[project_name] = Doxygen(str(doxygen.get_output_xml_folder()), parser=parser, cache=cache) # Print parsed files - if self.debug: + if self.config.debug: self.doxygen[project_name].printStructure() # Prepare generator for future use (GeneratorAuto, SnippetGenerator) - self.generatorBase[project_name] = GeneratorBase( - project_data.get("template-dir", ""), - ignore_errors=self.config["ignore-errors"], - debug=self.debug, + self.generator_base[project_name] = GeneratorBase( + project_config.custom_template_dir, + False, # ignore_errors=self.config.ignore_errors, + debug=self.config.debug, ) - if self.config["full-doc"] and project_data.get("full-doc", True): + if self.config.full_doc and project_config.full_doc: generatorAuto = GeneratorAuto( - generatorBase=self.generatorBase[project_name], - tempDoxyDir=tempDirApi, + generatorBase=self.generator_base[project_name], + tempDoxyDir=str(temp_doxy_folder), siteDir=config["site_dir"], - apiPath=project_data.get("api-path", project_name), + apiPath=project_name, doxygen=self.doxygen[project_name], useDirectoryUrls=config["use_directory_urls"], ) - project_config = self.defaultTemplateConfig.copy() - project_config.update(project_data) - generatorAuto.fullDoc(project_config) + template_config = self.default_template_config.copy() + + # Generate full documentation + generatorAuto.fullDoc(template_config) - generatorAuto.summary(project_config) + # Generate summary pages + generatorAuto.summary(template_config) + # Append files to be processed by MkDocs for file in generatorAuto.fullDocFiles: files.append(file) return files - def on_page_markdown( - self, - markdown: str, - page: pages.Page, - config: base.Config, - files: files.Files, - ) -> str: + def on_page_markdown(self, markdown: str, page: Page, config: Config, files: Files) -> str: """! Generate snippets and append them to the markdown. @details - - @param markdown (str): The markdown. - @param page (Page): The MkDocs page. - @param config (Config): The MkDocs config. - @param files (Files): The MkDocs files. - @return: (str) The markdown. + @param markdown: (str) The markdown content of the page. + @param page: (Page) The page object. + @param config: (Config) The global configuration object. + @param files: (Files) The files gathered by MkDocs. + @return: (str) The markdown content of the page. """ - if not self.is_enabled(): - return markdown - # update default template config with page meta - page_config = self.defaultTemplateConfig.copy() + # update default template config with page meta tags + page_config = self.default_template_config.copy() page_config.update(page.meta) - generatorSnippets = GeneratorSnippets( + generator_snippets = GeneratorSnippets( markdown=markdown, - generatorBase=self.generatorBase, + generatorBase=self.generator_base, doxygen=self.doxygen, - projects=self.projects_config, + projects=self.config.projects, useDirectoryUrls=config["use_directory_urls"], page=page, config=page_config, - debug=self.debug, + debug=self.config.debug, ) - return generatorSnippets.generate() + return generator_snippets.generate() # def on_serve(self, server): diff --git a/mkdoxy/property.py b/mkdoxy/property.py index bc37882b..a5d11ef3 100644 --- a/mkdoxy/property.py +++ b/mkdoxy/property.py @@ -8,7 +8,7 @@ class Property: class Details: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -28,7 +28,7 @@ def has(self) -> bool: return len(list(detaileddescription)) > 0 class Brief: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -53,7 +53,7 @@ def has(self) -> bool: return len(list(detaileddescription)) > 0 class Includes: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -80,7 +80,7 @@ def has(self) -> bool: return len(self.xml.findall("includes")) > 0 class Type: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -96,7 +96,7 @@ def has(self) -> bool: return self.xml.find("type") is not None class Location: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -128,7 +128,7 @@ def has(self) -> bool: return self.xml.find("location") is not None class Params: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -143,8 +143,8 @@ def array(self, plain: bool = False) -> [str]: ret = [] for param in self.xml.findall("param"): p = "" - type = param.find("type") - p = self.parser.paras_as_str(type, plain=plain) + type_elem = param.find("type") + p = self.parser.paras_as_str(type_elem, plain=plain) declname = param.find("declname") if declname is not None: @@ -165,7 +165,7 @@ def has(self) -> bool: return len(self.xml.findall("param")) > 0 class TemplateParams: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -187,8 +187,8 @@ def array(self, plain: bool = False, notype: bool = False) -> [str]: declname = param.find("type") ret.append(self.parser.paras_as_str(declname, plain=plain)) else: - type = param.find("type") - declaration = self.parser.paras_as_str(type, plain=plain) + type_elem = param.find("type") + declaration = self.parser.paras_as_str(type_elem, plain=plain) declname = param.find("declname") if declname is not None: declaration += f" {self.parser.paras_as_str(declname, plain=plain)}" @@ -199,7 +199,7 @@ def has(self) -> bool: return self.xml.find("templateparamlist") is not None class CodeBlock: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -214,7 +214,7 @@ def has(self) -> bool: return True class Specifiers: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -265,7 +265,7 @@ def has(self) -> bool: return self.xml.find("argsstring") is not None class Values: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -291,7 +291,7 @@ def has(self) -> bool: return self.xml.find("enumvalue") is not None if self.kind.is_enum() else False class Initializer: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -313,7 +313,7 @@ def has(self) -> bool: return self.xml.find("initializer") is not None class Definition: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind @@ -332,7 +332,7 @@ def has(self) -> bool: return self.xml.find("definition") is not None class Programlisting: - def __init__(self, xml: Element, parser: XmlParser, kind: Kind): + def __init__(self, xml: Element, parser: XmlParser, kind: Kind) -> None: self.xml = xml self.parser = parser self.kind = kind diff --git a/mkdoxy/utils.py b/mkdoxy/utils.py index b6617cc6..693784b3 100644 --- a/mkdoxy/utils.py +++ b/mkdoxy/utils.py @@ -28,7 +28,7 @@ def lookahead(iterable): yield last, False -def contains(a, pos, b): +def contains(a, pos, b) -> bool: ai = pos bi = 0 if len(b) > len(a) - ai: diff --git a/mkdoxy/xml_parser.py b/mkdoxy/xml_parser.py index a58de2a4..ef4db21d 100644 --- a/mkdoxy/xml_parser.py +++ b/mkdoxy/xml_parser.py @@ -5,11 +5,13 @@ Br, Code, Md, + MdBlockEquation, MdBlockQuote, MdBold, MdCodeBlock, MdHeader, MdImage, + MdInlineEquation, MdItalic, MdLink, MdList, @@ -51,7 +53,7 @@ class XmlParser: - def __init__(self, cache: Cache, debug: bool = False): + def __init__(self, cache: Cache, debug: bool = False) -> None: self.cache = cache self.debug = debug @@ -277,6 +279,12 @@ def paras(self, p: Element, italic: bool = False) -> [Md]: elif item.tag == "emphasis": ret.append(MdItalic(self.paras(item))) + elif item.tag == "formula": + equation = item.text.strip("$ ") + if len(p) == 1 and item.tail is None: + ret.append(MdBlockEquation(equation)) + else: + ret.append(MdInlineEquation(equation)) if item.tail: if italic: diff --git a/pyproject.toml b/pyproject.toml index 8f0b403f..28e90423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,113 @@ +[build-system] +requires = ["hatchling>=1.21.0"] +build-backend = "hatchling.build" + +[project] +name = "mkdoxy" +version = "2.0.0" +description = "MkDoxy → MkDocs + Doxygen = easy documentation generator with code snippets" +readme = "README.md" +keywords = ["mkdoxy", "python", "open-source", "documentation", "mkdocs", "doxygen", "multilanguage", "code-snippets", "code", "snippets", "documentation-generator"] +license = "MIT" +authors = [ + {name = "Jakub Andrýsek", email = "email@kubaandrysek.cz"} +] +maintainers = [ + {name = "Jakub Andrýsek", email = "email@kubaandrysek.cz"} +] +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", +] +dependencies = [ + "mkdocs>=1.4.0", +] + +[project.optional-dependencies] +dev = [ + "mkdocs-material~=9.6.18", + "mkdocs-open-in-new-tab~=1.0.8", + "pathlib~=1.0.1", + "isort~=6.0.1", + "pytest~=8.4.1", + "pre-commit~=4.3.0", + "setuptools~=80.9.0", + "build~=1.3.0", + "twine~=6.1.0", + "sourcery~=1.37.0", + "click~=8.2.1", + "ruff~=0.9.6", + "hatch~=1.14.0", +] + +[project.urls] +Homepage = "https://github.com/JakubAndrysek/MkDoxy" +Documentation = "https://mkdoxy.kubaandrysek.cz/" +Source = "https://github.com/JakubAndrysek/MkDoxy" +Tracker = "https://github.com/JakubAndrysek/MkDoxy/issues" +Funding = "https://github.com/sponsors/jakubandrysek" + +[project.scripts] +mkdoxy = "mkdoxy.__main__:main" + +[project.entry-points] +"mkdocs.plugins" = {mkdoxy = "mkdoxy.plugin:MkDoxy"} + +[tool.setuptools.packages.find] +include = ["mkdoxy*"] + +[tool.setuptools.package-data] +mkdoxy = ["templates/*.jinja2"] + [tool.ruff] line-length = 120 -[tool.black] -line-length = 120 +[tool.ruff.lint] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +select = ["E", "F"] +extend-select = [ + "I", # isort + "C9", # mccabe + "N", # pep8-naming + "UP", # pyupgrade + "ANN", # flake8-annotations + "S", # bandit + "BLE", # flake8-blind-except + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "EXE", # flake8-executable + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PIE", # flake8-pie + "PYI", # flake8-pyi + "RUF", # Ruff-specific rules +] +extend-ignore = [ + "ANN101", # Missing type annotation for self + "ANN102", # Missing type annotation for cls +] + +[tool.ruff.format] +# Configure formatting options +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.hatch.build.targets.wheel] +packages = ["mkdoxy"] + +[tool.hatch.build.targets.sdist] +include = [ + "/mkdoxy", + "/README.md", + "/LICENSE", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 016bb16d..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mkdocs diff --git a/setup.py b/setup.py deleted file mode 100755 index 0372c566..00000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -from setuptools import setup, find_packages - - -def readme(): - with open("README.md") as f: - return f.read() - - -def requirements(): - with open("requirements.txt") as f: - return f.read().splitlines() - - -def import_requirements(): - """Imports requirements from requirements.txt file.""" - with open("requirements.txt") as f: - return f.read().splitlines() - - -def import_dev_requirements(): - """Imports requirements from devdeps.txt file.""" - with open("devdeps.txt") as f: - return f.read().splitlines() - - -# https://pypi.org/project/mkdoxy/ -setup( - name="mkdoxy", - version="1.2.7", - description="MkDoxy → MkDocs + Doxygen = easy documentation generator with code snippets", - long_description=readme(), - long_description_content_type="text/markdown", - keywords="mkdoxy, python, open-source, documentation, mkdocs, doxygen, " - "multilanguage, code-snippets, code, snippets, documentation-generator", - # noqa: E501 - url="https://github.com/JakubAndrysek/MkDoxy", - author="Jakub Andrýsek", - author_email="email@kubaandrysek.cz", - license="MIT", - python_requires=">=3.9", - project_urls={ - "Source": "https://github.com/JakubAndrysek/MkDoxy", - "Documentation": "https://mkdoxy.kubaandrysek.cz/", - "Tracker": "https://github.com/JakubAndrysek/MkDoxy/issues", - "Funding": "https://github.com/sponsors/jakubandrysek", - }, - install_requires=import_requirements(), - extras_require={ - "dev": import_dev_requirements(), - }, - classifiers=[ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Operating System :: OS Independent", - ], - packages=find_packages(), - package_data={"mkdoxy": ["templates/*.jinja2"]}, - entry_points={"mkdocs.plugins": ["mkdoxy = mkdoxy.plugin:MkDoxy"]}, -) diff --git a/tests-old/callDoxyByName.py b/tests-old/callDoxyByName.py index 8b15e131..e9e945f0 100644 --- a/tests-old/callDoxyByName.py +++ b/tests-old/callDoxyByName.py @@ -1,19 +1,19 @@ class DoxyCall: - def __init__(self): + def __init__(self) -> None: self.DOXY_CALL = { "class": self.doxyClass, "class.list": self.doxyClassList, } - def callDoxyByName(self, name, config): + def callDoxyByName(self, name, config) -> None: if name in self.DOXY_CALL: funcName = self.DOXY_CALL[name] funcName(config) - def doxyClass(self, config): + def doxyClass(self, config) -> None: print("doxyClassasd", config) - def doxyClassList(self, config): + def doxyClassList(self, config) -> None: print("doxyClassList", config) diff --git a/tests-old/doxygenSubprocess.py b/tests-old/doxygenSubprocess.py index f38b716f..6c0a0180 100644 --- a/tests-old/doxygenSubprocess.py +++ b/tests-old/doxygenSubprocess.py @@ -1,4 +1,4 @@ -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen p = Popen(["doxygen", "-"], stdout=PIPE, stdin=PIPE, stderr=PIPE) diff --git a/tests-old/metaDataParse.py b/tests-old/metaDataParse.py index 2c8fbc6b..ae422710 100644 --- a/tests-old/metaDataParse.py +++ b/tests-old/metaDataParse.py @@ -1,7 +1,7 @@ -import sys import re -import yaml +import sys +import yaml text = """\ --- diff --git a/tests-old/mkdocsValidateCfg.py b/tests-old/mkdocsValidateCfg.py index f0eaecf2..57a4c9ac 100644 --- a/tests-old/mkdocsValidateCfg.py +++ b/tests-old/mkdocsValidateCfg.py @@ -1,5 +1,4 @@ -from mkdocs.config import config_options, Config - +from mkdocs.config import Config, config_options if __name__ == "__main__": config_scheme = ( diff --git a/tests-old/parseMdTags.py b/tests-old/parseMdTags.py index 44f72476..b60814c6 100644 --- a/tests-old/parseMdTags.py +++ b/tests-old/parseMdTags.py @@ -1,10 +1,11 @@ import re import sys + import yaml def readFile(filename: str) -> str: - file = open(filename, "r") + file = open(filename) return file.read() diff --git a/tests-old/snippetsTest.py b/tests-old/snippetsTest.py index b03e63dc..65da8ae4 100644 --- a/tests-old/snippetsTest.py +++ b/tests-old/snippetsTest.py @@ -1,11 +1,12 @@ +from pprint import pprint + +from doxygen_snippets.cache import Cache from doxygen_snippets.doxygen import Doxygen -from doxygen_snippets.generatorBase import GeneratorBase +from doxygen_snippets.doxyrun import DoxygenRun from doxygen_snippets.generatorAuto import GeneratorAuto +from doxygen_snippets.generatorBase import GeneratorBase from doxygen_snippets.generatorSnippets import GeneratorSnippets from doxygen_snippets.xml_parser import XmlParser -from doxygen_snippets.cache import Cache -from doxygen_snippets.doxyrun import DoxygenRun -from pprint import pprint if __name__ == "__main__": doxygenSource = "files/src-stm32" diff --git a/tests-old/testTemplate.py b/tests-old/testTemplate.py index 143fdcb7..cbe0f411 100644 --- a/tests-old/testTemplate.py +++ b/tests-old/testTemplate.py @@ -1,11 +1,12 @@ import os +from pprint import pprint + +from doxygen_snippets.cache import Cache from doxygen_snippets.doxygen import Doxygen -from doxygen_snippets.generatorBase import GeneratorBase +from doxygen_snippets.doxyrun import DoxygenRun from doxygen_snippets.generatorAuto import GeneratorAuto +from doxygen_snippets.generatorBase import GeneratorBase from doxygen_snippets.xml_parser import XmlParser -from doxygen_snippets.cache import Cache -from doxygen_snippets.doxyrun import DoxygenRun -from pprint import pprint if __name__ == "__main__": doxygenPath = "files/" diff --git a/tests/data/Doxyfile b/tests/config/data/Doxyfile similarity index 100% rename from tests/data/Doxyfile rename to tests/config/data/Doxyfile diff --git a/tests/test_doxyrun.py b/tests/config/test_doxyrun.py similarity index 57% rename from tests/test_doxyrun.py rename to tests/config/test_doxyrun.py index 6a1dd74e..de97c540 100644 --- a/tests/test_doxyrun.py +++ b/tests/config/test_doxyrun.py @@ -1,81 +1,71 @@ +from pathlib import Path + import pytest -from mkdoxy.doxyrun import DoxygenCustomConfigNotValid, DoxygenRun + +from mkdoxy.doxy_config import MkDoxyConfig, MkDoxyConfigProject +from mkdoxy.doxygen_generator import DoxygenCustomConfigNotValid, DoxygenGenerator -def test_dox_dict2str(): +def test_dox_dict2str() -> None: dox_dict = { "DOXYFILE_ENCODING": "UTF-8", - "GENERATE_XML": True, - "RECURSIVE": True, "EXAMPLE_PATH": "examples", - "SHOW_NAMESPACES": True, "GENERATE_HTML": False, "GENERATE_LATEX": False, + "GENERATE_XML": True, + "RECURSIVE": True, + "SHOW_NAMESPACES": True, } - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew=dox_dict, - ) - - result = doxygen_run.dox_dict2str(dox_dict) - expected_result = ( - "DOXYFILE_ENCODING = UTF-8\nGENERATE_XML = YES" - "\nRECURSIVE = YES\nEXAMPLE_PATH = examples" - "\nSHOW_NAMESPACES = YES\nGENERATE_HTML = NO" - "\nGENERATE_LATEX = NO" + "DOXYFILE_ENCODING = UTF-8\n" + "EXAMPLE_PATH = examples\n" + "GENERATE_HTML = NO\n" + "GENERATE_LATEX = NO\n" + "GENERATE_XML = YES\n" + "RECURSIVE = YES\n" + "SHOW_NAMESPACES = YES" ) - assert result == expected_result + assert DoxygenGenerator.dox_dict2str(dox_dict) == expected_result # Sets the Doxygen configuration using a custom config file -def test_set_doxy_cfg_custom_file(): - dox_dict = {} - - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyConfigFile="./tests/data/Doxyfile", - doxyCfgNew=dox_dict, +def test_set_doxy_cfg_custom_file() -> None: + project = MkDoxyConfigProject() + project.src_dirs = "/path/to/source/files" + + doxygen_run = DoxygenGenerator( + doxy_config=MkDoxyConfig(), + project_config=project, + temp_doxy_folder=Path("/path/to/temp/folder"), ) - result = doxygen_run.setDoxyCfg(dox_dict) + dox_dict = doxygen_run.get_merged_doxy_dict() expected_result = { "DOXYFILE_ENCODING": "UTF-8", - "GENERATE_XML": True, - "RECURSIVE": True, "EXAMPLE_PATH": "examples", - "SHOW_NAMESPACES": True, "GENERATE_HTML": False, "GENERATE_LATEX": False, + "GENERATE_XML": True, "INPUT": "/path/to/source/files", "OUTPUT_DIRECTORY": "/path/to/temp/folder", + "RECURSIVE": True, + "SHOW_NAMESPACES": True, } - assert result == expected_result + assert expected_result == dox_dict -def test_str2dox_dict(): +def test_str2dox_dict() -> None: dox_str = ( "DOXYFILE_ENCODING = UTF-8\nGENERATE_XML = YES\n" "RECURSIVE = YES\nEXAMPLE_PATH = examples\n" "SHOW_NAMESPACES = YES\nGENERATE_HTML = NO\nGENERATE_LATEX = NO" ) - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) - - result = doxygen_run.str2dox_dict(dox_str) + result = DoxygenGenerator.str2dox_dict(dox_str) expected_result = { "DOXYFILE_ENCODING": "UTF-8", @@ -90,7 +80,7 @@ def test_str2dox_dict(): assert result == expected_result -def test_str2dox_dict_expanded_config(): +def test_str2dox_dict_expanded_config() -> None: dox_str = ( "# This is a comment \n" "PROJECT_LOGO =\n" @@ -101,15 +91,6 @@ def test_str2dox_dict_expanded_config(): "PREDEFINED = BUILD_DATE DOXYGEN=1\n" ) - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) - - result = doxygen_run.str2dox_dict(dox_str) - expected_result = { "PROJECT_LOGO": "", "ABBREVIATE_BRIEF": '"The $name class" is', @@ -117,68 +98,61 @@ def test_str2dox_dict_expanded_config(): "PREDEFINED": "BUILD_DATE DOXYGEN=1", } - assert result == expected_result - + assert expected_result == DoxygenGenerator.str2dox_dict(dox_str) -def test_str2dox_dict_expanded_config_errors(): - doxygen_run = DoxygenRun( - doxygenBinPath="doxygen", - doxygenSource="/path/to/source/files", - tempDoxyFolder="/path/to/temp/folder", - doxyCfgNew={}, - ) +def test_str2dox_dict_expanded_config_errors() -> None: dox_str = "ONLY_KEY\n" - error_message = str( + error_message = ( "Invalid line: 'ONLY_KEY'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "= ONLY_VALUE\n" - error_message = str( + error_message = ( "Invalid line: '= ONLY_VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "KEY WITH SPACES = VALUE\n" - error_message = str( + error_message = ( "Invalid line: 'KEY WITH SPACES = VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") dox_str = "BAD_OPERATOR := VALUE\n" - error_message = str( + error_message = ( "Invalid line: 'BAD_OPERATOR := VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") - dox_str = "BAD_MULTILINE = BAD\n" " VALUE\n" - error_message = str( + dox_str = "BAD_MULTILINE = BAD\n VALUE\n" + error_message = ( "Invalid line: ' VALUE'" - "In custom Doxygen config file: None\n" + "In custom Doxygen config file: QuestionMark\n" "Make sure the file is in standard Doxygen format." "Look at https://mkdoxy.kubaandrysek.cz/usage/advanced/." ) with pytest.raises(DoxygenCustomConfigNotValid, match=error_message): - doxygen_run.str2dox_dict(dox_str) + DoxygenGenerator.str2dox_dict(dox_str, "QuestionMark") diff --git a/tests/config/test_doxyrun_helpers.py b/tests/config/test_doxyrun_helpers.py new file mode 100644 index 00000000..097e01ae --- /dev/null +++ b/tests/config/test_doxyrun_helpers.py @@ -0,0 +1,51 @@ +from mkdoxy.doxygen_generator import DoxygenGenerator + + +def test_merge_doxygen_input_empty_src_dirs(tmp_path) -> None: + # When src_dirs is empty, the function should return doxy_input unchanged. + src_dirs = "" + doxy_input = "dir1 dir2" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + assert result == doxy_input + + +def test_merge_doxygen_input_empty_doxy_input(tmp_path) -> None: + # When doxy_input is empty, the function should return src_dirs unchanged. + src_dirs = "dir1 dir2" + doxy_input = "" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + assert result == src_dirs + + +def test_merge_doxygen_input_both_non_empty(tmp_path) -> None: + # When both src_dirs and doxy_input are provided, the result should contain all unique paths. + src_dirs = "a b" + doxy_input = "b c" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + # The returned string is a space-separated list of paths relative to run_folder. + # We compare as sets because the order may not be predictable. + result_set = set(result.split()) + expected_set = {"a", "b", "c"} + assert result_set == expected_set + + +def test_merge_doxygen_input_duplicates(tmp_path) -> None: + # Duplicate paths in the inputs should be deduplicated. + src_dirs = "a a" + doxy_input = "a" + run_folder = tmp_path / "run" + run_folder.mkdir() + + result = DoxygenGenerator.merge_doxygen_input(src_dirs, doxy_input, run_folder) + result_list = result.split() + # Expect only one occurrence of "a" + assert result_list == ["a"] diff --git a/tests/migration/data/1_expect.yaml b/tests/migration/data/1_expect.yaml new file mode 100644 index 00000000..7015437d --- /dev/null +++ b/tests/migration/data/1_expect.yaml @@ -0,0 +1,215 @@ +site_name: MkDoxy Demo +site_url: https://mkdoxy-demo.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + This is a demo of MkDoxy, a tool for generating Doxygen documentation from Markdown files. + +# Repository +repo_name: JakubAndrysek/MkDoxy-demo +repo_url: https://github.com/JakubAndrysek/MkDoxy-demo + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + - navigation.instant + - navigation.tracking + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: amber + accent: amber + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: amber + accent: amber + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-6VB0GPP3MT + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - glightbox + - open-in-new-tab + - mkdoxy: + projects: + esp: + src_dirs: demo-projets/esp + full_doc: True + custom_template_dir: templates-custom + # Example of custom template: https://mkdoxy-demo.kubaandrysek.cz/esp/annotated/ + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + animal: + src_dirs: demo-projets/animal + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + stm: + src_dirs: demo-projets/stm32 + full_doc: True + + jaculus: + src_dirs: demo-projets/jaculus/main demo-projets/jaculus/managed_components/jac-link demo-projets/jaculus/managed_components/jac-machine + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + + custom_api_folder: .mkdoxy + full_doc: True + debug: False + ignore_errors: False + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + +nav: + - "Home": "README.md" + - useage.md + - API Demo: + - api/index.md + - ESP-32: + - esp/index.md + - "Links": "esp/links.md" + - "Classes": + - "Class List": "esp/annotated.md" + - "Class Index": "esp/classes.md" + - "Class Hierarchy": "esp/hierarchy.md" + - "Class Members": "esp/class_members.md" + - "Class Member Functions": "esp/class_member_functions.md" + - "Class Member Variables": "esp/class_member_variables.md" + - "Class Member Typedefs": "esp/class_member_typedefs.md" + - "Class Member Enumerations": "esp/class_member_enums.md" + - "Namespaces": + - "Namespace List": "esp/namespaces.md" + - "Namespace Members": "esp/namespace_members.md" + - "Namespace Member Functions": "esp/namespace_member_functions.md" + - "Namespace Member Variables": "esp/namespace_member_variables.md" + - "Namespace Member Typedefs": "esp/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "esp/namespace_member_enums.md" + - "Functions": "esp/functions.md" + - "Variables": "esp/variables.md" + - "Macros": "esp/macros.md" + - "Files": "esp/files.md" + - STM-32: + - stm32/index.md + - "Links": "stm/links.md" + - "Classes": + - "Class List": "stm/annotated.md" + - "Class Index": "stm/classes.md" + - "Class Hierarchy": "stm/hierarchy.md" + - "Class Members": "stm/class_members.md" + - "Class Member Functions": "stm/class_member_functions.md" + - "Class Member Variables": "stm/class_member_variables.md" + - "Class Member Typedefs": "stm/class_member_typedefs.md" + - "Class Member Enumerations": "stm/class_member_enums.md" + - "Namespaces": + - "Namespace List": "stm/namespaces.md" + - "Namespace Members": "stm/namespace_members.md" + - "Namespace Member Functions": "stm/namespace_member_functions.md" + - "Namespace Member Variables": "stm/namespace_member_variables.md" + - "Namespace Member Typedefs": "stm/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "stm/namespace_member_enums.md" + - "Functions": "stm/functions.md" + - "Variables": "stm/variables.md" + - "Macros": "stm/macros.md" + - "Files": "stm/files.md" + - Animal: + - animal/index.md + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Jaculus: + - jaculus/index.md + - "Links": "jaculus/links.md" + - "Classes": + - "Class List": "jaculus/annotated.md" + - "Class Index": "jaculus/classes.md" + - "Class Hierarchy": "jaculus/hierarchy.md" + - "Class Members": "jaculus/class_members.md" + - "Class Member Functions": "jaculus/class_member_functions.md" + - "Class Member Variables": "jaculus/class_member_variables.md" + - "Class Member Typedefs": "jaculus/class_member_typedefs.md" + - "Class Member Enumerations": "jaculus/class_member_enums.md" + - "Namespaces": + - "Namespace List": "jaculus/namespaces.md" + - "Namespace Members": "jaculus/namespace_members.md" + - "Namespace Member Functions": "jaculus/namespace_member_functions.md" + - "Namespace Member Variables": "jaculus/namespace_member_variables.md" + - "Namespace Member Typedefs": "jaculus/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "jaculus/namespace_member_enums.md" + - "Functions": "jaculus/functions.md" + - "Variables": "jaculus/variables.md" + - "Macros": "jaculus/macros.md" + - "Files": "jaculus/files.md" diff --git a/tests/migration/data/1_old.yaml b/tests/migration/data/1_old.yaml new file mode 100644 index 00000000..84e0dc23 --- /dev/null +++ b/tests/migration/data/1_old.yaml @@ -0,0 +1,215 @@ +site_name: MkDoxy Demo +site_url: https://mkdoxy-demo.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + This is a demo of MkDoxy, a tool for generating Doxygen documentation from Markdown files. + +# Repository +repo_name: JakubAndrysek/MkDoxy-demo +repo_url: https://github.com/JakubAndrysek/MkDoxy-demo + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + - navigation.instant + - navigation.tracking + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: amber + accent: amber + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: amber + accent: amber + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-6VB0GPP3MT + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - glightbox + - open-in-new-tab + - mkdoxy: + projects: + esp: + src-dirs: demo-projets/esp + full-doc: True + template-dir: templates-custom + # Example of custom template: https://mkdoxy-demo.kubaandrysek.cz/esp/annotated/ + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + animal: + src-dirs: demo-projets/animal + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + stm: + src-dirs: demo-projets/stm32 + full-doc: True + + jaculus: + src-dirs: demo-projets/jaculus/main demo-projets/jaculus/managed_components/jac-link demo-projets/jaculus/managed_components/jac-machine + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + + save-api: .mkdoxy + full-doc: True + debug: False + ignore-errors: False + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + +nav: + - "Home": "README.md" + - useage.md + - API Demo: + - api/index.md + - ESP-32: + - esp/index.md + - "Links": "esp/links.md" + - "Classes": + - "Class List": "esp/annotated.md" + - "Class Index": "esp/classes.md" + - "Class Hierarchy": "esp/hierarchy.md" + - "Class Members": "esp/class_members.md" + - "Class Member Functions": "esp/class_member_functions.md" + - "Class Member Variables": "esp/class_member_variables.md" + - "Class Member Typedefs": "esp/class_member_typedefs.md" + - "Class Member Enumerations": "esp/class_member_enums.md" + - "Namespaces": + - "Namespace List": "esp/namespaces.md" + - "Namespace Members": "esp/namespace_members.md" + - "Namespace Member Functions": "esp/namespace_member_functions.md" + - "Namespace Member Variables": "esp/namespace_member_variables.md" + - "Namespace Member Typedefs": "esp/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "esp/namespace_member_enums.md" + - "Functions": "esp/functions.md" + - "Variables": "esp/variables.md" + - "Macros": "esp/macros.md" + - "Files": "esp/files.md" + - STM-32: + - stm32/index.md + - "Links": "stm/links.md" + - "Classes": + - "Class List": "stm/annotated.md" + - "Class Index": "stm/classes.md" + - "Class Hierarchy": "stm/hierarchy.md" + - "Class Members": "stm/class_members.md" + - "Class Member Functions": "stm/class_member_functions.md" + - "Class Member Variables": "stm/class_member_variables.md" + - "Class Member Typedefs": "stm/class_member_typedefs.md" + - "Class Member Enumerations": "stm/class_member_enums.md" + - "Namespaces": + - "Namespace List": "stm/namespaces.md" + - "Namespace Members": "stm/namespace_members.md" + - "Namespace Member Functions": "stm/namespace_member_functions.md" + - "Namespace Member Variables": "stm/namespace_member_variables.md" + - "Namespace Member Typedefs": "stm/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "stm/namespace_member_enums.md" + - "Functions": "stm/functions.md" + - "Variables": "stm/variables.md" + - "Macros": "stm/macros.md" + - "Files": "stm/files.md" + - Animal: + - animal/index.md + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Jaculus: + - jaculus/index.md + - "Links": "jaculus/links.md" + - "Classes": + - "Class List": "jaculus/annotated.md" + - "Class Index": "jaculus/classes.md" + - "Class Hierarchy": "jaculus/hierarchy.md" + - "Class Members": "jaculus/class_members.md" + - "Class Member Functions": "jaculus/class_member_functions.md" + - "Class Member Variables": "jaculus/class_member_variables.md" + - "Class Member Typedefs": "jaculus/class_member_typedefs.md" + - "Class Member Enumerations": "jaculus/class_member_enums.md" + - "Namespaces": + - "Namespace List": "jaculus/namespaces.md" + - "Namespace Members": "jaculus/namespace_members.md" + - "Namespace Member Functions": "jaculus/namespace_member_functions.md" + - "Namespace Member Variables": "jaculus/namespace_member_variables.md" + - "Namespace Member Typedefs": "jaculus/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "jaculus/namespace_member_enums.md" + - "Functions": "jaculus/functions.md" + - "Variables": "jaculus/variables.md" + - "Macros": "jaculus/macros.md" + - "Files": "jaculus/files.md" diff --git a/tests/migration/data/2_expect.yaml b/tests/migration/data/2_expect.yaml new file mode 100644 index 00000000..f2ac4049 --- /dev/null +++ b/tests/migration/data/2_expect.yaml @@ -0,0 +1,208 @@ +site_name: MkDoxy +site_url: https://mkdoxy.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + MkDoxy -> MkDocs + Doxygen. Easy documentation generator with code snippets. + +# Repository +repo_name: JakubAndrysek/MkDoxy/ +repo_url: https://github.com/JakubAndrysek/MkDoxy/ + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: orange + accent: orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: orange + accent: orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-8WHJ2N4SHC + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - open-in-new-tab + - mkdoxy: + enabled: !ENV [ENABLE_MKDOXY, True] + projects: + mkdoxyApi: + src_dirs: mkdoxy + full_doc: True + custom_template_dir: templates-custom + doxy_config_file: demo-projects/animal/Doxyfile + doxy_config_dict: + FILE_PATTERNS: "*.py" + EXAMPLE_PATH: "" + RECURSIVE: True + OPTIMIZE_OUTPUT_JAVA: True + JAVADOC_AUTOBRIEF: True + EXTRACT_ALL: True + animal: + src_dirs: demo-projects/animal + full_doc: True + doxy_config_dict: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + custom_api_folder: .mkdoxy + full_doc: True + debug: False + ignore_errors: False + + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - def_list + - toc: + permalink: True + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences + - markdown.extensions.md_in_html + - pymdownx.snippets: + check_paths: true + - pymdownx.blocks.admonition: + types: + - new + - settings + - note + - abstract + - info + - tip + - success + - question + - warning + - failure + - danger + - bug + - example + - quote + - pymdownx.blocks.details: + - pymdownx.blocks.html: + - pymdownx.blocks.definition: + - pymdownx.blocks.tab: + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: + - "README.md" + - "Changelog": "changelog.md" + - "License": "license.md" + - "Usage": + - "usage/index.md" + - "Advanced Usage": "usage/advanced.md" + + - Snippets: + - "snippets/index.md" + - "Intelli sense and errors": "snippets/intelli_sense_and_errors.md" + - "Classes": "snippets/classes.md" + - "Source code": "snippets/source_code.md" + - "Links": "snippets/links.md" + - "Functions": "snippets/functions.md" + - "Namespaces": "snippets/namespaces.md" + - "Files": "snippets/files.md" + - MkDoxy API: + - "mkdoxyApi/index.md" + - "Links": "mkdoxyApi/links.md" + - "Classes": + - "Class List": "mkdoxyApi/annotated.md" + - "Class Index": "mkdoxyApi/classes.md" + - "Class Hierarchy": "mkdoxyApi/hierarchy.md" + - "Class Members": "mkdoxyApi/class_members.md" + - "Class Member Functions": "mkdoxyApi/class_member_functions.md" + - "Class Member Variables": "mkdoxyApi/class_member_variables.md" + - "Class Member Typedefs": "mkdoxyApi/class_member_typedefs.md" + - "Class Member Enumerations": "mkdoxyApi/class_member_enums.md" + - "Namespaces": + - "Namespace List": "mkdoxyApi/namespaces.md" + - "Namespace Members": "mkdoxyApi/namespace_members.md" + - "Namespace Member Functions": "mkdoxyApi/namespace_member_functions.md" + - "Namespace Member Variables": "mkdoxyApi/namespace_member_variables.md" + - "Namespace Member Typedefs": "mkdoxyApi/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "mkdoxyApi/namespace_member_enums.md" + - "Functions": "mkdoxyApi/functions.md" + - "Variables": "mkdoxyApi/variables.md" + - "Macros": "mkdoxyApi/macros.md" + - "Files": "mkdoxyApi/files.md" + - Demo API: + - "animal/index.md" + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Advanced Demo: "https://mkdoxy-demo.kubaandrysek.cz/" diff --git a/tests/migration/data/2_old.yaml b/tests/migration/data/2_old.yaml new file mode 100644 index 00000000..596b3541 --- /dev/null +++ b/tests/migration/data/2_old.yaml @@ -0,0 +1,208 @@ +site_name: MkDoxy +site_url: https://mkdoxy.kubaandrysek.cz/ +site_author: Jakub Andrýsek +site_description: >- + MkDoxy -> MkDocs + Doxygen. Easy documentation generator with code snippets. + +# Repository +repo_name: JakubAndrysek/MkDoxy/ +repo_url: https://github.com/JakubAndrysek/MkDoxy/ + +# Copyright +copyright: Copyright © 2023 Jakub Andrýsek + +theme: + name: material + language: en + logo: assets/logo.png + favicon: assets/logo.png + features: + - navigation.tabs + - navigation.indexes + - navigation.top + + icon: + repo: fontawesome/brands/github + + palette: + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: orange + accent: orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: orange + accent: orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/JakubAndrysek + - icon: fontawesome/brands/twitter + link: https://twitter.com/KubaAndrysek + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/jakub-andrysek/ + analytics: + provider: google + property: G-8WHJ2N4SHC + feedback: + title: Was this page helpful? + ratings: + - icon: material/emoticon-happy-outline + name: This page was helpful + data: 1 + note: >- + Thanks for your feedback! + - icon: material/emoticon-sad-outline + name: This page could be improved + data: 0 + note: >- + Thanks for your feedback! + +use_directory_urls: True +#use_directory_urls: False + +plugins: + - search + - open-in-new-tab + - mkdoxy: + enabled: !ENV [ENABLE_MKDOXY, True] + projects: + mkdoxyApi: + src-dirs: mkdoxy + full-doc: True + template-dir: templates-custom + doxy-cfg-file: demo-projects/animal/Doxyfile + doxy-cfg: + FILE_PATTERNS: "*.py" + EXAMPLE_PATH: "" + RECURSIVE: True + OPTIMIZE_OUTPUT_JAVA: True + JAVADOC_AUTOBRIEF: True + EXTRACT_ALL: True + animal: + src-dirs: demo-projects/animal + full-doc: True + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + EXAMPLE_PATH: examples + RECURSIVE: True + save-api: .mkdoxy + full-doc: True + debug: False + ignore-errors: False + + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - def_list + - toc: + permalink: True + - pymdownx.superfences + - admonition + - pymdownx.details + - pymdownx.superfences + - markdown.extensions.md_in_html + - pymdownx.snippets: + check_paths: true + - pymdownx.blocks.admonition: + types: + - new + - settings + - note + - abstract + - info + - tip + - success + - question + - warning + - failure + - danger + - bug + - example + - quote + - pymdownx.blocks.details: + - pymdownx.blocks.html: + - pymdownx.blocks.definition: + - pymdownx.blocks.tab: + - pymdownx.tabbed: + alternate_style: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +nav: + - Home: + - "README.md" + - "Changelog": "changelog.md" + - "License": "license.md" + - "Usage": + - "usage/index.md" + - "Advanced Usage": "usage/advanced.md" + + - Snippets: + - "snippets/index.md" + - "Intelli sense and errors": "snippets/intelli_sense_and_errors.md" + - "Classes": "snippets/classes.md" + - "Source code": "snippets/source_code.md" + - "Links": "snippets/links.md" + - "Functions": "snippets/functions.md" + - "Namespaces": "snippets/namespaces.md" + - "Files": "snippets/files.md" + - MkDoxy API: + - "mkdoxyApi/index.md" + - "Links": "mkdoxyApi/links.md" + - "Classes": + - "Class List": "mkdoxyApi/annotated.md" + - "Class Index": "mkdoxyApi/classes.md" + - "Class Hierarchy": "mkdoxyApi/hierarchy.md" + - "Class Members": "mkdoxyApi/class_members.md" + - "Class Member Functions": "mkdoxyApi/class_member_functions.md" + - "Class Member Variables": "mkdoxyApi/class_member_variables.md" + - "Class Member Typedefs": "mkdoxyApi/class_member_typedefs.md" + - "Class Member Enumerations": "mkdoxyApi/class_member_enums.md" + - "Namespaces": + - "Namespace List": "mkdoxyApi/namespaces.md" + - "Namespace Members": "mkdoxyApi/namespace_members.md" + - "Namespace Member Functions": "mkdoxyApi/namespace_member_functions.md" + - "Namespace Member Variables": "mkdoxyApi/namespace_member_variables.md" + - "Namespace Member Typedefs": "mkdoxyApi/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "mkdoxyApi/namespace_member_enums.md" + - "Functions": "mkdoxyApi/functions.md" + - "Variables": "mkdoxyApi/variables.md" + - "Macros": "mkdoxyApi/macros.md" + - "Files": "mkdoxyApi/files.md" + - Demo API: + - "animal/index.md" + - "Links": "animal/links.md" + - "Classes": + - "Class List": "animal/annotated.md" + - "Class Index": "animal/classes.md" + - "Class Hierarchy": "animal/hierarchy.md" + - "Class Members": "animal/class_members.md" + - "Class Member Functions": "animal/class_member_functions.md" + - "Class Member Variables": "animal/class_member_variables.md" + - "Class Member Typedefs": "animal/class_member_typedefs.md" + - "Class Member Enumerations": "animal/class_member_enums.md" + - "Namespaces": + - "Namespace List": "animal/namespaces.md" + - "Namespace Members": "animal/namespace_members.md" + - "Namespace Member Functions": "animal/namespace_member_functions.md" + - "Namespace Member Variables": "animal/namespace_member_variables.md" + - "Namespace Member Typedefs": "animal/namespace_member_typedefs.md" + - "Namespace Member Enumerations": "animal/namespace_member_enums.md" + - "Functions": "animal/functions.md" + - "Variables": "animal/variables.md" + - "Macros": "animal/macros.md" + - "Files": "animal/files.md" + - Advanced Demo: "https://mkdoxy-demo.kubaandrysek.cz/" diff --git a/tests/migration/test_migration.py b/tests/migration/test_migration.py new file mode 100644 index 00000000..45362192 --- /dev/null +++ b/tests/migration/test_migration.py @@ -0,0 +1,58 @@ +import shutil +from pathlib import Path + +import pytest + +from mkdoxy.migration import update_new_config + +# Directory containing test data files. +DATA_DIR = Path(__file__).parent / "data" + + +@pytest.fixture(params=["1", "2"]) +def migration_files(request, tmp_path: Path) -> tuple: + """ + Parameterized fixture that copies the legacy YAML file (_old.yaml) + to a temporary file and loads the expected file text from _expect.yaml. + + :returns: A tuple (old_yaml_path, expected_text, prefix) + """ + prefix = request.param + # Copy legacy file to a temporary file. + src = DATA_DIR / f"{prefix}_old.yaml" + dst = tmp_path / f"test_{prefix}.yaml" + shutil.copy(src, dst) + + # Load expected configuration text (without parsing YAML). + expected_text = (DATA_DIR / f"{prefix}_expect.yaml").read_text(encoding="utf-8") + return dst, expected_text, prefix + + +def test_migration_without_backup(migration_files) -> None: + """ + Test that migration updates the legacy configuration correctly without creating a backup. + """ + old_yaml, expected_text, prefix = migration_files + # Run migration with backup turned off. + update_new_config(old_yaml, backup=False, backup_file_name="backup.yaml") + + updated_text = old_yaml.read_text(encoding="utf-8") + assert updated_text == expected_text, f"Test case {prefix} failed: output text does not match expected." + + +def test_migration_with_backup(migration_files) -> None: + """ + Test that migration creates a backup file and updates the configuration correctly. + """ + old_yaml, expected_text, prefix = migration_files + backup_file_name = "backup.yaml" + + # Run migration with backup enabled. + update_new_config(old_yaml, backup=True, backup_file_name=backup_file_name) + + # Verify that the backup file was created. + backup_file = old_yaml.parent / backup_file_name + assert backup_file.exists(), f"Test case {prefix}: Backup file was not created." + + updated_text = old_yaml.read_text(encoding="utf-8") + assert updated_text == expected_text, f"Test case {prefix} failed: output text does not match expected."