2019-10-07 06:52:00 +00:00
|
|
|
#!/usr/bin/env python3
|
2019-10-08 09:39:27 +00:00
|
|
|
"""Prepare output and write compiled jinja2 templates."""
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
import glob
|
|
|
|
import ntpath
|
|
|
|
import os
|
2022-02-19 13:58:35 +00:00
|
|
|
import re
|
2019-10-08 09:30:31 +00:00
|
|
|
from functools import reduce
|
2019-10-08 09:39:27 +00:00
|
|
|
|
2019-10-07 06:52:00 +00:00
|
|
|
import jinja2.exceptions
|
|
|
|
import ruamel.yaml
|
2023-01-20 10:56:12 +00:00
|
|
|
from jinja2 import Environment, FileSystemLoader
|
2022-03-27 11:13:34 +00:00
|
|
|
from jinja2.filters import pass_eval_context
|
2019-10-07 06:52:00 +00:00
|
|
|
|
2021-01-01 12:50:41 +00:00
|
|
|
import ansibledoctor.exception
|
|
|
|
from ansibledoctor.config import SingleConfig
|
2023-01-20 10:56:12 +00:00
|
|
|
from ansibledoctor.utils import FileUtils, SingleLog
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Generator:
|
2020-04-05 21:16:53 +00:00
|
|
|
"""Generate documentation from jinja2 templates."""
|
|
|
|
|
2019-10-07 06:52:00 +00:00
|
|
|
def __init__(self, doc_parser):
|
|
|
|
self.template_files = []
|
|
|
|
self.extension = "j2"
|
|
|
|
self._parser = None
|
|
|
|
self.config = SingleConfig()
|
2019-10-08 09:30:31 +00:00
|
|
|
self.log = SingleLog()
|
|
|
|
self.logger = self.log.logger
|
2019-10-07 06:52:00 +00:00
|
|
|
self._parser = doc_parser
|
|
|
|
self._scan_template()
|
|
|
|
|
|
|
|
def _scan_template(self):
|
|
|
|
"""
|
|
|
|
Search for Jinja2 (.j2) files to apply to the destination.
|
|
|
|
|
|
|
|
:return: None
|
|
|
|
"""
|
2024-06-07 19:51:10 +00:00
|
|
|
template = self.config.get_template()
|
|
|
|
if os.path.isdir(template):
|
|
|
|
self.logger.info(f"Using template: {os.path.relpath(template, self.log.ctx)}")
|
2019-10-09 21:32:21 +00:00
|
|
|
else:
|
2024-06-07 19:51:10 +00:00
|
|
|
self.log.sysexit_with_message(f"Can not open template directory {template}")
|
2019-10-09 21:32:21 +00:00
|
|
|
|
2024-06-07 19:51:10 +00:00
|
|
|
for file in glob.iglob(template + "/**/*." + self.extension, recursive=True):
|
|
|
|
relative_file = file[len(template) + 1 :]
|
2019-10-07 06:52:00 +00:00
|
|
|
if ntpath.basename(file)[:1] != "_":
|
2022-08-21 21:10:03 +00:00
|
|
|
self.logger.debug(f"Found template file: {relative_file}")
|
2019-10-07 06:52:00 +00:00
|
|
|
self.template_files.append(relative_file)
|
|
|
|
else:
|
2022-08-21 21:10:03 +00:00
|
|
|
self.logger.debug(f"Ignoring template file: {relative_file}")
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
def _create_dir(self, directory):
|
2019-10-09 21:23:14 +00:00
|
|
|
if not self.config.config["dry_run"] and not os.path.isdir(directory):
|
|
|
|
try:
|
|
|
|
os.makedirs(directory, exist_ok=True)
|
2022-08-21 21:10:03 +00:00
|
|
|
self.logger.info(f"Creating dir: {directory}")
|
2019-10-09 21:23:14 +00:00
|
|
|
except FileExistsError as e:
|
2024-06-01 22:08:25 +00:00
|
|
|
self.log.sysexit_with_message(e)
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
def _write_doc(self):
|
|
|
|
files_to_overwite = []
|
|
|
|
|
|
|
|
for file in self.template_files:
|
2020-04-05 21:16:53 +00:00
|
|
|
doc_file = os.path.join(
|
2024-06-07 19:51:10 +00:00
|
|
|
self.config.config.get("renderer.dest"), os.path.splitext(file)[0]
|
2020-04-05 21:16:53 +00:00
|
|
|
)
|
2019-10-07 06:52:00 +00:00
|
|
|
if os.path.isfile(doc_file):
|
|
|
|
files_to_overwite.append(doc_file)
|
|
|
|
|
2024-06-07 19:51:10 +00:00
|
|
|
header_file = self.config.config.get("renderer.include_header")
|
2019-10-09 21:24:47 +00:00
|
|
|
role_data = self._parser.get_data()
|
|
|
|
header_content = ""
|
|
|
|
if bool(header_file):
|
|
|
|
role_data["internal"]["append"] = True
|
|
|
|
try:
|
2023-01-20 10:56:12 +00:00
|
|
|
with open(header_file) as a:
|
2019-10-09 21:24:47 +00:00
|
|
|
header_content = a.read()
|
|
|
|
except FileNotFoundError as e:
|
2023-05-28 21:07:24 +00:00
|
|
|
self.log.sysexit_with_message(f"Can not open custom header file\n{e!s}")
|
2023-01-20 10:56:12 +00:00
|
|
|
|
|
|
|
if (
|
2023-11-10 13:50:50 +00:00
|
|
|
len(files_to_overwite) > 0
|
2024-06-07 19:51:10 +00:00
|
|
|
and self.config.config.get("renderer.force_overwrite") is False
|
2023-01-20 10:56:12 +00:00
|
|
|
and not self.config.config["dry_run"]
|
|
|
|
):
|
|
|
|
files_to_overwite_string = "\n".join(files_to_overwite)
|
2023-11-23 09:33:36 +00:00
|
|
|
prompt = f"These files will be overwritten:\n{files_to_overwite_string}".replace(
|
|
|
|
"\n", "\n... "
|
|
|
|
)
|
2023-01-20 10:56:12 +00:00
|
|
|
|
|
|
|
try:
|
2023-11-23 09:33:36 +00:00
|
|
|
if not FileUtils.query_yes_no(f"{prompt}\nDo you want to continue?"):
|
2019-10-08 09:30:31 +00:00
|
|
|
self.log.sysexit_with_message("Aborted...")
|
2023-01-20 10:56:12 +00:00
|
|
|
except ansibledoctor.exception.InputError as e:
|
|
|
|
self.logger.debug(str(e))
|
|
|
|
self.log.sysexit_with_message("Aborted...")
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
for file in self.template_files:
|
2020-04-05 21:16:53 +00:00
|
|
|
doc_file = os.path.join(
|
2024-06-07 19:51:10 +00:00
|
|
|
self.config.config.get("renderer.dest"), os.path.splitext(file)[0]
|
2020-04-05 21:16:53 +00:00
|
|
|
)
|
2019-10-07 12:44:45 +00:00
|
|
|
source_file = self.config.get_template() + "/" + file
|
2019-10-07 06:52:00 +00:00
|
|
|
|
2024-06-07 19:51:10 +00:00
|
|
|
self.logger.debug(
|
|
|
|
f"Writing renderer output to: {os.path.relpath(doc_file, self.log.ctx)} "
|
|
|
|
f"from: {os.path.dirname(os.path.relpath(source_file, self.log.ctx))}"
|
|
|
|
)
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
# make sure the directory exists
|
2019-10-09 21:24:47 +00:00
|
|
|
self._create_dir(os.path.dirname(doc_file))
|
2019-10-07 06:52:00 +00:00
|
|
|
|
|
|
|
if os.path.exists(source_file) and os.path.isfile(source_file):
|
2023-01-20 10:56:12 +00:00
|
|
|
with open(source_file) as template:
|
2019-10-07 06:52:00 +00:00
|
|
|
data = template.read()
|
|
|
|
if data is not None:
|
|
|
|
try:
|
2020-04-05 21:28:39 +00:00
|
|
|
jenv = Environment( # nosec
|
2020-04-05 21:16:53 +00:00
|
|
|
loader=FileSystemLoader(self.config.get_template()),
|
|
|
|
lstrip_blocks=True,
|
2023-01-20 10:56:12 +00:00
|
|
|
trim_blocks=True,
|
2023-11-10 13:50:50 +00:00
|
|
|
autoescape=jinja2.select_autoescape(),
|
2020-04-05 21:28:39 +00:00
|
|
|
)
|
2019-10-07 06:52:00 +00:00
|
|
|
jenv.filters["to_nice_yaml"] = self._to_nice_yaml
|
2019-10-08 09:30:31 +00:00
|
|
|
jenv.filters["deep_get"] = self._deep_get
|
2023-02-09 18:27:22 +00:00
|
|
|
jenv.filters["safe_join"] = self._safe_join
|
|
|
|
# keep the old name of the function to not break custom templates.
|
|
|
|
jenv.filters["save_join"] = self._safe_join
|
2024-06-07 19:51:10 +00:00
|
|
|
template_options = self.config.config.get("template.options")
|
2024-06-01 20:05:16 +00:00
|
|
|
data = jenv.from_string(data).render(
|
2024-06-07 19:51:10 +00:00
|
|
|
role_data, role=role_data, options=template_options
|
2024-06-01 20:05:16 +00:00
|
|
|
)
|
2019-10-08 22:56:39 +00:00
|
|
|
if not self.config.config["dry_run"]:
|
2019-10-07 12:44:45 +00:00
|
|
|
with open(doc_file, "wb") as outfile:
|
2019-10-09 21:24:47 +00:00
|
|
|
outfile.write(header_content.encode("utf-8"))
|
2019-10-07 12:44:45 +00:00
|
|
|
outfile.write(data.encode("utf-8"))
|
2022-08-21 21:10:03 +00:00
|
|
|
self.logger.info(f"Writing to: {doc_file}")
|
2019-10-07 06:52:00 +00:00
|
|
|
else:
|
2022-08-21 21:10:03 +00:00
|
|
|
self.logger.info(f"Writing to: {doc_file}")
|
2020-04-05 21:16:53 +00:00
|
|
|
except (
|
2023-02-09 18:27:22 +00:00
|
|
|
jinja2.exceptions.UndefinedError,
|
|
|
|
jinja2.exceptions.TemplateSyntaxError,
|
2023-11-10 13:50:50 +00:00
|
|
|
jinja2.exceptions.TemplateRuntimeError,
|
2020-04-05 21:16:53 +00:00
|
|
|
) as e:
|
2019-10-08 09:30:31 +00:00
|
|
|
self.log.sysexit_with_message(
|
2023-07-19 08:16:46 +00:00
|
|
|
f"Jinja2 templating error while loading file: '{file}'\n{e!s}"
|
2020-04-05 21:16:53 +00:00
|
|
|
)
|
2019-10-07 06:52:00 +00:00
|
|
|
except UnicodeEncodeError as e:
|
2019-10-08 09:30:31 +00:00
|
|
|
self.log.sysexit_with_message(
|
2023-05-28 21:07:24 +00:00
|
|
|
f"Unable to print special characters\n{e!s}"
|
2020-04-05 21:16:53 +00:00
|
|
|
)
|
2019-10-07 06:52:00 +00:00
|
|
|
|
2023-01-20 10:56:12 +00:00
|
|
|
def _to_nice_yaml(self, a, indent=4, **kw):
|
2019-10-07 06:52:00 +00:00
|
|
|
"""Make verbose, human readable yaml."""
|
|
|
|
yaml = ruamel.yaml.YAML()
|
|
|
|
yaml.indent(mapping=indent, sequence=(indent * 2), offset=indent)
|
|
|
|
stream = ruamel.yaml.compat.StringIO()
|
|
|
|
yaml.dump(a, stream, **kw)
|
|
|
|
return stream.getvalue().rstrip()
|
|
|
|
|
2023-01-20 10:56:12 +00:00
|
|
|
def _deep_get(self, _, dictionary, keys):
|
2019-10-08 09:30:31 +00:00
|
|
|
default = None
|
2020-04-05 21:16:53 +00:00
|
|
|
return reduce(
|
2023-11-10 13:50:50 +00:00
|
|
|
lambda d, key: d.get(key, default) if isinstance(d, dict) else default,
|
|
|
|
keys.split("."),
|
|
|
|
dictionary,
|
2020-04-05 21:16:53 +00:00
|
|
|
)
|
2019-10-08 09:30:31 +00:00
|
|
|
|
2022-03-27 11:13:34 +00:00
|
|
|
@pass_eval_context
|
2023-02-09 18:27:22 +00:00
|
|
|
def _safe_join(self, eval_ctx, value, d=""):
|
2019-10-15 07:54:03 +00:00
|
|
|
if isinstance(value, str):
|
|
|
|
value = [value]
|
2022-02-19 13:58:35 +00:00
|
|
|
|
2022-03-02 13:33:37 +00:00
|
|
|
normalized = jinja2.filters.do_join(eval_ctx, value, d, attribute=None)
|
|
|
|
|
2024-06-07 19:51:10 +00:00
|
|
|
if self.config.config.renderer.autotrim:
|
2023-06-14 11:31:01 +00:00
|
|
|
for s in [r" +(\n|\t| )", r"(\n|\t) +"]:
|
|
|
|
normalized = re.sub(s, "\\1", normalized)
|
|
|
|
|
|
|
|
return jinja2.filters.do_mark_safe(normalized)
|
2019-10-15 07:54:03 +00:00
|
|
|
|
2019-10-07 06:52:00 +00:00
|
|
|
def render(self):
|
2024-06-07 19:51:10 +00:00
|
|
|
self.logger.info(f"Using renderer destination: {self.config.config.get('renderer.dest')}")
|
2019-10-07 12:44:45 +00:00
|
|
|
self._write_doc()
|