Skip to content

Commit 59bc1b9

Browse files
authored
Merge pull request #64 from dwhswenson/compile-docs
Automatic build of docs for `compile` command
2 parents 68fc7de + d1f430a commit 59bc1b9

File tree

17 files changed

+640
-17
lines changed

17 files changed

+640
-17
lines changed

paths_cli/commands/compile.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ def select_loader(filename):
5656
except KeyError:
5757
raise RuntimeError(f"Unknown file extension: {ext}")
5858

59+
def register_installed_plugins():
60+
plugin_types = (InstanceCompilerPlugin, CategoryPlugin)
61+
plugins = get_installed_plugins(
62+
default_loader=NamespacePluginLoader('paths_cli.compiling',
63+
plugin_types),
64+
plugin_types=plugin_types
65+
)
66+
register_plugins(plugins)
67+
68+
5969
@click.command(
6070
'compile',
6171
)
@@ -66,13 +76,7 @@ def compile_(input_file, output_file):
6676
with open(input_file, mode='r') as f:
6777
dct = loader(f)
6878

69-
plugin_types = (InstanceCompilerPlugin, CategoryPlugin)
70-
plugins = get_installed_plugins(
71-
default_loader=NamespacePluginLoader('paths_cli.compiling',
72-
plugin_types),
73-
plugin_types=plugin_types
74-
)
75-
register_plugins(plugins)
79+
register_installed_plugins()
7680

7781
objs = do_compile(dct)
7882
print(f"Saving {len(objs)} user-specified objects to {output_file}....")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# _gendocs
2+
3+
Tools for generating documentation for the tools import `paths_cli.compiling`.
4+
Note that this entire directory is considered outside the API, so nothing in
5+
here should be strongly relied on.
6+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .config_handler import load_config, DocCategoryInfo
2+
from .docs_generator import DocsGenerator
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from collections import namedtuple
2+
from paths_cli.commands.compile import select_loader
3+
4+
DocCategoryInfo = namedtuple('DocCategoryInfo', ['header', 'description',
5+
'type_required'],
6+
defaults=[None, True])
7+
8+
9+
def load_config(config_file):
10+
"""Load a configuration file for gendocs.
11+
12+
The configuration file should be YAML or JSON, and should map each
13+
category name to the headings necessary to fill a DocCategoryInfo
14+
instance.
15+
16+
Parameters
17+
----------
18+
config_file : str
19+
name of YAML or JSON file
20+
"""
21+
loader = select_loader(config_file)
22+
with open(config_file, mode='r', encoding='utf-8') as f:
23+
dct = loader(f)
24+
25+
result = {category: DocCategoryInfo(**details)
26+
for category, details in dct.items()}
27+
return result
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import sys
2+
from paths_cli.compiling.core import Parameter
3+
from .json_type_handlers import json_type_to_string
4+
from .config_handler import DocCategoryInfo
5+
6+
PARAMETER_RST = """* **{p.name}**{type_str} - {p.description}{required}\n"""
7+
8+
9+
class DocsGenerator:
10+
"""This generates the RST to describe options for compile input files.
11+
12+
Parameters
13+
----------
14+
config : Dict[str, DocCategoryInfo]
15+
mapping of category name to DocCategoryInfo for that category;
16+
usually generated by :method:`.load_config`
17+
"""
18+
19+
parameter_template = PARAMETER_RST
20+
_ANCHOR_SEP = "--"
21+
22+
def __init__(self, config):
23+
self.config = config
24+
25+
def format_parameter(self, parameter, type_str=None):
26+
"""Format a single :class:`.paths_cli.compiling.Parameter` in RST
27+
"""
28+
required = " (required)" if parameter.required else ""
29+
return self.parameter_template.format(
30+
p=parameter, type_str=type_str, required=required
31+
)
32+
33+
def _get_cat_info(self, category_plugin):
34+
cat_info = self.config.get(category_plugin.label, None)
35+
if cat_info is None:
36+
cat_info = DocCategoryInfo(category_plugin.label)
37+
return cat_info
38+
39+
def generate_category_rst(self, category_plugin):
40+
"""Generate the RST for a given category plugin.
41+
42+
Parameters
43+
----------
44+
category_plugin : :class:`.CategoryPlugin`
45+
the plugin for which we should generate the RST page
46+
47+
Returns
48+
-------
49+
str :
50+
RST string for this category
51+
"""
52+
cat_info = self._get_cat_info(category_plugin)
53+
type_required = cat_info.type_required
54+
rst = f".. _compiling--{category_plugin.label}:\n\n"
55+
rst += f"{cat_info.header}\n{'=' * len(str(cat_info.header))}\n\n"
56+
if cat_info.description:
57+
rst += cat_info.description + "\n\n"
58+
rst += ".. contents:: :local:\n\n"
59+
for obj in category_plugin.type_dispatch.values():
60+
rst += self.generate_plugin_rst(
61+
obj, category_plugin.label, type_required
62+
)
63+
return rst
64+
65+
def generate_plugin_rst(self, plugin, category_name,
66+
type_required=True):
67+
"""Generate the RST for a given object plugin.
68+
69+
Parameters
70+
----------
71+
plugin : class:`.InstanceCompilerPlugin`
72+
the object plugin for to generate the RST for
73+
category_name : str
74+
the name of the category for this object
75+
type_required : bool
76+
whether the ``type`` parameter is required in the dict input for
77+
compiling this type of object (usually category-dependent)
78+
79+
Returns
80+
-------
81+
str :
82+
RST string for this object plugin
83+
"""
84+
rst_anchor = f".. _{category_name}{self._ANCHOR_SEP}{plugin.name}:"
85+
rst = f"{rst_anchor}\n\n{plugin.name}\n{'-' * len(plugin.name)}\n\n"
86+
if plugin.description:
87+
rst += plugin.description + "\n\n"
88+
if type_required:
89+
type_param = Parameter(
90+
"type",
91+
json_type="",
92+
loader=None,
93+
description=(f"type identifier; must exactly match the "
94+
f"string ``{plugin.name}``"),
95+
)
96+
rst += self.format_parameter(
97+
type_param, type_str=""
98+
)
99+
100+
name_param = Parameter(
101+
"name",
102+
json_type="string",
103+
loader=None,
104+
default="",
105+
description="name this object in order to reuse it",
106+
)
107+
rst += self.format_parameter(name_param, type_str=" (*string*)")
108+
for param in plugin.parameters:
109+
type_str = f" ({json_type_to_string(param.json_type)})"
110+
rst += self.format_parameter(param, type_str)
111+
112+
rst += "\n\n"
113+
return rst
114+
115+
@staticmethod
116+
def _get_filename(cat_info):
117+
fname = str(cat_info.header).lower()
118+
fname = fname.translate(str.maketrans(' ', '_'))
119+
return f"{fname}.rst"
120+
121+
def generate(self, category_plugins, stdout=False):
122+
"""Generate RST output for the given plugins.
123+
124+
This is the main method used to generate the entire set of
125+
documentation.
126+
127+
Parameters
128+
----------
129+
category_plugin : List[:class:`.CategoryPlugin`]
130+
list of category plugins document
131+
stdout : bool
132+
if False (default) a separate output file is generated for each
133+
category plugin. If True, all text is output to stdout
134+
(particularly useful for debugging/dry runs).
135+
"""
136+
for plugin in category_plugins:
137+
rst = self.generate_category_rst(plugin)
138+
if stdout:
139+
sys.stdout.write(rst)
140+
sys.stdout.flush()
141+
else:
142+
cat_info = self._get_cat_info(plugin)
143+
filename = self._get_filename(cat_info)
144+
with open(filename, 'w') as f:
145+
f.write(rst)
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
class JsonTypeHandler:
2+
"""Abstract class to obtain documentation type from JSON schema type.
3+
4+
Parameters
5+
----------
6+
is_my_type : Callable[Any] -> bool
7+
return True if this instance should handle the given input type
8+
handler : Callable[Any] -> str
9+
convert the input type to a string suitable for the RST docs
10+
"""
11+
def __init__(self, is_my_type, handler):
12+
self._is_my_type = is_my_type
13+
self.handler = handler
14+
15+
def is_my_type(self, json_type):
16+
"""Determine whether this instance should handle this type.
17+
18+
Parameters
19+
----------
20+
json_type : Any
21+
input type from JSON schema
22+
23+
Returns
24+
-------
25+
bool :
26+
whether to handle this type with this instance
27+
"""
28+
return self._is_my_type(json_type)
29+
30+
def __call__(self, json_type):
31+
if self.is_my_type(json_type):
32+
return self.handler(json_type)
33+
return json_type
34+
35+
36+
handle_object = JsonTypeHandler(
37+
is_my_type=lambda json_type: json_type == "object",
38+
handler=lambda json_type: "dict",
39+
)
40+
41+
42+
def _is_listof(json_type):
43+
try:
44+
return json_type["type"] == "array"
45+
except: # any exception should return false (mostly Key/Type Error)
46+
return False
47+
48+
49+
handle_listof = JsonTypeHandler(
50+
is_my_type=_is_listof,
51+
handler=lambda json_type: "list of "
52+
+ json_type_to_string(json_type["items"]),
53+
)
54+
55+
56+
class RefTypeHandler(JsonTypeHandler):
57+
"""Handle JSON types of the form {"$ref": "#/definitions/..."}
58+
59+
Parameters
60+
----------
61+
type_name : str
62+
the name to use in the RST type
63+
def_string : str
64+
the string following "#/definitions/" in the JSON type definition
65+
link_to : str or None
66+
if not None, the RST type will be linked with a ``:ref:`` pointing
67+
to the anchor given by ``link_to``
68+
"""
69+
def __init__(self, type_name, def_string, link_to):
70+
self.type_name = type_name
71+
self.def_string = def_string
72+
self.link_to = link_to
73+
self.json_check = {"$ref": "#/definitions/" + def_string}
74+
super().__init__(is_my_type=self._reftype, handler=self._refhandler)
75+
76+
def _reftype(self, json_type):
77+
return json_type == self.json_check
78+
79+
def _refhandler(self, json_type):
80+
rst = f"{self.type_name}"
81+
if self.link_to:
82+
rst = f":ref:`{rst} <{self.link_to}>`"
83+
return rst
84+
85+
86+
class CategoryHandler(RefTypeHandler):
87+
"""Handle JSON types for OPS category definitions.
88+
89+
OPS category definitions show up with JSON references pointing to
90+
"#/definitions/{CATEGORY}_type". This provides a convenience class over
91+
the :class:RefTypeHandler to treat OPS categories.
92+
93+
Parameters
94+
----------
95+
category : str
96+
name of the category
97+
"""
98+
def __init__(self, category):
99+
self.category = category
100+
def_string = f"{category}_type"
101+
link_to = f"compiling--{category}"
102+
super().__init__(
103+
type_name=category, def_string=def_string, link_to=link_to
104+
)
105+
106+
107+
class EvalHandler(RefTypeHandler):
108+
"""Handle JSON types for OPS custom evaluation definitions.
109+
110+
Some parameters for the OPS compiler use the OPS custom evaluation
111+
mechanism, which evaluates certain Python-like string input. These are
112+
treated as special definition types in the JSON schema, and this object
113+
provides a convenience class over :class:`.RefTypeHandler` to treat
114+
custom evaluation types.
115+
116+
Parameters
117+
----------
118+
type_name : str
119+
name of the custom evaluation type
120+
link_to : str or None
121+
if not None, the RST type will be linked with a ``:ref:`` pointing
122+
to the anchor given by ``link_to``
123+
"""
124+
def __init__(self, type_name, link_to=None):
125+
super().__init__(
126+
type_name=type_name, def_string=type_name, link_to=link_to
127+
)
128+
129+
130+
JSON_TYPE_HANDLERS = [
131+
handle_object,
132+
handle_listof,
133+
CategoryHandler("engine"),
134+
CategoryHandler("cv"),
135+
CategoryHandler("volume"),
136+
EvalHandler("EvalInt"),
137+
EvalHandler("EvalFloat"),
138+
]
139+
140+
141+
def json_type_to_string(json_type):
142+
"""Convert JSON schema type to string for RST docs.
143+
144+
This is the primary public-facing method for dealing with JSON schema
145+
types in RST document generation.
146+
147+
Parameters
148+
----------
149+
json_type : Any
150+
the type from the JSON schema
151+
152+
Returns
153+
-------
154+
str :
155+
the type string description to be used in the RST document
156+
"""
157+
for handler in JSON_TYPE_HANDLERS:
158+
handled = handler(json_type)
159+
if handled != json_type:
160+
return handled
161+
return json_type

0 commit comments

Comments
 (0)