YouTubeDownloader v1.1.2
YouTube content downloader
Loading...
Searching...
No Matches
doxygen_creator.py
Go to the documentation of this file.
1"""!
2********************************************************************************
3@file doxygen_creator.py
4@brief create doxygen documentation
5********************************************************************************
6"""
7
8import os
9import logging
10import difflib
11import subprocess
12import webbrowser
13import time
14import argparse
15import zipfile
16from typing import Any, Optional
17from threading import Thread
18from difflib import get_close_matches
19import requests
20import packaging.version
21
22from Documentation.DoxygenCreator.configParser import ConfigParser
23
24B_PLANTUML_SUPPORT = True
25B_GITHUB_CORNER_SUPPORT = True
26B_DOXY_CONFIG_DIFF_SUPPORT = True
27B_DOXY_PY_CHECKER_SUPPORT = True
28B_AUTO_VERSION_SUPPORT = True
29B_FOOTER_SUPPORT = False
30
31if B_DOXY_PY_CHECKER_SUPPORT:
32 from Documentation.DoxygenCreator.doxy_py_checker import DoxyPyChecker # pylint: disable=wrong-import-position
33
34log = logging.getLogger("DoxygenCreator")
35
36YES = "YES"
37NO = "NO"
38WARNING_FAIL = "FAIL_ON_WARNINGS"
39
40S_DOXYGEN_PATH = "doxygen.exe" # required: add doxygen bin path to file path in system variables
41S_DEFAULT_OUTPUT_FOLDER = "Output_Doxygen"
42
43S_MAIN_FOLDER_FOLDER = "../../"
44
45DOXYGEN_VERSION = "1.12.0"
46S_DOXYGEN_URL = f"https://sourceforge.net/projects/doxygen/files/rel-{DOXYGEN_VERSION}/doxygen-{DOXYGEN_VERSION}.windows.x64.bin.zip/download"
47S_DOXYGEN_ZIP = f"doxygen-{DOXYGEN_VERSION}.windows.x64.bin.zip"
48S_DOXYGEN_DLL = "libclang.dll"
49
50S_WARNING_FILE_PREFIX = "Doxygen_warnings_"
51S_WARNING_FILE_SUFFIX = ".log"
52S_INDEX_FILE = "html/index.html"
53I_TIMEOUT = 5 # timeout for tool download
54
55S_PYTHON_PATTERN = "*.py"
56L_DEFAULT_FILE_PATTERN: list[str] = []
57
58if B_PLANTUML_SUPPORT:
59 PLANT_UML_VERSION = "1.2024.7"
60 S_PLANTUML_JAR_URL = f"https://github.com/plantuml/plantuml/releases/download/v{PLANT_UML_VERSION}/plantuml-{PLANT_UML_VERSION}.jar"
61 S_PLANTUML_JAR_NAME = "plantuml.jar"
62 S_PLANTUML_PATH = "./" # need plantuml.jar in this folder
63else:
64 S_PLANTUML_PATH = ""
65
66if B_DOXY_CONFIG_DIFF_SUPPORT:
67 S_DOXY_DIFF_HTML_NAME = "DoxyfileDiff.html"
68 S_DOXY_FILE_DEFAULT_NAME = "Default.Doxyfile"
69 I_WRAP_LENGTH = 100
70
71if B_GITHUB_CORNER_SUPPORT:
72 S_GITHUB_CORNER_FIRST = "<a href="
73 S_GITHUB_CORNER_LAST = """ class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>"""
74
75
76class OpenNotepad(Thread):
77 """!
78 @brief Class to open Notepad in thread to prevent program stop until close file
79 @param s_file : file to open
80 """
81
82 def __init__(self, s_file: str):
83 Thread.__init__(self)
84 self.s_file = s_file
85 self.start()
86
87 def run(self) -> None:
88 """!
89 @brief Open file with notepad
90 """
91 time.sleep(1)
92 with subprocess.Popen(["notepad.exe", self.s_file]):
93 pass
94
95
97 """!
98 @brief Class to generate Doxygen documentation for any code documentation with uniform settings and styling.
99 @param s_webside : URL to website
100 """
101 d_settings: dict[str, str | list[str] | int] = {
102 "PROJECT_NAME": "MyProject", # important to define default user name to create output folder and files
103 "OUTPUT_DIRECTORY": S_DEFAULT_OUTPUT_FOLDER,
104 "ABBREVIATE_BRIEF": "", # Each string in this list, will be stripped from the text ("" to prevent warning)
105 "FULL_PATH_NAMES": NO,
106 "JAVADOC_AUTOBRIEF": YES, # first line (until the first dot) of a Javadoc-style comment as the brief description
107 "OPTIMIZE_OUTPUT_JAVA": YES,
108 "TIMESTAMP": YES,
109 "EXTRACT_ALL": YES,
110 "INTERNAL_DOCS": YES, # documentation after internal command is included
111 "HIDE_SCOPE_NAMES": YES, # full class and namespace scopes in the documentation
112 "SORT_BRIEF_DOCS": YES, # sort the brief descriptions of file namespace and class members alphabetically by member name
113 "SORT_BY_SCOPE_NAME": YES,
114 "WARN_NO_PARAMDOC": YES,
115 "WARN_AS_ERROR": WARNING_FAIL,
116 "INPUT": ["."],
117 "FILE_PATTERNS": L_DEFAULT_FILE_PATTERN,
118 "RECURSIVE": YES,
119 "IMAGE_PATH": S_MAIN_FOLDER_FOLDER,
120 "USE_MDFILE_AS_MAINPAGE": f"{S_MAIN_FOLDER_FOLDER}README.md",
121 "SOURCE_BROWSER": YES,
122 "INLINE_SOURCES": YES,
123 "HTML_COLORSTYLE": "LIGHT", # required with Doxygen >= 1.9.5
124 "HTML_COLORSTYLE_HUE": 209, # required for Doxygen Awesome
125 "HTML_COLORSTYLE_SAT": 255, # required for Doxygen Awesome
126 "HTML_COLORSTYLE_GAMMA": 113, # required for Doxygen Awesome
127 "HTML_DYNAMIC_SECTIONS": YES,
128 "HTML_COPY_CLIPBOARD": NO, # required for Doxygen Awesome
129 "DISABLE_INDEX": NO, # required for Doxygen Awesome
130 "GENERATE_TREEVIEW": YES, # required for Doxygen Awesome
131 "FULL_SIDEBAR": NO, # required for Doxygen Awesome
132 "SEARCHENGINE": YES,
133 "SERVER_BASED_SEARCH": NO,
134 "EXTERNAL_SEARCH": NO,
135 "GENERATE_LATEX": NO,
136 "LATEX_CMD_NAME": "latex",
137 "UML_LOOK": YES,
138 "DOT_IMAGE_FORMAT": "svg",
139 "INTERACTIVE_SVG": YES,
140 "PLANTUML_JAR_PATH": S_PLANTUML_PATH,
141 "PLANTUML_INCLUDE_PATH": S_PLANTUML_PATH,
142 "DOT_MULTI_TARGETS": YES,
143 "DOT_CLEANUP": YES,
144 }
145
146 def __init__(self, s_webside: Optional[str] = None) -> None:
147 self.s_webside = s_webside
148 self.l_warnings: list[str] = []
152
153 def create_default_doxyfile(self, s_file_name: str) -> None:
154 """!
155 @brief Create default doxyfile
156 @param s_file_name : doxygen file name
157 """
158 subprocess.call([S_DOXYGEN_PATH, "-g", s_file_name])
159
160 def set_configuration(self, s_type: str, value: Any, b_override: bool = True) -> None:
161 """!
162 @brief Set doxygen configuration.
163 @param s_type : type to set in configuration
164 @param value : value to set for s_type in configuration
165 @param b_override : info if selected default setting should override
166 """
167 if isinstance(value, list):
168 if b_override or (s_type not in self.d_settings):
169 self.d_settings[s_type] = []
170 list_to_extend = self.d_settings[s_type] # Now mypy knows this is a list
171 if isinstance(list_to_extend, list):
172 list_to_extend.extend(value)
173 else:
174 log.warning("Not possible to append value to %s", s_type)
175 else:
176 if b_override or (s_type not in self.d_settings):
177 self.d_settings[s_type] = value
178
179 def get_configuration(self, s_type: str) -> Any:
180 """!
181 @brief Get type in doxygen configuration.
182 @param s_type : type in configuration get setting
183 @return configuration settings
184 """
185 if s_type in self.d_settings:
186 value = self.d_settings[s_type]
187 else:
188 value = None # is default value
189 return value
190
192 """!
193 @brief Prepare doxfile configuration with default settings that depend on other parameters.
194 Parameters that set before as fix parameter or by user will not override.
195 This allows the user to make settings that differ from the default configuration.
196 """
197 # set filenames and directories needed to save/open files
198 self.s_output_dirs_output_dir = self.get_configuration('OUTPUT_DIRECTORY') # save output directory witch used for other file names
199 s_project_name = self.get_configuration("PROJECT_NAME")
200 self.s_doxyfile_name = os.path.join(self.s_output_dirs_output_dir, f"{s_project_name}.Doxyfile")
201 self.s_warning_names_warning_name = os.path.join(self.s_output_dirs_output_dir, f"{S_WARNING_FILE_PREFIX}{s_project_name}{S_WARNING_FILE_SUFFIX}")
202
203 # set uniform doxygen configuration that depend on other parameters
204 self.set_configuration("PROJECT_BRIEF", f"{s_project_name}-Documentation", b_override=False) # use project name as default if nothing defined
205 self.set_configuration("WARN_LOGFILE", self.s_warning_names_warning_name, b_override=False)
206 self.set_configuration("EXCLUDE_PATTERNS", [self.s_output_dirs_output_dir], b_override=False)
207
208 if B_AUTO_VERSION_SUPPORT:
209 # write version prefix for version numbers as project number
210 s_version = self.get_configuration("PROJECT_NUMBER")
211 if s_version:
212 try:
213 packaging.version.Version(s_version) # test if s_sersion is a valid versions string
214 self.set_configuration("PROJECT_NUMBER", f"v{s_version}")
215 except packaging.version.InvalidVersion:
216 pass # not a valid version, do not prefix with "v"
217
218 # set doxygen awesome settings witch depends on path
219 l_extra_files = ["doxygen-awesome-darkmode-toggle.js",
220 "doxygen-awesome-fragment-copy-button.js",
221 "doxygen-awesome-paragraph-link.js",
222 "doxygen-awesome-interactive-toc.js",
223 "doxygen-awesome-tabs.js"]
224 self.set_configuration("HTML_EXTRA_FILES", l_extra_files, b_override=False)
225 l_extra_stylesheet = ["doxygen-awesome.css",
226 "doxygen-awesome-sidebar-only.css",
227 "doxygen-awesome-sidebar-only-darkmode-toggle.css"]
228 self.set_configuration("HTML_EXTRA_STYLESHEET", l_extra_stylesheet, b_override=False) # doxygen styling code
229 self.set_configuration("HTML_HEADER", "header.html", b_override=False) # use own doxygen header
230 if B_FOOTER_SUPPORT:
231 self.set_configuration("HTML_FOOTER", "footer.html", b_override=False) # use own doxygen footer
232
234 """!
235 @brief Override default settings in doxyfile with selected settings.
236 """
237 # load the default doxygen template file
238 config_parser = ConfigParser()
239 configuration = config_parser.load_configuration(self.s_doxyfile_name)
240 l_existing_keys = list(configuration.keys())
241
242 # write doxygen settings
243 for key, value in self.d_settings.items():
244 if key in l_existing_keys:
245 if isinstance(value, list):
246 new_values = []
247 for entry in value:
248 new_values.append(entry)
249 configuration[key] = new_values
250 else:
251 if isinstance(value, int):
252 value = str(value) # integer can only write as string to doxyfile
253 configuration[key] = value
254 else:
255 text = f"'{key}' is a invalid doxygen setting"
256 similar_key = get_close_matches(key, l_existing_keys, n=1)
257 if len(similar_key) > 0:
258 text += f". Did you mean: '{similar_key[0]}'?"
259 self.l_warnings.append(text)
260
261 # store the configuration in doxyfile
262 config_parser.store_configuration(configuration, self.s_doxyfile_name)
263
264 def add_warnings(self) -> None:
265 """!
266 @brief Add warnings to warning file
267 """
268 if self.l_warnings:
269 with open(self.s_warning_names_warning_name, mode="a", encoding="utf-8") as file:
270 for s_warning in self.l_warnings:
271 file.write(s_warning + "\n")
272
273 if B_PLANTUML_SUPPORT:
274 def download_plantuml_jar(self) -> None:
275 """!
276 @brief Download PlantUML Jar
277 """
278 if not os.path.exists(S_PLANTUML_JAR_NAME):
279 log.info("Download %s ...", S_PLANTUML_JAR_NAME)
280 try:
281 with requests.get(S_PLANTUML_JAR_URL, timeout=I_TIMEOUT) as response:
282 response.raise_for_status()
283 with open(S_PLANTUML_JAR_NAME, mode="wb") as file: # download plantuml.jar
284 file.write(response.content) # download plantuml.jar
285 except requests.Timeout:
286 log.error("Timeout for download %s!", S_PLANTUML_JAR_NAME)
287 except requests.RequestException as e:
288 log.error("Can not download %s! %s", S_PLANTUML_JAR_NAME, e)
289 else:
290 log.info("%s already exist!", S_PLANTUML_JAR_NAME)
291
292 def download_doxygen(self) -> None:
293 """!
294 @brief Download Doxygen
295 """
296 if not os.path.exists(S_DOXYGEN_PATH) or not os.path.exists(S_DOXYGEN_DLL):
297 if not os.path.exists(S_DOXYGEN_ZIP):
298 log.info("Download %s ...", S_DOXYGEN_ZIP)
299 try:
300 with requests.get(S_DOXYGEN_URL, timeout=I_TIMEOUT) as response:
301 response.raise_for_status()
302 with open(S_DOXYGEN_ZIP, mode="wb") as file: # download doxygen
303 file.write(response.content)
304 except requests.Timeout:
305 log.error("Timeout for download %s!", S_DOXYGEN_ZIP)
306 except requests.RequestException as e:
307 log.error("Can not download %s! %s", S_DOXYGEN_ZIP, e)
308 else:
309 log.info("%s already exist!", S_DOXYGEN_ZIP)
310 with zipfile.ZipFile(S_DOXYGEN_ZIP, mode="r") as zip_ref:
311 zip_ref.extract(S_DOXYGEN_PATH, "./")
312 zip_ref.extract(S_DOXYGEN_DLL, "./")
313 else:
314 log.info("%s and %s already exist!", S_DOXYGEN_PATH, S_DOXYGEN_DLL)
315
316 def check_doxygen_warnings(self, b_open_warning_file: bool = True) -> bool:
317 """!
318 @brief Check for doxygen Warnings
319 @param b_open_warning_file : [True] open warning file; [False] only check for warnings
320 @return status if doxygen warnings exist
321 """
322 b_warnings = False
323
324 if os.path.exists(self.s_warning_names_warning_name):
325 with open(self.s_warning_names_warning_name, mode="r", encoding="utf-8") as file:
326 lines = file.readlines()
327 if len(lines) != 0:
328 b_warnings = True
329 log.warning("Doxygen Warnings found!!!")
330 for s_line in lines:
331 log.warning(" %s", s_line)
332
333 if b_open_warning_file and b_warnings:
335
336 return b_warnings
337
338 def generate_doxygen_output(self, b_open_doxygen_output: bool = True) -> None:
339 """!
340 @brief Generate Doxygen output depend on existing doxyfile
341 @param b_open_doxygen_output : [True] open output in browser; [False] only generate output
342 """
343 subprocess.call([S_DOXYGEN_PATH, self.s_doxyfile_name])
344
345 if b_open_doxygen_output:
346 # open doxygen output
347 filename = f"file:///{os.getcwd()}/{self.s_output_dir}/{S_INDEX_FILE}"
348 webbrowser.open_new_tab(filename)
349
350 if B_GITHUB_CORNER_SUPPORT:
351 def add_github_corner(self) -> None:
352 """!
353 @brief Add Github corner and tab icon
354 """
355 s_folder = f"{self.s_output_dir}/html/"
356 if self.s_webside is not None:
357 s_corner_text = S_GITHUB_CORNER_FIRST + self.s_webside + S_GITHUB_CORNER_LAST
358 else:
359 s_corner_text = None
360 for html_file in os.listdir(s_folder):
361 if html_file.endswith(".html"):
362 file_path = f"{s_folder}{html_file}"
363 with open(file_path, mode="a", encoding="utf-8") as file:
364 if s_corner_text is not None:
365 file.write(s_corner_text + "\n")
366
367 def add_nojekyll_file(self) -> None:
368 """!
369 @brief Add .nojekyll file that files with underscores visible
370 """
371 s_file_name = ".nojekyll"
372 with open(f"{self.s_output_dir}/html/{s_file_name}", mode="w", encoding="utf-8") as file:
373 file.write("")
374
375 if B_DOXY_CONFIG_DIFF_SUPPORT:
377 """!
378 @brief Generate doxyfile diff to view changes
379 """
380 s_default_doxyfile = os.path.join(self.s_output_dirs_output_dir, S_DOXY_FILE_DEFAULT_NAME)
381
382 # create and read default doxyfile
383 self.create_default_doxyfile(s_default_doxyfile)
384 config_parser = ConfigParser()
385 configuration = config_parser.load_configuration(s_default_doxyfile)
386 config_parser.store_configuration(configuration, s_default_doxyfile)
387 with open(s_default_doxyfile, mode="r", encoding="utf-8") as file:
388 s_default_config = file.read()
389 os.remove(s_default_doxyfile)
390
391 # read modified doxyfile
392 with open(self.s_doxyfile_name, mode="r", encoding="utf-8") as file:
393 s_modified_config = file.read()
394
395 s_diff_file_name = os.path.join(self.s_output_dirs_output_dir, S_DOXY_DIFF_HTML_NAME)
396 difference = difflib.HtmlDiff(wrapcolumn=I_WRAP_LENGTH).make_file(s_default_config.splitlines(), s_modified_config.splitlines(), "Default", "Modified")
397 with open(s_diff_file_name, mode="w", encoding="utf-8") as file:
398 file.write(difference)
399
400 def run_doxygen(self, b_open_doxygen_output: bool = True) -> bool:
401 """!
402 @brief Generate Doxyfile and Doxygen output depend on doxyfile settings
403 @param b_open_doxygen_output : [True] open output in browser; [False] only generate output
404 @return status for found doxygen warning
405 """
406 self.download_doxygen()
407 if B_PLANTUML_SUPPORT:
410 if not os.path.exists(self.s_output_dirs_output_dir):
411 os.makedirs(self.s_output_dirs_output_dir)
414 self.generate_doxygen_output(b_open_doxygen_output)
415 if B_GITHUB_CORNER_SUPPORT:
416 self.add_github_corner()
417 self.add_nojekyll_file()
418 if B_DOXY_CONFIG_DIFF_SUPPORT:
420
421 if B_DOXY_PY_CHECKER_SUPPORT:
422 # additional script to check for valid doxygen specification in python files
423 file_patterns = self.d_settings["FILE_PATTERNS"]
424 if isinstance(file_patterns, list):
425 l_file_patterns = [str(pattern) for pattern in file_patterns]
426 else:
427 l_file_patterns = [str(file_patterns)]
428 if S_PYTHON_PATTERN in l_file_patterns:
429 doxy_checker = DoxyPyChecker(S_MAIN_FOLDER_FOLDER)
430 l_finding = doxy_checker.run_check()
431 self.l_warnings.extend(l_finding)
432
433 self.add_warnings() # add own warnings to warning file
434 b_warnings = self.check_doxygen_warnings(b_open_doxygen_output)
435
436 return b_warnings
437
438
439def get_cmd_args() -> argparse.Namespace:
440 """!
441 @brief Function to define CMD arguments.
442 @return Function returns argument parser.
443 """
444 o_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
445 o_parser.add_argument("-o", "--open",
446 type=bool,
447 default=False,
448 help="open output files after generation")
449 return o_parser.parse_args()
This class should be used to parse and store a doxygen configuration file.
Class to generate Doxygen documentation for any code documentation with uniform settings and styling.
None prepare_doxyfile_configuration(self)
Prepare doxfile configuration with default settings that depend on other parameters.
None set_configuration(self, str s_type, Any value, bool b_override=True)
Set doxygen configuration.
None __init__(self, Optional[str] s_webside=None)
None generate_configuration_diff(self)
Generate doxyfile diff to view changes.
None generate_doxygen_output(self, bool b_open_doxygen_output=True)
Generate Doxygen output depend on existing doxyfile.
None create_default_doxyfile(self, str s_file_name)
Create default doxyfile.
bool run_doxygen(self, bool b_open_doxygen_output=True)
Generate Doxyfile and Doxygen output depend on doxyfile settings.
None add_nojekyll_file(self)
Add .nojekyll file that files with underscores visible.
bool check_doxygen_warnings(self, bool b_open_warning_file=True)
Check for doxygen Warnings.
Any get_configuration(self, str s_type)
Get type in doxygen configuration.
None edit_select_doxyfile_settings(self)
Override default settings in doxyfile with selected settings.
None add_github_corner(self)
Add Github corner and tab icon.
None add_warnings(self)
Add warnings to warning file.
Class to open Notepad in thread to prevent program stop until close file.
argparse.Namespace get_cmd_args()
Function to define CMD arguments.