mirror of
https://github.com/thegeeklab/ansible-doctor.git
synced 2024-11-21 12:20:40 +00:00
fork; initial commit
This commit is contained in:
commit
b3cd1d0978
8
.flake8
Normal file
8
.flake8
Normal file
@ -0,0 +1,8 @@
|
||||
[flake8]
|
||||
# Temp disable Docstring checks D101, D102, D103, D107
|
||||
ignore = E501, W503, F401, N813, D101, D102, D103, D107
|
||||
max-line-length = 100
|
||||
inline-quotes = double
|
||||
exclude = .git,.tox,__pycache__,build,dist,tests,*.pyc,*.egg-info,.cache,.eggs,env*
|
||||
application-import-names = ansiblelater
|
||||
format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
|
103
.gitignore
vendored
Normal file
103
.gitignore
vendored
Normal file
@ -0,0 +1,103 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
env*/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# Ignore ide addons
|
||||
.server-script
|
||||
.on-save.json
|
||||
.vscode
|
||||
.pytest_cache
|
||||
|
||||
pip-wheel-metadata
|
165
LICENSE
Normal file
165
LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include LICENSE
|
||||
recursive-include ansibledoctor/templates *
|
139
ansibledoctor/Annotation.py
Normal file
139
ansibledoctor/Annotation.py
Normal file
@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import pprint
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
import anyconfig
|
||||
import yaml
|
||||
|
||||
from ansibledoctor.Config import SingleConfig
|
||||
from ansibledoctor.FileRegistry import Registry
|
||||
from ansibledoctor.Utils import SingleLog
|
||||
|
||||
|
||||
class AnnotationItem:
|
||||
|
||||
# next time improve this by looping over public available attributes
|
||||
def __init__(self):
|
||||
self.data = defaultdict(dict)
|
||||
|
||||
def get_obj(self):
|
||||
return self.data
|
||||
|
||||
|
||||
class Annotation:
|
||||
def __init__(self, name, files_registry):
|
||||
self._all_items = defaultdict(dict)
|
||||
self._file_handler = None
|
||||
self.config = SingleConfig()
|
||||
self.log = SingleLog()
|
||||
self._files_registry = files_registry
|
||||
|
||||
self._all_annotations = self.config.get_annotations_definition()
|
||||
|
||||
if name in self._all_annotations.keys():
|
||||
self._annotation_definition = self._all_annotations[name]
|
||||
|
||||
if self._annotation_definition is not None:
|
||||
self._find_annotation()
|
||||
|
||||
def get_details(self):
|
||||
return self._all_items
|
||||
|
||||
def _find_annotation(self):
|
||||
regex = r"(\#\ *\@" + self._annotation_definition["name"] + r"\ +.*)"
|
||||
for rfile in self._files_registry.get_files():
|
||||
self._file_handler = open(rfile, encoding="utf8")
|
||||
|
||||
while True:
|
||||
line = self._file_handler.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
if re.match(regex, line):
|
||||
item = self._get_annotation_data(
|
||||
line, self._annotation_definition["name"])
|
||||
if item:
|
||||
self._populate_item(item.get_obj().items())
|
||||
|
||||
self._file_handler.close()
|
||||
|
||||
def _populate_item(self, item):
|
||||
for key, value in item:
|
||||
anyconfig.merge(self._all_items[key],
|
||||
value, ac_merge=anyconfig.MS_DICTS)
|
||||
|
||||
def _get_annotation_data(self, line, name):
|
||||
"""
|
||||
Make some string conversion on a line in order to get the relevant data.
|
||||
|
||||
:param line:
|
||||
"""
|
||||
item = AnnotationItem()
|
||||
|
||||
# step1 remove the annotation
|
||||
# reg1 = "(\#\ *\@"++"\ *)"
|
||||
reg1 = r"(\#\ *\@" + name + r"\ *)"
|
||||
line1 = re.sub(reg1, "", line).strip()
|
||||
|
||||
# step3 take the main key value from the annotation
|
||||
parts = [part.strip() for part in line1.split(":", 2)]
|
||||
key = str(parts[0])
|
||||
item.data[key] = {}
|
||||
multiline_char = [">"]
|
||||
|
||||
if len(parts) < 2:
|
||||
return
|
||||
|
||||
if len(parts) == 2:
|
||||
parts = parts[:1] + ["value"] + parts[1:]
|
||||
|
||||
if name == "var":
|
||||
try:
|
||||
content = {key: json.loads(parts[2].strip())}
|
||||
except ValueError:
|
||||
content = {key: parts[2].strip()}
|
||||
else:
|
||||
content = parts[2]
|
||||
|
||||
item.data[key][parts[1]] = content
|
||||
|
||||
# step4 check for multiline description
|
||||
if parts[2] in multiline_char:
|
||||
multiline = []
|
||||
stars_with_annotation = r"(\#\ *[\@][\w]+)"
|
||||
current_file_position = self._file_handler.tell()
|
||||
|
||||
while True:
|
||||
next_line = self._file_handler.readline()
|
||||
|
||||
if not next_line.strip():
|
||||
self._file_handler.seek(current_file_position)
|
||||
break
|
||||
|
||||
# match if annotation in line
|
||||
if re.match(stars_with_annotation, next_line):
|
||||
self._file_handler.seek(current_file_position)
|
||||
break
|
||||
# match if empty line or commented empty line
|
||||
test_line = next_line.replace("#", "").strip()
|
||||
if len(test_line) == 0:
|
||||
self._file_handler.seek(current_file_position)
|
||||
break
|
||||
|
||||
# match if does not start with comment
|
||||
test_line2 = next_line.strip()
|
||||
if test_line2[:1] != "#":
|
||||
self._file_handler.seek(current_file_position)
|
||||
break
|
||||
|
||||
final = next_line.replace("#", "").rstrip()
|
||||
if final[:1] == " ":
|
||||
final = final[1:]
|
||||
multiline.append(final)
|
||||
|
||||
item.data[key][parts[1]] = multiline
|
||||
|
||||
return item
|
130
ansibledoctor/Cli.py
Normal file
130
ansibledoctor/Cli.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ansibledoctor import __version__
|
||||
from ansibledoctor.Config import SingleConfig
|
||||
from ansibledoctor.DocumentationGenerator import Generator
|
||||
from ansibledoctor.DocumentationParser import Parser
|
||||
from ansibledoctor.Utils import SingleLog
|
||||
|
||||
|
||||
class AnsibleDoctor:
|
||||
|
||||
def __init__(self):
|
||||
self.config = SingleConfig()
|
||||
self.log = SingleLog(self.config.debug_level)
|
||||
args = self._cli_args()
|
||||
self._parse_args(args)
|
||||
|
||||
doc_parser = Parser()
|
||||
doc_generator = Generator(doc_parser)
|
||||
doc_generator.render()
|
||||
|
||||
def _cli_args(self):
|
||||
"""
|
||||
Use argparse for parsing CLI arguments.
|
||||
|
||||
:return: args objec
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate documentation from annotated playbooks and roles using templates")
|
||||
parser.add_argument("project_dir", nargs="?", default=os.getcwd(),
|
||||
help="role directory, (default: current working dir)")
|
||||
parser.add_argument("-c", "--conf", nargs="?", default="",
|
||||
help="location of configuration file")
|
||||
parser.add_argument("-o", "--output", action="store", dest="output", type=str,
|
||||
help="output base dir")
|
||||
parser.add_argument("-f", "--force", action="store_true", help="force overwrite output file")
|
||||
parser.add_argument("-d", "--dry-run", action="store_true", help="dry run without writing")
|
||||
parser.add_argument("-D", "--default", action="store_true", help="print the default configuration")
|
||||
parser.add_argument("-p", "--print", nargs="?", default="_unset_",
|
||||
help="use print template instead of writing to files")
|
||||
parser.add_argument("--version", action="version", version="%(prog)s {}".format(__version__))
|
||||
|
||||
debug_level = parser.add_mutually_exclusive_group()
|
||||
debug_level.add_argument("-v", action="store_true", help="Set debug level to info")
|
||||
debug_level.add_argument("-vv", action="store_true", help="Set debug level to debug")
|
||||
debug_level.add_argument("-vvv", action="store_true", help="Set debug level to trace")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def _parse_args(self, args):
|
||||
"""
|
||||
Use an args object to apply all the configuration combinations to the config object.
|
||||
|
||||
:param args:
|
||||
:return: None
|
||||
"""
|
||||
self.config.set_base_dir(os.path.abspath(args.project_dir))
|
||||
|
||||
# search for config file
|
||||
if args.conf != "":
|
||||
conf_file = os.path.abspath(args.conf)
|
||||
if os.path.isfile(conf_file) and os.path.basename(conf_file) == self.config.config_file_name:
|
||||
self.config.load_config_file(conf_file)
|
||||
# re apply log level based on config
|
||||
self.log.set_level(self.config.debug_level)
|
||||
else:
|
||||
self.log.warn("No configuration file found: " + conf_file)
|
||||
else:
|
||||
conf_file = self.config.get_base_dir() + "/" + self.config.config_file_name
|
||||
if os.path.isfile(conf_file):
|
||||
self.config.load_config_file(conf_file)
|
||||
# re apply log level based on config
|
||||
self.log.set_level(self.config.debug_level)
|
||||
|
||||
# sample configuration
|
||||
if args.default:
|
||||
print(self.config.sample_config)
|
||||
sys.exit()
|
||||
|
||||
# Debug levels
|
||||
if args.v is True:
|
||||
self.log.set_level("info")
|
||||
elif args.vv is True:
|
||||
self.log.set_level("debug")
|
||||
elif args.vvv is True:
|
||||
self.log.set_level("trace")
|
||||
|
||||
# need to send the message after the log levels have been set
|
||||
self.log.debug("using configuration file: " + conf_file)
|
||||
|
||||
# Overwrite
|
||||
if args.y is True:
|
||||
self.config.template_overwrite = True
|
||||
|
||||
# Dry run
|
||||
if args.dry_run is True:
|
||||
self.config.dry_run = True
|
||||
if self.log.log_level > 1:
|
||||
self.log.set_level(1)
|
||||
self.log.info("Running in Dry mode: Therefore setting log level at least to INFO")
|
||||
|
||||
# Print template
|
||||
if args.p == "_unset_":
|
||||
pass
|
||||
elif args.p is None:
|
||||
self.config.use_print_template = "all"
|
||||
else:
|
||||
self.config.use_print_template = args.p
|
||||
|
||||
# output dir
|
||||
if args.output is not None:
|
||||
self.config.output_dir = os.path.abspath(args.output)
|
||||
|
||||
# some debug
|
||||
self.log.debug(args)
|
||||
self.log.info("Using base dir: " + self.config.get_base_dir())
|
||||
|
||||
if self.config.is_role:
|
||||
self.log.info("This is detected as: ROLE ")
|
||||
elif self.config.is_role is not None and not self.config.is_role:
|
||||
self.log.info("This is detected as: PLAYBOOK ")
|
||||
else:
|
||||
self.log.error([
|
||||
self.config.get_base_dir() + "/tasks"
|
||||
], "No ansible role detected, checked for: ")
|
||||
sys.exit(1)
|
210
ansibledoctor/Config.py
Normal file
210
ansibledoctor/Config.py
Normal file
@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
import yaml
|
||||
|
||||
from ansibledoctor.Utils import Singleton
|
||||
|
||||
|
||||
class Config:
|
||||
sample_config = """---
|
||||
# filename: doctor.conf.yaml
|
||||
|
||||
# base directoy to scan, relative dir to configuration file
|
||||
# base_dir: "./"
|
||||
|
||||
# documentation output directory, relative dir to configuration file.
|
||||
output_dir: "./doc"
|
||||
|
||||
# directory containing templates, relative dir to configuration file,
|
||||
# comment to use default build in ones
|
||||
# template_dir: "./template"
|
||||
|
||||
# template directory name within template_dir
|
||||
# build in "doc_and_readme" and "readme"
|
||||
template: "readme"
|
||||
|
||||
# Overwrite documentation pages if already exist
|
||||
# this is equal to -y
|
||||
# template_overwrite : False
|
||||
|
||||
# set the debug level: trace | debug | info | warn
|
||||
# see -v | -vv | -vvv
|
||||
# debug_level: "warn"
|
||||
|
||||
# when searching for yaml files in roles projects,
|
||||
# excluded this paths (dir and files) from analysis
|
||||
# default values
|
||||
excluded_roles_dirs: []
|
||||
|
||||
"""
|
||||
# path to the documentation output dir
|
||||
output_dir = ""
|
||||
|
||||
# project base directory
|
||||
_base_dir = ""
|
||||
|
||||
# current directory of this object,
|
||||
# used to get the default template directory
|
||||
script_base_dir = ""
|
||||
|
||||
# path to the directory that contains the templates
|
||||
template_dir = ""
|
||||
# default template name
|
||||
default_template = "readme"
|
||||
# template to use
|
||||
template = ""
|
||||
# flag to ask if files can be overwritten
|
||||
template_overwrite = False
|
||||
# flag to use the cli print template
|
||||
use_print_template = False
|
||||
|
||||
# don"t modify any file
|
||||
dry_run = False
|
||||
|
||||
# default debug level
|
||||
debug_level = "warn"
|
||||
|
||||
# internal flag
|
||||
is_role = None
|
||||
# internal when is_rote is True
|
||||
project_name = ""
|
||||
|
||||
# name of the config file to search for
|
||||
config_file_name = "doctor.conf.yaml"
|
||||
# if config file is not in root of project, this is used to make output relative to config file
|
||||
_config_file_dir = ""
|
||||
|
||||
excluded_roles_dirs = []
|
||||
|
||||
# annotation search patterns
|
||||
|
||||
# for any pattern like " # @annotation: [annotation_key] # description "
|
||||
# name = annotation ( without "@" )
|
||||
# allow_multiple = True allow to repeat the same annotation, i.e. @todo
|
||||
# automatic = True this action will be parsed based on the annotation in name without calling the parse method
|
||||
|
||||
annotations = {
|
||||
"meta": {
|
||||
"name": "meta",
|
||||
"automatic": True
|
||||
},
|
||||
"todo": {
|
||||
"name": "todo",
|
||||
"automatic": True,
|
||||
},
|
||||
"var": {
|
||||
"name": "var",
|
||||
"automatic": True,
|
||||
},
|
||||
"example": {
|
||||
"name": "example",
|
||||
"regex": r"(\#\ *\@example\ *\: *.*)"
|
||||
},
|
||||
"tag": {
|
||||
"name": "tag",
|
||||
"automatic": True,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.script_base_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
def set_base_dir(self, directory):
|
||||
self._base_dir = directory
|
||||
self._set_is_role()
|
||||
|
||||
def get_base_dir(self):
|
||||
return self._base_dir
|
||||
|
||||
def get_annotations_definition(self, automatic=True):
|
||||
annotations = {}
|
||||
|
||||
if automatic:
|
||||
for k, item in self.annotations.items():
|
||||
if "automatic" in item.keys() and item["automatic"]:
|
||||
annotations[k] = item
|
||||
|
||||
return annotations
|
||||
|
||||
def get_annotations_names(self, automatic=True):
|
||||
|
||||
annotations = []
|
||||
|
||||
if automatic:
|
||||
for k, item in self.annotations.items():
|
||||
if "automatic" in item.keys() and item["automatic"]:
|
||||
annotations.append(k)
|
||||
|
||||
return annotations
|
||||
|
||||
def _set_is_role(self):
|
||||
if os.path.isdir(self._base_dir + "/tasks"):
|
||||
self.is_role = True
|
||||
else:
|
||||
self.is_role = None
|
||||
|
||||
def get_output_dir(self):
|
||||
"""
|
||||
Get the relative path to cwd of the output directory for the documentation.
|
||||
|
||||
:return: str path
|
||||
"""
|
||||
if self.use_print_template:
|
||||
return ""
|
||||
if self.output_dir == "":
|
||||
return os.path.realpath(self._base_dir)
|
||||
elif os.path.isabs(self.output_dir):
|
||||
return os.path.realpath(self.output_dir)
|
||||
elif not os.path.isabs(self.output_dir):
|
||||
return os.path.realpath(self._config_file_dir + "/" + self.output_dir)
|
||||
|
||||
def get_template_base_dir(self):
|
||||
"""
|
||||
Get the base dir for the template to use.
|
||||
|
||||
:return: str abs path
|
||||
"""
|
||||
if self.use_print_template:
|
||||
return os.path.realpath(self.script_base_dir + "/templates/cliprint")
|
||||
|
||||
if self.template == "":
|
||||
template = self.default_template
|
||||
else:
|
||||
template = self.template
|
||||
|
||||
if self.template_dir == "":
|
||||
return os.path.realpath(self.script_base_dir + "/templates/" + template)
|
||||
elif os.path.isabs(self.template_dir):
|
||||
return os.path.realpath(self.template_dir + "/" + template)
|
||||
elif not os.path.isabs(self.template_dir):
|
||||
return os.path.realpath(self._config_file_dir + "/" + self.template_dir + "/" + template)
|
||||
|
||||
def load_config_file(self, file):
|
||||
|
||||
allow_to_overwrite = [
|
||||
"base_dir",
|
||||
"output_dir",
|
||||
"template_dir",
|
||||
"template",
|
||||
"template_overwrite",
|
||||
"debug_level",
|
||||
"excluded_roles_dirs",
|
||||
|
||||
]
|
||||
|
||||
with open(file, "r") as yaml_file:
|
||||
try:
|
||||
self._config_file_dir = os.path.dirname(os.path.realpath(file))
|
||||
data = yaml.safe_load(yaml_file)
|
||||
if data:
|
||||
for item_to_configure in allow_to_overwrite:
|
||||
if item_to_configure in data.keys():
|
||||
self.__setattr__(item_to_configure, data[item_to_configure])
|
||||
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
|
||||
|
||||
class SingleConfig(Config, metaclass=Singleton):
|
||||
pass
|
4
ansibledoctor/Contstants.py
Normal file
4
ansibledoctor/Contstants.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
DOCTOR_CONF_FILE = "doctor.conf.yaml"
|
||||
YAML_EXTENSIONS = ["yaml","yml"]
|
141
ansibledoctor/DocumentationGenerator.py
Normal file
141
ansibledoctor/DocumentationGenerator.py
Normal file
@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import codecs
|
||||
import glob
|
||||
import json
|
||||
import ntpath
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
import jinja2.exceptions
|
||||
import ruamel.yaml
|
||||
from jinja2 import Environment
|
||||
from jinja2 import FileSystemLoader
|
||||
from six import binary_type
|
||||
from six import text_type
|
||||
|
||||
from ansibledoctor.Config import SingleConfig
|
||||
from ansibledoctor.Utils import FileUtils
|
||||
from ansibledoctor.Utils import SingleLog
|
||||
|
||||
|
||||
class Generator:
|
||||
def __init__(self, doc_parser):
|
||||
self.template_files = []
|
||||
self.extension = "j2"
|
||||
self._parser = None
|
||||
self.config = SingleConfig()
|
||||
self.log = SingleLog()
|
||||
self.log.info("Using template dir: " + self.config.get_template_base_dir())
|
||||
self._parser = doc_parser
|
||||
self._scan_template()
|
||||
|
||||
def _scan_template(self):
|
||||
"""
|
||||
Search for Jinja2 (.j2) files to apply to the destination.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
base_dir = self.config.get_template_base_dir()
|
||||
|
||||
for file in glob.iglob(base_dir + "/**/*." + self.extension, recursive=True):
|
||||
|
||||
relative_file = file[len(base_dir) + 1:]
|
||||
if ntpath.basename(file)[:1] != "_":
|
||||
self.log.trace("[GENERATOR] found template file: " + relative_file)
|
||||
self.template_files.append(relative_file)
|
||||
else:
|
||||
self.log.debug("[GENERATOR] ignoring template file: " + relative_file)
|
||||
|
||||
def _create_dir(self, directory):
|
||||
if not self.config.dry_run:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
else:
|
||||
self.log.info("[GENERATOR][DRY] Creating dir: " + dir)
|
||||
|
||||
def _write_doc(self):
|
||||
files_to_overwite = []
|
||||
|
||||
for file in self.template_files:
|
||||
doc_file = self.config.get_output_dir() + "/" + file[:-len(self.extension) - 1]
|
||||
if os.path.isfile(doc_file):
|
||||
files_to_overwite.append(doc_file)
|
||||
|
||||
if len(files_to_overwite) > 0 and self.config.template_overwrite is False:
|
||||
SingleLog.print("This files will be overwritten:", files_to_overwite)
|
||||
if not self.config.dry_run:
|
||||
resulst = FileUtils.query_yes_no("Do you want to continue?")
|
||||
if resulst != "yes":
|
||||
sys.exit()
|
||||
|
||||
for file in self.template_files:
|
||||
doc_file = self.config.get_output_dir() + "/" + file[:-len(self.extension) - 1]
|
||||
source_file = self.config.get_template_base_dir() + "/" + file
|
||||
|
||||
self.log.trace("[GENERATOR] Writing doc output to: " + doc_file + " from: " + source_file)
|
||||
|
||||
# make sure the directory exists
|
||||
self._create_dir(os.path.dirname(os.path.realpath(doc_file)))
|
||||
|
||||
if os.path.exists(source_file) and os.path.isfile(source_file):
|
||||
with open(source_file, "r") as template:
|
||||
data = template.read()
|
||||
if data is not None:
|
||||
try:
|
||||
print(json.dumps(self._parser.get_data(), indent=4, sort_keys=True))
|
||||
jenv = Environment(loader=FileSystemLoader(self.config.get_template_base_dir()), lstrip_blocks=True, trim_blocks=True, autoescape=True)
|
||||
jenv.filters["to_nice_yaml"] = self._to_nice_yaml
|
||||
data = jenv.from_string(data).render(self._parser.get_data(), role=self._parser.get_data())
|
||||
if not self.config.dry_run:
|
||||
with open(doc_file, "w") as outfile:
|
||||
outfile.write(data)
|
||||
self.log.info("Writing to: " + doc_file)
|
||||
else:
|
||||
self.log.info("[GENERATOR][DRY] Writing to: " + doc_file)
|
||||
except jinja2.exceptions.UndefinedError as e:
|
||||
self.log.error("Jinja2 templating error: <" + str(e) + "> when loading file: '" + file + "', run in debug mode to see full except")
|
||||
if self.log.log_level < 1:
|
||||
raise
|
||||
except UnicodeEncodeError as e:
|
||||
self.log.error("At the moment I'm unable to print special chars: <" + str(e) + ">, run in debug mode to see full except")
|
||||
if self.log.log_level < 1:
|
||||
raise
|
||||
sys.exit()
|
||||
|
||||
def _to_nice_yaml(self, a, indent=4, *args, **kw):
|
||||
"""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 print_to_cli(self):
|
||||
for file in self.template_files:
|
||||
source_file = self.config.get_template_base_dir() + "/" + file
|
||||
with open(source_file, "r") as template:
|
||||
data = template.read()
|
||||
|
||||
if data is not None:
|
||||
try:
|
||||
data = Environment(loader=FileSystemLoader(self.config.get_template_base_dir()), lstrip_blocks=True, trim_blocks=True, autoescape=True).from_string(data).render(self._parser.get_data(), r=self._parser)
|
||||
print(data)
|
||||
except jinja2.exceptions.UndefinedError as e:
|
||||
self.log.error("Jinja2 templating error: <" + str(e) + "> when loading file: '" + file + "', run in debug mode to see full except")
|
||||
if self.log.log_level < 1:
|
||||
raise
|
||||
except UnicodeEncodeError as e:
|
||||
self.log.error("At the moment I'm unable to print special chars: <" + str(e) + ">, run in debug mode to see full except")
|
||||
if self.log.log_level < 1:
|
||||
raise
|
||||
except Exception:
|
||||
print("Unexpected error:", sys.exc_info()[0])
|
||||
raise
|
||||
|
||||
def render(self):
|
||||
if self.config.use_print_template:
|
||||
self.print_to_cli()
|
||||
else:
|
||||
self.log.info("Using output dir: " + self.config.get_output_dir())
|
||||
self._write_doc()
|
77
ansibledoctor/DocumentationParser.py
Normal file
77
ansibledoctor/DocumentationParser.py
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import fnmatch
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
import anyconfig
|
||||
import yaml
|
||||
|
||||
from ansibledoctor.Annotation import Annotation
|
||||
from ansibledoctor.Config import SingleConfig
|
||||
from ansibledoctor.Contstants import YAML_EXTENSIONS
|
||||
from ansibledoctor.FileRegistry import Registry
|
||||
from ansibledoctor.Utils import SingleLog
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self):
|
||||
self._annotation_objs = {}
|
||||
self._data = defaultdict(dict)
|
||||
self.config = SingleConfig()
|
||||
self.log = SingleLog()
|
||||
self._files_registry = Registry()
|
||||
self._parse_meta_file()
|
||||
self._parse_vars_file()
|
||||
self._populate_doc_data()
|
||||
|
||||
def _parse_vars_file(self):
|
||||
extensions = YAML_EXTENSIONS
|
||||
|
||||
for rfile in self._files_registry.get_files():
|
||||
if any(fnmatch.fnmatch(rfile, "*/defaults/*." + ext) for ext in extensions):
|
||||
with open(rfile, "r", encoding="utf8") as yaml_file:
|
||||
try:
|
||||
data = defaultdict(dict, yaml.load(yaml_file, Loader=yaml.SafeLoader))
|
||||
for key, value in data.items():
|
||||
self._data["var"][key] = {"value": {key: value}}
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
|
||||
def _parse_meta_file(self):
|
||||
extensions = YAML_EXTENSIONS
|
||||
|
||||
for rfile in self._files_registry.get_files():
|
||||
if any("meta/main." + ext in rfile for ext in extensions):
|
||||
with open(rfile, "r", encoding="utf8") as yaml_file:
|
||||
try:
|
||||
data = defaultdict(dict, yaml.load(yaml_file, Loader=yaml.SafeLoader))
|
||||
if data.get("galaxy_info"):
|
||||
for key, value in data.get("galaxy_info").items():
|
||||
self._data["meta"][key] = {"value": value}
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
|
||||
def _populate_doc_data(self):
|
||||
"""Generate the documentation data object."""
|
||||
tags = defaultdict(dict)
|
||||
for annotaion in self.config.get_annotations_names(automatic=True):
|
||||
self.log.info("Finding annotations for: @" + annotaion)
|
||||
self._annotation_objs[annotaion] = Annotation(name=annotaion, files_registry=self._files_registry)
|
||||
tags[annotaion] = self._annotation_objs[annotaion].get_details()
|
||||
# print(json.dumps(tags, indent=4, sort_keys=True))
|
||||
anyconfig.merge(self._data, tags, ac_merge=anyconfig.MS_DICTS)
|
||||
|
||||
def get_data(self):
|
||||
return self._data
|
||||
|
||||
def cli_print_section(self):
|
||||
return self.config.use_print_template
|
||||
|
||||
def cli_left_space(self, item1="", left=25):
|
||||
item1 = item1.ljust(left)
|
||||
return item1
|
||||
|
||||
def test(self):
|
||||
return "test()"
|
62
ansibledoctor/FileRegistry.py
Normal file
62
ansibledoctor/FileRegistry.py
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ansibledoctor.Config import SingleConfig
|
||||
from ansibledoctor.Contstants import YAML_EXTENSIONS
|
||||
from ansibledoctor.Utils import SingleLog
|
||||
|
||||
|
||||
class Registry:
|
||||
|
||||
_doc = {}
|
||||
log = None
|
||||
config = None
|
||||
|
||||
def __init__(self):
|
||||
self._doc = []
|
||||
self.config = SingleConfig()
|
||||
self.log = SingleLog()
|
||||
self._scan_for_yamls()
|
||||
|
||||
def get_files(self):
|
||||
return self._doc
|
||||
|
||||
def _scan_for_yamls(self):
|
||||
"""
|
||||
Search for the yaml files in each project/role root and append to the corresponding object.
|
||||
|
||||
:param base: directory in witch we are searching
|
||||
:return: None
|
||||
"""
|
||||
extensions = YAML_EXTENSIONS
|
||||
base_dir = self.config.get_base_dir()
|
||||
|
||||
self.log.debug("Scan for files: " + base_dir)
|
||||
|
||||
for extension in extensions:
|
||||
for filename in glob.iglob(base_dir + "/**/*." + extension, recursive=True):
|
||||
if self._is_excluded_yaml_file(filename, base_dir):
|
||||
self.log.trace("Excluding: " + filename)
|
||||
else:
|
||||
self.log.trace("Adding to role:" + base_dir + " => " + filename)
|
||||
self._doc.append(filename)
|
||||
|
||||
def _is_excluded_yaml_file(self, file, role_base_dir=None):
|
||||
"""
|
||||
Sub method for handling file exclusions based on the full path starts with.
|
||||
|
||||
:param file:
|
||||
:param role_base_dir:
|
||||
:return:
|
||||
"""
|
||||
base_dir = role_base_dir
|
||||
excluded = self.config.excluded_roles_dirs.copy()
|
||||
|
||||
is_filtered = False
|
||||
for excluded_dir in excluded:
|
||||
if file.startswith(base_dir + "/" + excluded_dir):
|
||||
is_filtered = True
|
||||
|
||||
return is_filtered
|
123
ansibledoctor/Utils.py
Normal file
123
ansibledoctor/Utils.py
Normal file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/python3
|
||||
import os
|
||||
import pprint
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Log:
|
||||
levels = {
|
||||
"trace": -1,
|
||||
"debug": 0,
|
||||
"info": 1,
|
||||
"warn": 2,
|
||||
"error": 3,
|
||||
}
|
||||
log_level = 1
|
||||
|
||||
def __init__(self, level=1):
|
||||
self.set_level(level)
|
||||
|
||||
def set_level(self, s):
|
||||
|
||||
if isinstance(s, str):
|
||||
for level, v in self.levels.items():
|
||||
if level == s:
|
||||
self.log_level = v
|
||||
elif isinstance(s, int):
|
||||
if s in range(4):
|
||||
self.log_level = s
|
||||
|
||||
def trace(self, msg, h=""):
|
||||
if self.log_level <= -1:
|
||||
self._p("*TRACE*: " + h, msg)
|
||||
|
||||
def debug(self, msg, h=""):
|
||||
if self.log_level <= 0:
|
||||
self._p("*DEBUG*: " + h, msg)
|
||||
|
||||
def info(self, msg, h=""):
|
||||
if self.log_level <= 1:
|
||||
self._p("*INFO*: " + h, msg)
|
||||
|
||||
def warn(self, msg, h=""):
|
||||
if self.log_level <= 2:
|
||||
self._p("*WARN*: " + h, msg)
|
||||
|
||||
def error(self, msg, h=""):
|
||||
if self.log_level <= 3:
|
||||
self._p("*ERROR*: " + h, msg)
|
||||
|
||||
@staticmethod
|
||||
def _p(head, msg, print_type=True):
|
||||
|
||||
if isinstance(msg, list):
|
||||
t = " <list>" if print_type else ""
|
||||
print(head + t)
|
||||
i = 0
|
||||
for line in msg:
|
||||
print(" [" + str(i) + "]: " + str(line))
|
||||
i += 1
|
||||
|
||||
elif isinstance(msg, dict):
|
||||
t = " <dict>" if print_type else ""
|
||||
print(head + t)
|
||||
pprint.pprint(msg)
|
||||
else:
|
||||
print(head + str(msg))
|
||||
|
||||
@staticmethod
|
||||
def print(msg, data):
|
||||
Log._p(msg, data, False)
|
||||
|
||||
|
||||
class SingleLog(Log, metaclass=Singleton):
|
||||
pass
|
||||
|
||||
|
||||
class FileUtils:
|
||||
@staticmethod
|
||||
def create_path(path):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
# http://code.activestate.com/recipes/577058/
|
||||
@staticmethod
|
||||
def query_yes_no(question, default="yes"):
|
||||
"""Ask a yes/no question via input() and return their answer.
|
||||
|
||||
"question" is a string that is presented to the user.
|
||||
"default" is the presumed answer if the user just hits <Enter>.
|
||||
It must be "yes" (the default), "no" or None (meaning
|
||||
an answer is required of the user).
|
||||
|
||||
The "answer" return value is one of "yes" or "no".
|
||||
"""
|
||||
valid = {"yes": "yes", "y": "yes", "ye": "yes",
|
||||
"no": "no", "n": "no"}
|
||||
if default is None:
|
||||
prompt = " [y/n] "
|
||||
elif default == "yes":
|
||||
prompt = " [Y/n] "
|
||||
elif default == "no":
|
||||
prompt = " [y/N] "
|
||||
else:
|
||||
raise ValueError("Invalid default answer: '%s'" % default)
|
||||
|
||||
while 1:
|
||||
choice = input(question + prompt).lower()
|
||||
if default is not None and choice == "":
|
||||
return default
|
||||
elif choice in valid.keys():
|
||||
return valid[choice]
|
||||
else:
|
||||
sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n")
|
9
ansibledoctor/__init__.py
Normal file
9
ansibledoctor/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""Default package."""
|
||||
|
||||
__author__ = "Robert Kaussow"
|
||||
__project__ = "ansible-doctor"
|
||||
__version__ = "0.1.0"
|
||||
__license__ = "LGPLv3"
|
||||
__maintainer__ = "Robert Kaussow"
|
||||
__email__ = "mail@geeklabor.de"
|
||||
__status__ = "Production"
|
10
ansibledoctor/__main__.py
Normal file
10
ansibledoctor/__main__.py
Normal file
@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Main program."""
|
||||
|
||||
from ansibledoctor.Cli import AnsibleDoctor
|
||||
|
||||
def main():
|
||||
doc = AnsibleDoctor()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
15
ansibledoctor/templates/cliprint/_action.j2
Normal file
15
ansibledoctor/templates/cliprint/_action.j2
Normal file
@ -0,0 +1,15 @@
|
||||
# ============================================================
|
||||
# Actions (variables: action)
|
||||
# ============================================================
|
||||
{% for role in r.get_roles(False) %}
|
||||
{% if r.get_type("action",role) %}
|
||||
{{ r.capitalize(r.fprn(role)) }}:
|
||||
{% endif %}
|
||||
{##}
|
||||
{% for key , values in r.get_multi_type("action",role) %}
|
||||
{{ r.capitalize(key) }}:
|
||||
{% for item in values %}
|
||||
* {{ item.desc }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
10
ansibledoctor/templates/cliprint/_description.j2
Normal file
10
ansibledoctor/templates/cliprint/_description.j2
Normal file
@ -0,0 +1,10 @@
|
||||
# ============================================================
|
||||
# Project Description
|
||||
# ============================================================
|
||||
{% for role in r.get_roles(False) %}
|
||||
{{ r.capitalize(r.fprn(role)) }}:
|
||||
{% for item in r.get_type("meta",role) %}
|
||||
{{ r.cli_left_space(r.capitalize(item.key),25) }} {{ item.value }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
19
ansibledoctor/templates/cliprint/_tags.j2
Normal file
19
ansibledoctor/templates/cliprint/_tags.j2
Normal file
@ -0,0 +1,19 @@
|
||||
# ============================================================
|
||||
# Tags (variable: tag)
|
||||
# ============================================================
|
||||
{% for role in r.get_roles(False) %}
|
||||
{{ r.capitalize(r.fprn(role)) }}:
|
||||
{% for item in r.get_type("tag",role) %}
|
||||
{{ r.cli_left_space(" * "+item.key,25) }} {{ r.capitalize(item.desc) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{#{{ tag | pprint }}#}
|
||||
Duplicate Tags:
|
||||
{% for k,v in r.get_duplicates("tag") %}
|
||||
{{ " * "+k }} in files:
|
||||
{% for item in v %}
|
||||
{{ item.file }} {% if item.line != "" %}(line: {{ item.line }}) {% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endfor %}
|
26
ansibledoctor/templates/cliprint/_todo.j2
Normal file
26
ansibledoctor/templates/cliprint/_todo.j2
Normal file
@ -0,0 +1,26 @@
|
||||
# ============================================================
|
||||
# Todo (variables: todo)
|
||||
# ============================================================
|
||||
{% for role in r.get_roles(False) %}
|
||||
{% if r.get_type("todo",role) %}
|
||||
|
||||
{{ r.capitalize(r.fprn(role)) }}:
|
||||
{% endif %}
|
||||
{##}
|
||||
{% for key , values in r.get_multi_type("todo",role) %}
|
||||
{% if key == "_unset_" %}
|
||||
Todos without section:
|
||||
{% for item in values %}
|
||||
* {{ item.desc }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for key , values in r.get_multi_type("todo",role) %}
|
||||
{% if key != "_unset_" %}
|
||||
{{ r.capitalize(key) }}:
|
||||
{% for item in values %}
|
||||
* {{ item.desc }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
22
ansibledoctor/templates/cliprint/_var.j2
Normal file
22
ansibledoctor/templates/cliprint/_var.j2
Normal file
@ -0,0 +1,22 @@
|
||||
# ============================================================
|
||||
# Variables (variable: var)
|
||||
# ============================================================
|
||||
{% for role in r.get_roles(False) %}
|
||||
{% if r.get_type("var",role) %}
|
||||
{{ r.capitalize(r.fprn(role)) }}:
|
||||
{% endif %}
|
||||
{% for item in r.get_type("var",role) %}
|
||||
{{ r.cli_left_space(" * "+ item.key+": "+item.value,35) }} {{ item.desc }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
Duplicate Vars:
|
||||
{% for k,v in r.get_duplicates("var") %}
|
||||
{{ " * "+k }} in files:
|
||||
{% for item in v %}
|
||||
{{ item.file }} {% if item.line != "" %}(line: {{ item.line }}) {% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{#{{ var | pprint }}#}
|
21
ansibledoctor/templates/cliprint/print_to_cli.j2
Normal file
21
ansibledoctor/templates/cliprint/print_to_cli.j2
Normal file
@ -0,0 +1,21 @@
|
||||
### CLI tempate ###
|
||||
|
||||
{% if r.cli_print_section() == "all" or r.cli_print_section() == "info" %}
|
||||
{% include '_description.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if r.cli_print_section() == "all" or r.cli_print_section() == "action" %}
|
||||
{% include '_action.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if r.cli_print_section() == "all" or r.cli_print_section() == "tag" %}
|
||||
{% include '_tags.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if r.cli_print_section() == "all" or r.cli_print_section() == "todo" %}
|
||||
{% include '_todo.j2' %}
|
||||
{% endif %}
|
||||
|
||||
{% if r.cli_print_section() == "all" or r.cli_print_section() == "var" %}
|
||||
{% include '_var.j2' %}
|
||||
{% endif %}
|
9
ansibledoctor/templates/readme/README.md.j2
Normal file
9
ansibledoctor/templates/readme/README.md.j2
Normal file
@ -0,0 +1,9 @@
|
||||
{% set meta = role.meta | default({}) %}
|
||||
# {{ (meta.name | default({"value": "_undefined_"})).value }}
|
||||
|
||||
{% if meta.description is defined %}
|
||||
{{ meta.description.value }}
|
||||
{% endif %}
|
||||
|
||||
{# Vars #}
|
||||
{% include '_vars.j2' %}
|
4
ansibledoctor/templates/readme/_dev_var_dump.txt.j2
Normal file
4
ansibledoctor/templates/readme/_dev_var_dump.txt.j2
Normal file
@ -0,0 +1,4 @@
|
||||
#============================================================================================================
|
||||
# This is a dump of the documentation variable : tags
|
||||
#============================================================================================================
|
||||
{{ tag | pprint }}
|
39
ansibledoctor/templates/readme/_vars.j2
Normal file
39
ansibledoctor/templates/readme/_vars.j2
Normal file
@ -0,0 +1,39 @@
|
||||
{% set var = role.var | default({}) %}
|
||||
{% if var %}
|
||||
## Default Variables
|
||||
|
||||
{% for key, item in var.items() %}
|
||||
|
||||
### {{ key }}
|
||||
{% if item.description is defined and item.description %}
|
||||
|
||||
{% for desc_line in item.description %}
|
||||
{{ desc_line }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
#### Default value
|
||||
|
||||
```YAML
|
||||
{{ item.value | to_nice_yaml(indent=2) }}
|
||||
```
|
||||
|
||||
{% if item.example is defined and item.example %}
|
||||
|
||||
#### Example usage
|
||||
|
||||
```YAML
|
||||
{% if item.example is mapping %}
|
||||
{{ item.example | to_nice_yaml(indent=2) }}
|
||||
{% else %}
|
||||
{% for ex_line in item.example %}
|
||||
{{ ex_line }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
{% endif %}
|
||||
|
||||
---
|
||||
{% endfor %}
|
||||
{% endif %}
|
7
bin/ansible-doctor
Executable file
7
bin/ansible-doctor
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
|
||||
import ansibledoctor.__main__
|
||||
|
||||
sys.exit(ansibledoctor.__main__.main())
|
20
setup.cfg
Normal file
20
setup.cfg
Normal file
@ -0,0 +1,20 @@
|
||||
[metadata]
|
||||
description-file = README.md
|
||||
license_file = LICENSE
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
|
||||
[isort]
|
||||
default_section = THIRDPARTY
|
||||
known_first_party = ansibledoctor
|
||||
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
||||
force_single_line = true
|
||||
line_length = 120
|
||||
skip_glob = **/env/*
|
||||
|
||||
[tool:pytest]
|
||||
filterwarnings =
|
||||
ignore::FutureWarning
|
||||
ignore:.*collections.*:DeprecationWarning
|
||||
ignore:.*pep8.*:FutureWarning
|
72
setup.py
Executable file
72
setup.py
Executable file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/python3
|
||||
"""Setup script for the package."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
PACKAGE_NAME = "ansibledoctor"
|
||||
|
||||
|
||||
def get_property(prop, project):
|
||||
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
result = re.search(
|
||||
r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop),
|
||||
open(os.path.join(current_dir, project, "__init__.py")).read())
|
||||
return result.group(1)
|
||||
|
||||
|
||||
def get_readme(filename="README.md"):
|
||||
this = os.path.abspath(os.path.dirname(__file__))
|
||||
with io.open(os.path.join(this, filename), encoding="utf-8") as f:
|
||||
long_description = f.read()
|
||||
return long_description
|
||||
|
||||
|
||||
setup(
|
||||
name=get_property("__project__", PACKAGE_NAME),
|
||||
version=get_property("__version__", PACKAGE_NAME),
|
||||
description="Generate documentation from annotated Ansible roles using templates",
|
||||
keywords="ansible role documentation",
|
||||
author=get_property("__author__", PACKAGE_NAME),
|
||||
author_email=get_property("__email__", PACKAGE_NAME),
|
||||
url="https://github.com/xoxys/ansible-doctor",
|
||||
license=get_property("__license__", PACKAGE_NAME),
|
||||
long_description=get_readme(),
|
||||
long_description_content_type="text/markdown",
|
||||
packages=find_packages(exclude=["*.tests", "tests", "tests.*"]),
|
||||
python_requires=">=3.5",
|
||||
classifiers=[
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: Information Technology",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: POSIX",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Topic :: System :: Installation/Setup",
|
||||
"Topic :: System :: Systems Administration",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Software Development",
|
||||
"Topic :: Software Development :: Documentation",
|
||||
],
|
||||
install_requires=[
|
||||
"pyyaml",
|
||||
"ruamel.yaml",
|
||||
"appdirs",
|
||||
"colorama",
|
||||
"anyconfig",
|
||||
"python-json-logger",
|
||||
"jsonschema",
|
||||
"jinja2"
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"ansible-doctor = ansibledoctor.__main__:main"
|
||||
]
|
||||
},
|
||||
test_suite="tests"
|
||||
)
|
19
test-requirements.txt
Normal file
19
test-requirements.txt
Normal file
@ -0,0 +1,19 @@
|
||||
# open issue
|
||||
# https://gitlab.com/pycqa/flake8-docstrings/issues/36
|
||||
pydocstyle<4.0.0
|
||||
flake8
|
||||
flake8-colors
|
||||
flake8-blind-except
|
||||
flake8-builtins
|
||||
flake8-colors
|
||||
flake8-docstrings<=3.0.0
|
||||
flake8-isort
|
||||
flake8-logging-format
|
||||
flake8-polyfill
|
||||
flake8-quotes
|
||||
pep8-naming
|
||||
wheel
|
||||
pytest
|
||||
pytest-mock
|
||||
pytest-cov
|
||||
bandit
|
Loading…
Reference in New Issue
Block a user