793 lines
25 KiB
Python
Executable File
793 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
'''
|
||
This script renders Jinja comments found at the beginning
|
||
of `.sls` files as state documentation.
|
||
|
||
The code is horrible, but I'm too lazy to rewrite it properly
|
||
because it works for my use cases. There will likely be a lot of bugs.
|
||
|
||
Full configuration in ``pyproject.toml`` with defaults:
|
||
|
||
[tool.slsdoc]
|
||
# Choose which renderer to use. `single` or `depth`
|
||
renderer = "single"
|
||
# Also list states without documentation
|
||
include_empty = false
|
||
# Path prefixes to ignore, relative to formula root
|
||
ignore = ["_mapdata"]
|
||
# Given a directory with `package.init`, `package.install`
|
||
# and `package.clean`, only dump init and clean.
|
||
avoid_init_dup = True
|
||
# By default, states are ordered alphabetically, but init
|
||
# always comes first and all clean states are separated.
|
||
# This overrides the purely alphabetical order for the roots.
|
||
sort_list = ["init", "clean", "package", "config", "service"]
|
||
|
||
[tool.slsdoc.single_renderer]
|
||
# Include init.sls files in state list
|
||
init = true
|
||
# Include .init suffix in when init is true
|
||
init_suffix = false
|
||
# Include clean.sls files in state list
|
||
clean = true
|
||
# When rendering with prefix `settings.something`,
|
||
# write `else` instead of `settings.something.else`
|
||
cut_prefix = false
|
||
# Write `salt.package` instead of `package` when formula root == `salt`
|
||
include_formula_root = true
|
||
# Default .rst main header, when not defined in root init.sls
|
||
default_header = "Available States"
|
||
# Header, overrides everything
|
||
header = ""
|
||
# Description, overrides everything
|
||
description = ""
|
||
# Default description, when init.sls is empty
|
||
default_description = """
|
||
.. contents::
|
||
:local:
|
||
"""
|
||
# Always append default description, regardless of
|
||
# presence in init.sls
|
||
always_include_default_description = false
|
||
# Character to use to underline main headings
|
||
main_heading_char = "="
|
||
# Character to use to underline sub headings
|
||
sub_heading_char = "^"
|
||
# Make state IDs literal – ``state.id`` instead of state.id
|
||
literal_sub_heading = false
|
||
# File to write to
|
||
target = "docs/states.rst"
|
||
|
||
[tool.slsdoc.depth_renderer]
|
||
# maxdepth for generated index.rst files (toctree)
|
||
index_maxdepth = 4
|
||
# Directory to write to
|
||
target = "docs/states"
|
||
|
||
[tool.slsdoc.depth_renderer.single_opts]
|
||
# Include init.sls files in state list
|
||
init = false
|
||
# Include .init suffix in when init is true
|
||
init_suffix = false
|
||
# Include clean.sls files in state list
|
||
clean = false
|
||
# When rendering with prefix `settings.something`,
|
||
# write `else` instead of `settings.something.else`
|
||
cut_prefix = true
|
||
# Write `salt.package` instead of `package` when formula root == `salt`
|
||
include_formula_root = false
|
||
'''
|
||
|
||
from pathlib import Path
|
||
|
||
import toml
|
||
from jinja2 import Environment
|
||
from jinja2.exceptions import TemplateSyntaxError
|
||
|
||
# import sphinx.util.docstrings as d
|
||
|
||
base_dir = Path(__file__).parent.parent.absolute()
|
||
|
||
|
||
class State:
|
||
"""
|
||
Object representing a documented state.
|
||
"""
|
||
|
||
def __init__(self, path, comment, formula_root):
|
||
if not isinstance(path, Path):
|
||
path = Path(path)
|
||
self.path = path
|
||
self.comment = str(comment)
|
||
self.formula_root = formula_root
|
||
|
||
def to_state_id(
|
||
self, include_init=True, relative_to="", split=False, formula_root=False
|
||
):
|
||
"""
|
||
Return this state's state ID.
|
||
|
||
include_init
|
||
If False, remove ``.init`` suffix. Defaults to True.
|
||
|
||
relative_to
|
||
Render the state ID relative to another state or file.
|
||
|
||
split
|
||
Return a tuple of parts instead of a string.
|
||
|
||
formula_root
|
||
If not using relative_to, include the formula's root
|
||
directory in the state ID (the actually correct one).
|
||
Defaults to False (for compatibility reasons).
|
||
"""
|
||
ret = self.path.with_suffix("")
|
||
|
||
if not include_init and ret.parts[-1] == "init":
|
||
ret = ret.parent
|
||
|
||
if relative_to:
|
||
relative_to = Path(relative_to.replace(".", "/"))
|
||
ret = ret.relative_to(relative_to)
|
||
elif formula_root:
|
||
ret = Path(self.formula_root) / ret
|
||
|
||
ret = str(ret).replace("/", ".")
|
||
if not split:
|
||
return ret
|
||
return tuple(ret.split("."))
|
||
|
||
def is_init(self):
|
||
"""
|
||
Checks whether this is an ``.init`` state.
|
||
"""
|
||
return self.path.parts[-1] == "init.sls"
|
||
|
||
def is_clean(self):
|
||
"""
|
||
Checks whether this is a ``.clean`` state.
|
||
"""
|
||
return self.path.parts[-1] == "clean.sls"
|
||
|
||
def __str__(self):
|
||
return self.to_state_id()
|
||
|
||
|
||
class StateRegistry:
|
||
"""
|
||
Registers State objects. It generates several indices:
|
||
|
||
1. nested dictionary containing the states
|
||
2. flattened list of states
|
||
3. flattened list of prefixes ~ directories containing .sls files
|
||
|
||
All of them are guaranteed to be sorted, which is required
|
||
by some of the downstream classes.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
data=None,
|
||
states=None,
|
||
prefixes=None,
|
||
default_sort_fn=None,
|
||
avoid_init_dup=True,
|
||
sort_list=None,
|
||
):
|
||
# nested dict
|
||
self.reg = data or {}
|
||
# flat dict with ptr to reg
|
||
self.states = states or {}
|
||
# flat dict with ptr to reg
|
||
self.prefixes = prefixes or {}
|
||
self.default_sort_fn = default_sort_fn or self._default_sort
|
||
# ensure no prefix lists the root
|
||
self.prefixes[()] = self.reg
|
||
self.avoid_init_dup = avoid_init_dup
|
||
self.sort_list = tuple(
|
||
sort_list or ("init", "clean", "package", "config", "service")
|
||
)
|
||
|
||
def _default_sort(self, a):
|
||
"""
|
||
Sort prefixes package > config > service, rest alphabetically.
|
||
Init always comes first. Clean states are separated.
|
||
"""
|
||
sort_key = ()
|
||
if a[0] in self.sort_list:
|
||
sort_key += (str(self.sort_list.index(a[0])),)
|
||
else:
|
||
sort_key += (str(len(self.sort_list)),)
|
||
if a[-1] == "init":
|
||
sort_key += a[:-1]
|
||
elif a[-1] == "clean":
|
||
sort_key = ("zzzzzzzzz",) + sort_key + a[:-1]
|
||
else:
|
||
sort_key += a
|
||
return sort_key
|
||
|
||
def lookup(self, state, tree=False):
|
||
"""
|
||
Looks up State objects by ID ('some.example' or ['some', 'example']).
|
||
If tree, also returns pointers to dicts inside the nested registry.
|
||
"""
|
||
ptr = self.reg
|
||
if isinstance(state, str):
|
||
state = state.split(".")
|
||
state = list(state)
|
||
|
||
try:
|
||
while state:
|
||
cur = state.pop(0)
|
||
ptr = ptr[cur]
|
||
except KeyError:
|
||
return False
|
||
|
||
if isinstance(ptr, State) or tree:
|
||
return ptr
|
||
# map init.sls same way as salt does
|
||
if "init" in ptr:
|
||
return ptr["init"]
|
||
|
||
raise ValueError("State could not be found. Most likely, this is a prefix.")
|
||
|
||
def register(self, state):
|
||
"""
|
||
Register a state object.
|
||
"""
|
||
try:
|
||
state_id = state.to_state_id(split=True)
|
||
except AttributeError as err:
|
||
raise ValueError("`state` argument must be an instance of `State`") from err
|
||
|
||
ptr = self.reg
|
||
prefix = []
|
||
parts = list(state_id)
|
||
|
||
while len(parts):
|
||
cur = parts.pop(0)
|
||
prefix = prefix + [cur]
|
||
if not parts:
|
||
ptr[cur] = state
|
||
break
|
||
# make sure subdicts are initialized
|
||
if cur not in ptr:
|
||
ptr[cur] = {}
|
||
self.prefixes[tuple(prefix)] = ptr[cur]
|
||
ptr = ptr[cur]
|
||
self.states[state_id] = ptr[cur]
|
||
|
||
def unregister(self, state):
|
||
"""
|
||
Unregister a state object.
|
||
"""
|
||
try:
|
||
state_id = state.to_state_id(split=True)
|
||
except AttributeError as err:
|
||
raise ValueError("`state` argument must be an instance of `State`") from err
|
||
try:
|
||
self.states.pop(state_id)
|
||
except KeyError as err:
|
||
raise RuntimeError(f"Specified state {state} was not registered") from err
|
||
|
||
parts = list(state_id)
|
||
first_run = True
|
||
|
||
while parts:
|
||
key = parts.pop()
|
||
parent = self.lookup(parts, tree=True)
|
||
if first_run:
|
||
del parent[key]
|
||
elif not len(parent[key]):
|
||
self.prefixes.pop(tuple(parts + [key]))
|
||
del parent[key]
|
||
first_run = False
|
||
|
||
def depth(self, prefix=""):
|
||
"""
|
||
Returns depth of nested dict. Optionally from a specified prefix.
|
||
"""
|
||
d = self.lookup(prefix, tree=True) if prefix else self.reg
|
||
return self._get_depth(d)
|
||
|
||
def _get_depth(self, d):
|
||
"""
|
||
Recursively look up depth.
|
||
"""
|
||
if isinstance(d, dict) and set(d.keys()) - {"init", "clean"}:
|
||
return 1 + (max(map(self._get_depth, d.values())) if d else 0)
|
||
return 0
|
||
|
||
def count(self):
|
||
"""
|
||
Return the number of registered states.
|
||
"""
|
||
return len(self.states)
|
||
|
||
def dump(self, prefix=None, sort_fn=None):
|
||
"""
|
||
Returns all states sorted, optionally beginning from a prefix only.
|
||
"""
|
||
prefix = prefix or ()
|
||
if isinstance(prefix, str):
|
||
prefix = tuple(prefix.split("."))
|
||
for state_id in sorted(self.states, key=sort_fn or self.default_sort_fn):
|
||
if state_id[: len(prefix)] == prefix:
|
||
if self.avoid_init_dup and state_id[-1] not in ["init", "clean"]:
|
||
# given package.init, package.install and package.clean,
|
||
# only dump init and clean
|
||
parent = self.lookup(state_id[:-1], tree=True)
|
||
if len(parent) == 3 and "init" in parent and "clean" in parent:
|
||
continue
|
||
yield self.states[state_id]
|
||
|
||
|
||
class Renderer:
|
||
"""
|
||
Base class for Renderer objects.
|
||
"""
|
||
|
||
_default_opts = {
|
||
"default_description": """\
|
||
The following states are found in {where}:
|
||
|
||
.. contents::
|
||
:local:
|
||
""",
|
||
"always_include_default_description": False,
|
||
"default_header": "Available States",
|
||
"main_heading_char": "=",
|
||
"sub_heading_char": "-",
|
||
"literal_sub_heading": False,
|
||
}
|
||
|
||
def __init__(self, target="docs", prefix=None, basedir=base_dir, opts=None):
|
||
self.opts = self._default_opts | (opts or {})
|
||
self.target = basedir / target
|
||
if isinstance(prefix, str):
|
||
prefix = tuple(prefix.split("."))
|
||
self.prefix = prefix or ()
|
||
self.basedir = basedir
|
||
self._ensurepath(self.target)
|
||
|
||
def render(self, states):
|
||
raise NotImplementedError()
|
||
|
||
def _write(self, text, ncount=1):
|
||
"""
|
||
Convenience method to mimic writeln
|
||
This is bad form though, implicitly requires
|
||
self.f to be set before being called
|
||
"""
|
||
if not hasattr(self, "f"):
|
||
raise ValueError(
|
||
"Sorry, you need to set self.f to a file pointer "
|
||
"before calling self._write methods."
|
||
)
|
||
self.f.write(text + "\n" * ncount)
|
||
|
||
def _write_heading(self, text, uline=None):
|
||
"""
|
||
Convenience method to write a subheading
|
||
"""
|
||
if self.opts["literal_sub_heading"]:
|
||
text = f"``{text}``"
|
||
self._write(text)
|
||
self._write((uline or self.opts["sub_heading_char"]) * len(text))
|
||
|
||
def _write_main_heading(self, text, uline=None):
|
||
"""
|
||
Convenience method to write the main heading
|
||
"""
|
||
self.f.write(text + "\n")
|
||
self.f.write((uline or self.opts["main_heading_char"]) * len(text) + "\n\n")
|
||
|
||
def _write_main_description(self, text):
|
||
"""
|
||
Convenience method to write the main description
|
||
"""
|
||
self.f.write(text + "\n")
|
||
|
||
def _get_main_heading(self, states):
|
||
"""
|
||
Returns a string to be used as the main heading of an
|
||
rst file. Looks up in the following order:
|
||
1. If there is a documented root init.sls, return its first line.
|
||
2. If a header was specified in opts, return that.
|
||
3. If this renderer was instantiated with a prefix, return the
|
||
capitalized last part of it.
|
||
4. "Available States"
|
||
"""
|
||
if header := self.opts.get("header"):
|
||
return header
|
||
if (main := states.lookup(self.prefix + ("init",))) and main.comment:
|
||
lines = main.comment.splitlines()
|
||
header = lines.pop(0)
|
||
# make sure the header does not get printed twice
|
||
main.comment = "\n".join(lines).strip("\n")
|
||
return header
|
||
if self.prefix:
|
||
return self.prefix[-1].title()
|
||
if header := self.opts.get("default_header"):
|
||
return header
|
||
return self.opts["default_header"]
|
||
|
||
def _get_main_description(self, states):
|
||
"""
|
||
Returns a string to be used as the main description (after the main header).
|
||
Looks up in the following order:
|
||
1. If there is a documented root init.sls, return everything but its first line.
|
||
If it was found, unregister it to not render it twice
|
||
(bad form again, maybe consider was_rendered on State)
|
||
2. If a description was specified in opts, return that.
|
||
3. general description
|
||
"""
|
||
|
||
def get_default_desc():
|
||
return self.opts["default_description"].format(
|
||
where=".".join(self.prefix) if self.prefix else "this formula"
|
||
)
|
||
|
||
if desc := self.opts.get("description"):
|
||
return desc
|
||
if main := states.lookup(self.prefix + ("init",)):
|
||
desc = main.comment
|
||
if self.opts.get("always_include_default_description"):
|
||
desc += "\n" + get_default_desc()
|
||
return desc
|
||
return get_default_desc()
|
||
|
||
def _write_header(self, states):
|
||
"""
|
||
Convenience method to write the main heading and description.
|
||
"""
|
||
self._write_main_heading(self._get_main_heading(states))
|
||
self._write_main_description(self._get_main_description(states))
|
||
self.f.write("\n")
|
||
|
||
def _ensurepath(self, path):
|
||
"""
|
||
Convenience method to mimic mkdir -p on the parent dir of a path
|
||
"""
|
||
if not path.parent.is_dir():
|
||
path.parent.mkdir(parents=True)
|
||
|
||
|
||
class SingleRenderer(Renderer):
|
||
"""
|
||
Renders documented Salt states in a single file. When a prefix
|
||
is specified, it only considers states nested inside that prefix.
|
||
|
||
If there is a documented top-layer init.sls, the first line of
|
||
its comment will become the main heading of the file, the stripped
|
||
rest will be rendered directly below before any nested states as
|
||
the main description.
|
||
|
||
Should have been a Jinja template @TODO
|
||
"""
|
||
|
||
default_opts = {
|
||
# include init.sls files
|
||
"init": True,
|
||
# include .init suffix
|
||
"init_suffix": False,
|
||
# include clean.sls files
|
||
"clean": True,
|
||
# when rendering with prefix `settings.something`,
|
||
# write `else` instead of `settings.something.else`
|
||
"cut_prefix": False,
|
||
"include_formula_root": True,
|
||
}
|
||
|
||
def __init__(
|
||
self, target="docs/states.rst", prefix=None, basedir=base_dir, **kwargs
|
||
):
|
||
if isinstance(target, str):
|
||
target = Path(target)
|
||
target = target.with_suffix(".rst")
|
||
super().__init__(
|
||
target=target,
|
||
prefix=prefix,
|
||
basedir=basedir,
|
||
opts=self.default_opts | kwargs,
|
||
)
|
||
|
||
def render(self, states):
|
||
relative_to = "/".join(self.prefix) if self.opts["cut_prefix"] else ""
|
||
|
||
with open(self.target, "w") as f:
|
||
self.f = f
|
||
|
||
self._write_header(states)
|
||
|
||
for state in states.dump(self.prefix):
|
||
if not self.opts["init"] and state.is_init():
|
||
continue
|
||
if not self.opts["clean"] and state.is_clean():
|
||
continue
|
||
state_id = state.to_state_id(
|
||
self.opts["init_suffix"],
|
||
relative_to,
|
||
formula_root=self.opts["include_formula_root"],
|
||
)
|
||
if state_id == ".":
|
||
continue
|
||
self._write_heading(
|
||
state.to_state_id(
|
||
self.opts["init_suffix"],
|
||
relative_to,
|
||
formula_root=self.opts["include_formula_root"],
|
||
)
|
||
)
|
||
self._write(state.comment, 3)
|
||
|
||
|
||
class DepthRenderer(Renderer):
|
||
"""
|
||
Renders documented Salt states in separate files. They are combined
|
||
according to the maximum nesting depth specified during instantiation.
|
||
|
||
Consider an example with max_depth=1.
|
||
|
||
This directory structure:
|
||
settings
|
||
├── animations
|
||
│ ├── cursor_blinking
|
||
│ └── window_resize_time
|
||
└── apps
|
||
└── messages
|
||
└── read_receipts
|
||
|
||
will result in these files:
|
||
* docs/settings/index.rst
|
||
- animations
|
||
- apps/index
|
||
* docs/settings/animations.rst
|
||
- cursor_blinking doc
|
||
- window_resize_time doc
|
||
* docs/settings/apps/index.rst
|
||
- messages
|
||
* docs/settings/apps/messages.rst
|
||
- read_receipts doc
|
||
"""
|
||
|
||
default_opts = {
|
||
# maxdepth for generated index.rst files
|
||
"index_maxdepth": 4,
|
||
"single_opts": {
|
||
"init": True,
|
||
"init_suffix": False,
|
||
"clean": False,
|
||
"cut_prefix": True,
|
||
"include_formula_root": False,
|
||
},
|
||
}
|
||
|
||
def __init__(
|
||
self,
|
||
max_depth=1,
|
||
target="docs/states",
|
||
delegate_cls=None,
|
||
basedir=base_dir,
|
||
**kwargs,
|
||
):
|
||
self.max_depth = max_depth
|
||
self.delegate = delegate_cls or SingleRenderer
|
||
super().__init__(
|
||
target=target, basedir=basedir, opts=self.default_opts | (kwargs or {})
|
||
)
|
||
|
||
def render(self, states):
|
||
found = []
|
||
indices = {}
|
||
cur_top = ()
|
||
|
||
# Find at which prefix length the maxdepth is fulfilled
|
||
for prefix in states.prefixes:
|
||
# Filtering out more specific = shallower dicts
|
||
# This requires the list to be sorted!
|
||
# This also goes not too well with rendering clean states
|
||
# by default since they will be separate.
|
||
if cur_top:
|
||
if prefix[: len(cur_top)] == cur_top:
|
||
continue
|
||
cur_top = ()
|
||
if states.depth(prefix) <= self.max_depth:
|
||
found.append(prefix)
|
||
cur_top = prefix
|
||
|
||
# fill indices with relative links to
|
||
# a) other indices if there are nested prefixes
|
||
# b) the rendered files at that depth
|
||
# example: settings.one settings.two.[subone, subtwo] =>
|
||
# { settings: {'_index': ['one', 'two/index'],
|
||
# 'two': {'_index': [subone, subtwo]}},
|
||
# '_index': 'settings/index'}
|
||
|
||
for prefix in found:
|
||
ptr = indices
|
||
prefix = list(prefix)
|
||
|
||
while prefix:
|
||
cur = prefix.pop(0)
|
||
if prefix and cur not in ptr:
|
||
ptr[cur] = {}
|
||
if "_index" not in ptr:
|
||
ptr["_index"] = []
|
||
if prefix and cur + "/index" not in ptr["_index"]:
|
||
ptr["_index"].append(cur + "/index")
|
||
elif not prefix and cur not in ptr["_index"]:
|
||
ptr["_index"].append(cur)
|
||
if prefix:
|
||
ptr = ptr[cur]
|
||
|
||
if "_index" in indices:
|
||
self._render_index(states, indices)
|
||
|
||
for prefix in found:
|
||
self._render_single(states, prefix)
|
||
|
||
def _render_single(self, states, prefix):
|
||
target = self._get_target(prefix)
|
||
rendr = self.delegate(
|
||
target=target, prefix=prefix, **self.opts["single_opts"]
|
||
)
|
||
rendr.render(states)
|
||
|
||
def _render_index(self, states, indices, prefix=None):
|
||
prefix = prefix or ()
|
||
target = self._get_target(prefix) / Path("index.rst")
|
||
with open(target, "w") as f:
|
||
self.f = f
|
||
self.prefix = prefix
|
||
self._write_header(states)
|
||
self.prefix = ()
|
||
f.write(
|
||
f"""
|
||
.. toctree::
|
||
:maxdepth: {self.opts['index_maxdepth']}
|
||
:caption: Contents:
|
||
|
||
"""
|
||
)
|
||
for s in sorted(indices["_index"]):
|
||
f.write(" " + s + "\n")
|
||
for sub in sorted(indices):
|
||
if sub == "_index":
|
||
continue
|
||
if "_index" in indices[sub]:
|
||
new_prefix = prefix + (sub,)
|
||
self._render_index(states, indices[sub], new_prefix)
|
||
|
||
def _get_target(self, prefix):
|
||
target_prefix = Path("/".join(prefix))
|
||
target = self.target / target_prefix
|
||
self._ensurepath(target)
|
||
return target
|
||
|
||
|
||
class SaltAutodoc:
|
||
"""
|
||
Looks for salt state files (*.sls) relative to base,
|
||
reads them and looks for the first token being a Jinja
|
||
comment. If the comment does not start with the string
|
||
'@internal', it will be saved in the registry.
|
||
|
||
The registry is then passed to the renderer.
|
||
"""
|
||
|
||
SKIP_FILE = "@internal"
|
||
|
||
def __init__(
|
||
self,
|
||
base=None,
|
||
rendr=None,
|
||
registry=None,
|
||
env=None,
|
||
include_empty=False,
|
||
ignore=None,
|
||
):
|
||
"""
|
||
base: base path containing Salt states to autodocument
|
||
renderer: renderer to use for rendering
|
||
"""
|
||
if base is None:
|
||
base = list(base_dir.glob("*/init.sls"))[0].parent
|
||
self.base = base
|
||
self.renderer = rendr or SingleRenderer()
|
||
self.env = env or Environment()
|
||
self.registry = registry or StateRegistry()
|
||
self.include_empty = include_empty
|
||
self.ignore = ignore or []
|
||
|
||
def run(self, src=""):
|
||
"""
|
||
1. look for .sls files
|
||
2. lex them with Jinja and see if the first token is a comment
|
||
3. in that case, save it to the registry
|
||
4. pass the registry to the renderer
|
||
"""
|
||
for file in self.glob(src):
|
||
if str(file.relative_to(self.base)).startswith(tuple(self.ignore)):
|
||
continue
|
||
with open(file, "r") as f:
|
||
text = "".join(f.readlines())
|
||
try:
|
||
comment = self.find_comment(text)
|
||
except TemplateSyntaxError as err:
|
||
raise RuntimeError(
|
||
f"There was an error parsing the Jinja syntax in file '{file}':\n\n{err}"
|
||
) from err
|
||
|
||
if (comment or self.include_empty) and not comment.startswith(
|
||
self.SKIP_FILE
|
||
):
|
||
path = file.relative_to(self.base)
|
||
self.registry.register(State(path, comment, self.base.name))
|
||
|
||
self.renderer.render(self.registry)
|
||
|
||
def glob(self, src):
|
||
"""
|
||
Returns Path objects pointing to Salt states provided by this formula.
|
||
"""
|
||
for path in (self.base / Path(src)).rglob("*.sls"):
|
||
yield path
|
||
|
||
def find_comment(self, text):
|
||
"""
|
||
Lexes files with Jinja. If the first token is a comment, return it.
|
||
"""
|
||
lexed = list(self.env.lex(text))
|
||
try:
|
||
i = 0
|
||
t = ""
|
||
while i < 5 and t != "comment":
|
||
_, t, content = lexed[i]
|
||
i += 1
|
||
except IndexError:
|
||
t = content = ""
|
||
|
||
if t == "comment":
|
||
return self.parse(content)
|
||
|
||
return ""
|
||
|
||
def parse(self, comment):
|
||
"""
|
||
Strips indentation from [multiline] strings. Could have used
|
||
docutils.prepare_docstring.
|
||
"""
|
||
# return d.prepare_docstring(comment)
|
||
ws = min(len(x) - len(x.lstrip()) for x in comment.splitlines() if len(x) > 0)
|
||
return "\n".join([x[ws:] for x in comment.splitlines()]).strip("\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
options = (
|
||
toml.loads((base_dir / "pyproject.toml").read_text())
|
||
.get("tool", {})
|
||
.get("slsdoc", {})
|
||
)
|
||
renderer = options.get("renderer", "single")
|
||
|
||
if renderer == "single":
|
||
rend = SingleRenderer(**options.get("single_renderer", {}))
|
||
elif renderer == "depth":
|
||
rend = DepthRenderer(**options.get("depth_renderer", {}))
|
||
else:
|
||
raise ValueError(f"Invalid value for renderer: {renderer}")
|
||
|
||
reg = StateRegistry(
|
||
avoid_init_dup=options.get("avoid_init_dup", True),
|
||
sort_list=options.get("sort_list"),
|
||
)
|
||
doc = SaltAutodoc(
|
||
rendr=rend,
|
||
registry=reg,
|
||
include_empty=options.get("include_empty", False),
|
||
ignore=options.get("ignore", ["_mapdata"]),
|
||
)
|
||
doc.run()
|