ironic/doc/source/_exts/web_api_docstring.py
Mahnoor Asghar 3e631a5931 Create API documentation from docstrings
Create a new Sphinx extension called 'web_api_docstring' to process
docstrings from the API classes, in order to generate API
documentation.

Story: 2009785
Task: 44291
Change-Id: Ia6b2b3741e2b1cbd29531c21795df4f0f0dc70ca
2022-03-17 01:22:44 +05:00

347 lines
11 KiB
Python

# -*- coding: utf-8 -*-
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from http import HTTPStatus
import os
import re # Stdlib
from docutils import nodes
from docutils.parsers.rst import Directive # 3rd Party
from sphinx.util.docfields import GroupedField # 3rd Party
import yaml # 3rd party
from ironic.common import exception # Application
def read_from_file(fpath):
"""Read the data in file given by fpath."""
with open(fpath, 'r') as stream:
yaml_data = yaml.load(stream, Loader=yaml.SafeLoader)
return yaml_data
def split_str_to_field(input_str):
"""Split the input_str into 2 parts, the field name and field body.
The split is based on this regex format: :field_name: field_body.
"""
regex_pattern = "((^:{1}.*:{1})(.*))"
field_name = None
field_body = None
if input_str is None:
return field_name, field_body
regex_output = re.match(regex_pattern, input_str)
if regex_output is None and len(input_str) > 0:
field_body = input_str.lstrip(' ')
if regex_output is not None:
field = regex_output.groups()
field_name = field[1].strip(':')
field_body = field[2].strip()
return field_name, field_body
def parse_field_list(content):
"""Convert list of fields as strings, to a dictionary.
This function takes a list of strings as input, each item being
a :field_name: field_body combination, and converts it into a dictionary
with the field names as keys, and field bodies as values.
"""
field_list = {} # dictionary to hold parsed input field list
for c in content:
if c is None:
continue
field_name, field_body = split_str_to_field(c)
field_list[field_name] = field_body
return field_list
def create_bullet_list(input_dict, input_build_env):
"""Convert input_dict into a sphinx representaion of a bullet list."""
grp_field = GroupedField('grp_field', label='title')
bullet_list = nodes.paragraph()
for field_name in input_dict:
fbody_txt_node = nodes.Text(data=input_dict[field_name])
tmp_field_node = grp_field.make_field(domain='py',
types=nodes.field,
items=[(field_name,
fbody_txt_node)],
env=input_build_env)
for c in tmp_field_node.children:
if c.tagname == 'field_body':
for ch in c.children:
bullet_list += ch
return bullet_list
def create_table(table_title, table_contents):
"""Construct a docutils-based table (single row and column)."""
table = nodes.table()
tgroup = nodes.tgroup(cols=1)
colspec = nodes.colspec(colwidth=1)
tgroup.append(colspec)
table += tgroup
thead = nodes.thead()
tgroup += thead
row = nodes.row()
entry = nodes.entry()
entry += nodes.paragraph(text=table_title)
row += entry
thead.append(row)
rows = []
row = nodes.row()
rows.append(row)
entry = nodes.entry()
entry += table_contents
row += entry
tbody = nodes.tbody()
tbody.extend(rows)
tgroup += tbody
return table
def split_list(input_list):
"""Split input_list into three sub-lists.
This function splits the input_list into three, one list containing the
inital non-empty items, one list containing items appearing after the
string 'Success' in input_list; and the other list containing items
appearing after the string 'Failure' in input_list.
"""
initial_flag = 1
success_flag = 0
failure_flag = 0
initial_list = []
success_list = []
failure_list = []
for c in input_list:
if c == 'Success:':
success_flag = 1
failure_flag = 0
elif c == 'Failure:':
failure_flag = 1
success_flag = 0
elif c != '' and success_flag:
success_list.append(c)
elif c != '' and failure_flag:
failure_list.append(c)
elif c != '' and initial_flag:
initial_list.append(c)
return initial_list, success_list, failure_list
def process_list(input_list):
"""Combine fields split over multiple list items into one.
This function expects to receive a field list as input,
with each item in the list representing a line
read from the document, as-is.
It combines the field bodies split over multiple lines into
one list item, making each field (name and body) one list item.
It also removes extra whitespace which was used for indentation
in input.
"""
out_list = []
# Convert list to string
str1 = "".join(input_list)
# Replace multiple spaces with one space
str2 = re.sub(r'\s+', ' ', str1)
regex_pattern = r'(:\S*.:)'
# Split the string, based on field names
list3 = re.split(regex_pattern, str2)
# Remove empty items from the list
list4 = list(filter(None, list3))
# Append the field name and field body strings together
for i in range(0, len(list4), 2):
out_list.append(list4[i] + list4[i + 1])
return out_list
def add_exception_info(failure_list):
"""Add exception information to fields.
This function takes a list of fields (field name and field body)
as an argument. If the field name is the name of an exception, it adds
the exception code into the field name, and exception message into
the field body.
"""
failure_dict = {}
# Add the exception code and message string
for f in failure_list:
field_name, field_body = split_str_to_field(f)
exc_code = ""
exc_msg = ""
if (field_name is not None) and hasattr(exception, field_name):
# Get the exception code and message string
exc_class = getattr(exception, field_name)
try:
exc_code = exc_class.code
exc_msg = exc_class._msg_fmt
except AttributeError:
pass
# Add the exception's HTTP code and HTTP phrase
# to the field name
if isinstance(exc_code, HTTPStatus):
field_name = (field_name
+ " (HTTP "
+ str(exc_code.value)
+ " "
+ exc_code.phrase
+ ")")
else:
field_name = field_name + " (HTTP " + str(exc_code) + ")"
# Add the exception's HTTP description to the field body
field_body = exc_msg + " \n" + field_body
# Add to dictionary if field name and field body exist
if field_name is not None and field_body is not None:
failure_dict[field_name] = field_body
return failure_dict
class Parameters(Directive):
"""This class implements the Parameters Directive."""
required_arguments = 1
has_content = True
def run(self):
# Parse the input field list from the docstring, as a dictionary
input_dict = {}
input_dict = parse_field_list(self.content)
# Read from yaml file
param_file = self.arguments[0]
cur_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
param_file_path = cur_path + '/' + param_file
yaml_data = read_from_file(param_file_path)
# Substitute the parameter descriptions with the yaml file descriptions
for field_name in input_dict:
old_field_body = input_dict[field_name]
if old_field_body in yaml_data.keys():
input_dict[field_name] = yaml_data[old_field_body]["description"]
# Convert dictionary to bullet list format
params_build_env = self.state.document.settings.env
params_bullet_list = create_bullet_list(input_dict, params_build_env)
# Create a table to display the final Parameters directive output
params_table = create_table('Parameters', params_bullet_list)
return [params_table]
class Return(Directive):
"""This class implements the Return Directive."""
has_content = True
def run(self):
initial_list, success_list, failure_list = split_list(self.content)
# Concatenate the field bodies split over multiple lines
proc_fail_list = process_list(failure_list)
# Add the exception code(s) and corresponding message string(s)
failure_dict = {}
failure_dict = add_exception_info(proc_fail_list)
ret_table_contents = nodes.paragraph()
if len(initial_list) > 0:
for i in initial_list:
initial_cont = nodes.Text(data=i)
ret_table_contents += initial_cont
if len(success_list) > 0:
# Add heading 'Success:' to output
success_heading = nodes.strong()
success_heading += nodes.Text(data='Success:')
ret_table_contents += success_heading
# Add Success details to output
success_detail = nodes.paragraph()
for s in success_list:
success_detail += nodes.Text(data=s)
ret_table_contents += success_detail
if len(proc_fail_list) > 0:
# Add heading 'Failure:' to output
failure_heading = nodes.strong()
failure_heading += nodes.Text(data='Failure:')
ret_table_contents += failure_heading
# Add failure details to output
ret_build_env = self.state.document.settings.env
failure_detail = create_bullet_list(failure_dict, ret_build_env)
ret_table_contents += failure_detail
if len(initial_list) > 0 or len(success_list) > 0 or len(proc_fail_list) > 0:
# Create a table to display the final Returns directive output
ret_table = create_table('Returns', ret_table_contents)
return [ret_table]
else:
return None
def setup(app):
app.add_directive("parameters", Parameters)
app.add_directive("return", Return)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}