ansible-doctor/ansibledoctor/doc_generator.py

179 lines
7.1 KiB
Python
Raw Normal View History

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
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
from jinja2 import Environment, FileSystemLoader
from jinja2.filters import pass_eval_context
2019-10-07 06:52:00 +00:00
import ansibledoctor.exception
from ansibledoctor.config import SingleConfig
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
"""
template_dir = self.config.get_template()
if os.path.isdir(template_dir):
self.logger.info(f"Using template dir: {template_dir}")
else:
self.log.sysexit_with_message(f"Can not open template dir {template_dir}")
for file in glob.iglob(template_dir + "/**/*." + self.extension, recursive=True):
relative_file = file[len(template_dir) + 1 :]
2019-10-07 06:52:00 +00:00
if ntpath.basename(file)[:1] != "_":
self.logger.debug(f"Found template file: {relative_file}")
2019-10-07 06:52:00 +00:00
self.template_files.append(relative_file)
else:
self.logger.debug(f"Ignoring template file: {relative_file}")
2019-10-07 06:52:00 +00:00
def _create_dir(self, directory):
if not self.config.config["dry_run"] and not os.path.isdir(directory):
try:
os.makedirs(directory, exist_ok=True)
self.logger.info(f"Creating dir: {directory}")
except FileExistsError as e:
self.log.sysexit_with_message(str(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(
self.config.config.get("output_dir"), 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)
header_file = self.config.config.get("custom_header")
role_data = self._parser.get_data()
header_content = ""
if bool(header_file):
role_data["internal"]["append"] = True
try:
with open(header_file) as a:
header_content = a.read()
except FileNotFoundError as e:
self.log.sysexit_with_message(f"Can not open custom header file\n{e!s}")
if (
len(files_to_overwite) > 0
and self.config.config.get("force_overwrite") is False
and not self.config.config["dry_run"]
):
files_to_overwite_string = "\n".join(files_to_overwite)
prompt = f"These files will be overwritten:\n{files_to_overwite_string}".replace(
"\n", "\n... "
)
try:
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...")
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(
self.config.config.get("output_dir"), os.path.splitext(file)[0]
2020-04-05 21:16:53 +00:00
)
source_file = self.config.get_template() + "/" + file
2019-10-07 06:52:00 +00:00
self.logger.debug(f"Writing doc output to: {doc_file} from: {source_file}")
2019-10-07 06:52:00 +00:00
# make sure the directory exists
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):
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,
trim_blocks=True,
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
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
data = jenv.from_string(data).render(role_data, role=role_data)
2019-10-08 22:56:39 +00:00
if not self.config.config["dry_run"]:
with open(doc_file, "wb") as outfile:
outfile.write(header_content.encode("utf-8"))
outfile.write(data.encode("utf-8"))
self.logger.info(f"Writing to: {doc_file}")
2019-10-07 06:52:00 +00:00
else:
self.logger.info(f"Writing to: {doc_file}")
2020-04-05 21:16:53 +00:00
except (
jinja2.exceptions.UndefinedError,
jinja2.exceptions.TemplateSyntaxError,
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(
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(
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
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()
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(
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
@pass_eval_context
def _safe_join(self, eval_ctx, value, d=""):
if isinstance(value, str):
value = [value]
normalized = jinja2.filters.do_join(eval_ctx, value, d, attribute=None)
if self.config.config["template_autotrim"]:
for s in [r" +(\n|\t| )", r"(\n|\t) +"]:
normalized = re.sub(s, "\\1", normalized)
return jinja2.filters.do_mark_safe(normalized)
2019-10-07 06:52:00 +00:00
def render(self):
self.logger.info(f"Using output dir: {self.config.config.get('output_dir')}")
self._write_doc()