Skip to content

Commit

Permalink
Merge pull request #64 from dwhswenson/compile-docs
Browse files Browse the repository at this point in the history
Automatic build of docs for `compile` command
  • Loading branch information
dwhswenson authored Nov 2, 2021
2 parents 68fc7de + d1f430a commit 59bc1b9
Show file tree
Hide file tree
Showing 17 changed files with 640 additions and 17 deletions.
18 changes: 11 additions & 7 deletions paths_cli/commands/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ def select_loader(filename):
except KeyError:
raise RuntimeError(f"Unknown file extension: {ext}")

def register_installed_plugins():
plugin_types = (InstanceCompilerPlugin, CategoryPlugin)
plugins = get_installed_plugins(
default_loader=NamespacePluginLoader('paths_cli.compiling',
plugin_types),
plugin_types=plugin_types
)
register_plugins(plugins)


@click.command(
'compile',
)
Expand All @@ -66,13 +76,7 @@ def compile_(input_file, output_file):
with open(input_file, mode='r') as f:
dct = loader(f)

plugin_types = (InstanceCompilerPlugin, CategoryPlugin)
plugins = get_installed_plugins(
default_loader=NamespacePluginLoader('paths_cli.compiling',
plugin_types),
plugin_types=plugin_types
)
register_plugins(plugins)
register_installed_plugins()

objs = do_compile(dct)
print(f"Saving {len(objs)} user-specified objects to {output_file}....")
Expand Down
6 changes: 6 additions & 0 deletions paths_cli/compiling/_gendocs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# _gendocs

Tools for generating documentation for the tools import `paths_cli.compiling`.
Note that this entire directory is considered outside the API, so nothing in
here should be strongly relied on.

2 changes: 2 additions & 0 deletions paths_cli/compiling/_gendocs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .config_handler import load_config, DocCategoryInfo
from .docs_generator import DocsGenerator
27 changes: 27 additions & 0 deletions paths_cli/compiling/_gendocs/config_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from collections import namedtuple
from paths_cli.commands.compile import select_loader

DocCategoryInfo = namedtuple('DocCategoryInfo', ['header', 'description',
'type_required'],
defaults=[None, True])


def load_config(config_file):
"""Load a configuration file for gendocs.
The configuration file should be YAML or JSON, and should map each
category name to the headings necessary to fill a DocCategoryInfo
instance.
Parameters
----------
config_file : str
name of YAML or JSON file
"""
loader = select_loader(config_file)
with open(config_file, mode='r', encoding='utf-8') as f:
dct = loader(f)

result = {category: DocCategoryInfo(**details)
for category, details in dct.items()}
return result
145 changes: 145 additions & 0 deletions paths_cli/compiling/_gendocs/docs_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import sys
from paths_cli.compiling.core import Parameter
from .json_type_handlers import json_type_to_string
from .config_handler import DocCategoryInfo

PARAMETER_RST = """* **{p.name}**{type_str} - {p.description}{required}\n"""


class DocsGenerator:
"""This generates the RST to describe options for compile input files.
Parameters
----------
config : Dict[str, DocCategoryInfo]
mapping of category name to DocCategoryInfo for that category;
usually generated by :method:`.load_config`
"""

parameter_template = PARAMETER_RST
_ANCHOR_SEP = "--"

def __init__(self, config):
self.config = config

def format_parameter(self, parameter, type_str=None):
"""Format a single :class:`.paths_cli.compiling.Parameter` in RST
"""
required = " (required)" if parameter.required else ""
return self.parameter_template.format(
p=parameter, type_str=type_str, required=required
)

def _get_cat_info(self, category_plugin):
cat_info = self.config.get(category_plugin.label, None)
if cat_info is None:
cat_info = DocCategoryInfo(category_plugin.label)
return cat_info

def generate_category_rst(self, category_plugin):
"""Generate the RST for a given category plugin.
Parameters
----------
category_plugin : :class:`.CategoryPlugin`
the plugin for which we should generate the RST page
Returns
-------
str :
RST string for this category
"""
cat_info = self._get_cat_info(category_plugin)
type_required = cat_info.type_required
rst = f".. _compiling--{category_plugin.label}:\n\n"
rst += f"{cat_info.header}\n{'=' * len(str(cat_info.header))}\n\n"
if cat_info.description:
rst += cat_info.description + "\n\n"
rst += ".. contents:: :local:\n\n"
for obj in category_plugin.type_dispatch.values():
rst += self.generate_plugin_rst(
obj, category_plugin.label, type_required
)
return rst

def generate_plugin_rst(self, plugin, category_name,
type_required=True):
"""Generate the RST for a given object plugin.
Parameters
----------
plugin : class:`.InstanceCompilerPlugin`
the object plugin for to generate the RST for
category_name : str
the name of the category for this object
type_required : bool
whether the ``type`` parameter is required in the dict input for
compiling this type of object (usually category-dependent)
Returns
-------
str :
RST string for this object plugin
"""
rst_anchor = f".. _{category_name}{self._ANCHOR_SEP}{plugin.name}:"
rst = f"{rst_anchor}\n\n{plugin.name}\n{'-' * len(plugin.name)}\n\n"
if plugin.description:
rst += plugin.description + "\n\n"
if type_required:
type_param = Parameter(
"type",
json_type="",
loader=None,
description=(f"type identifier; must exactly match the "
f"string ``{plugin.name}``"),
)
rst += self.format_parameter(
type_param, type_str=""
)

name_param = Parameter(
"name",
json_type="string",
loader=None,
default="",
description="name this object in order to reuse it",
)
rst += self.format_parameter(name_param, type_str=" (*string*)")
for param in plugin.parameters:
type_str = f" ({json_type_to_string(param.json_type)})"
rst += self.format_parameter(param, type_str)

rst += "\n\n"
return rst

@staticmethod
def _get_filename(cat_info):
fname = str(cat_info.header).lower()
fname = fname.translate(str.maketrans(' ', '_'))
return f"{fname}.rst"

def generate(self, category_plugins, stdout=False):
"""Generate RST output for the given plugins.
This is the main method used to generate the entire set of
documentation.
Parameters
----------
category_plugin : List[:class:`.CategoryPlugin`]
list of category plugins document
stdout : bool
if False (default) a separate output file is generated for each
category plugin. If True, all text is output to stdout
(particularly useful for debugging/dry runs).
"""
for plugin in category_plugins:
rst = self.generate_category_rst(plugin)
if stdout:
sys.stdout.write(rst)
sys.stdout.flush()
else:
cat_info = self._get_cat_info(plugin)
filename = self._get_filename(cat_info)
with open(filename, 'w') as f:
f.write(rst)
161 changes: 161 additions & 0 deletions paths_cli/compiling/_gendocs/json_type_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
class JsonTypeHandler:
"""Abstract class to obtain documentation type from JSON schema type.
Parameters
----------
is_my_type : Callable[Any] -> bool
return True if this instance should handle the given input type
handler : Callable[Any] -> str
convert the input type to a string suitable for the RST docs
"""
def __init__(self, is_my_type, handler):
self._is_my_type = is_my_type
self.handler = handler

def is_my_type(self, json_type):
"""Determine whether this instance should handle this type.
Parameters
----------
json_type : Any
input type from JSON schema
Returns
-------
bool :
whether to handle this type with this instance
"""
return self._is_my_type(json_type)

def __call__(self, json_type):
if self.is_my_type(json_type):
return self.handler(json_type)
return json_type


handle_object = JsonTypeHandler(
is_my_type=lambda json_type: json_type == "object",
handler=lambda json_type: "dict",
)


def _is_listof(json_type):
try:
return json_type["type"] == "array"
except: # any exception should return false (mostly Key/Type Error)
return False


handle_listof = JsonTypeHandler(
is_my_type=_is_listof,
handler=lambda json_type: "list of "
+ json_type_to_string(json_type["items"]),
)


class RefTypeHandler(JsonTypeHandler):
"""Handle JSON types of the form {"$ref": "#/definitions/..."}
Parameters
----------
type_name : str
the name to use in the RST type
def_string : str
the string following "#/definitions/" in the JSON type definition
link_to : str or None
if not None, the RST type will be linked with a ``:ref:`` pointing
to the anchor given by ``link_to``
"""
def __init__(self, type_name, def_string, link_to):
self.type_name = type_name
self.def_string = def_string
self.link_to = link_to
self.json_check = {"$ref": "#/definitions/" + def_string}
super().__init__(is_my_type=self._reftype, handler=self._refhandler)

def _reftype(self, json_type):
return json_type == self.json_check

def _refhandler(self, json_type):
rst = f"{self.type_name}"
if self.link_to:
rst = f":ref:`{rst} <{self.link_to}>`"
return rst


class CategoryHandler(RefTypeHandler):
"""Handle JSON types for OPS category definitions.
OPS category definitions show up with JSON references pointing to
"#/definitions/{CATEGORY}_type". This provides a convenience class over
the :class:RefTypeHandler to treat OPS categories.
Parameters
----------
category : str
name of the category
"""
def __init__(self, category):
self.category = category
def_string = f"{category}_type"
link_to = f"compiling--{category}"
super().__init__(
type_name=category, def_string=def_string, link_to=link_to
)


class EvalHandler(RefTypeHandler):
"""Handle JSON types for OPS custom evaluation definitions.
Some parameters for the OPS compiler use the OPS custom evaluation
mechanism, which evaluates certain Python-like string input. These are
treated as special definition types in the JSON schema, and this object
provides a convenience class over :class:`.RefTypeHandler` to treat
custom evaluation types.
Parameters
----------
type_name : str
name of the custom evaluation type
link_to : str or None
if not None, the RST type will be linked with a ``:ref:`` pointing
to the anchor given by ``link_to``
"""
def __init__(self, type_name, link_to=None):
super().__init__(
type_name=type_name, def_string=type_name, link_to=link_to
)


JSON_TYPE_HANDLERS = [
handle_object,
handle_listof,
CategoryHandler("engine"),
CategoryHandler("cv"),
CategoryHandler("volume"),
EvalHandler("EvalInt"),
EvalHandler("EvalFloat"),
]


def json_type_to_string(json_type):
"""Convert JSON schema type to string for RST docs.
This is the primary public-facing method for dealing with JSON schema
types in RST document generation.
Parameters
----------
json_type : Any
the type from the JSON schema
Returns
-------
str :
the type string description to be used in the RST document
"""
for handler in JSON_TYPE_HANDLERS:
handled = handler(json_type)
if handled != json_type:
return handled
return json_type
Loading

0 comments on commit 59bc1b9

Please sign in to comment.