Skip to content

Commit

Permalink
Add function help to Read The Docs as well as adding docstrings (#126)
Browse files Browse the repository at this point in the history
Add documentation to metadata for functions and function parameters
Add docstrings to generated functions in session.py
Add function reference on readthedocs
Numerous changes to helper functions.
  • Loading branch information
texasaggie97-zz authored Aug 16, 2017
1 parent b279b6c commit feb6060
Show file tree
Hide file tree
Showing 20 changed files with 9,743 additions and 458 deletions.
1 change: 1 addition & 0 deletions build/defines.mak
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,6 @@ DEFAULT_RST_FILES_TO_GENERATE := \
session.rst \
enums.rst \
attributes.rst \
functions.rst \


318 changes: 310 additions & 8 deletions build/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Coding convention transformation functions.

# TODO(marcoskirsch): not being used
def shoutcase_to_camelcase(shout_string):
def shoutcase_to_camelcase(shout_string):
'''Converts a C-style SHOUT_CASE string to camelCase'''
components = shout_string.split('_')
return components[0].lower() + "".join(component.title() for component in components[1:])
Expand Down Expand Up @@ -55,7 +55,7 @@ def extract_enum_parameters(parameters):
def extract_ivi_dance_parameter(parameters):
'''Returns the ivi-dance parameter of a session method if there is one. This is the parameter whose size is determined at runtime.'''
param = [x for x in parameters if x['size']['mechanism'] == 'ivi-dance']
assert len(param) <= 1, '{1} ivi-dance parameters. No more than one is allowed'.format(len(param))
assert len(param) <= 1, '{0} ivi-dance parameters. No more than one is allowed'.format(len(param))
if len(param) == 0:
return None
assert param[0]['direction'] == 'out', "ivi-dance parameter must have 'direction':'out'. Check your metadata."
Expand Down Expand Up @@ -177,10 +177,16 @@ def get_method_parameters_snippet(parameters, skip_session_handle, skip_output_p
snippets.append(x['python_name'])
return ', '.join(snippets)

def get_function_parameters_snippet(parameters):
'''Returns a string suitable for the parameter list of a method given a list of parameter objects'''
def get_function_parameters_snippet(parameters, session_name='vi'):
'''
Returns a string suitable for the parameter list of a method given a list of parameter objects
If session_name set, skip that parameter
'''
snippets = []
for x in parameters:
if session_name is not None and x['python_name'] == session_name:
continue
snippets.append(x['python_name'])
return ', '.join(snippets)

Expand Down Expand Up @@ -280,9 +286,20 @@ def sorted_attrs(a):

def get_indented_docstring_snippet(d, indent=4):
'''
Returns a docstring with the correct amount of indentation. Can't use similar construct as
get_dictionary_snippet ('\n' + (' ' * indent)).join(d_lines) because empty lines would get
Returns a docstring with the correct amount of indentation.
First line is not indented.
Can't use similar construct as get_dictionary_snippet
('\n' + (' ' * indent)).join(d_lines) because empty lines would get
the spaces, which violates pep8 and causes the flake8 step to fail
Args:
docstring (str): multiline string to format
indent (int): How much to indent lines 2+
Returns:
str: formatted string
'''
d_lines = d.strip().splitlines()
ret_val = ''
Expand All @@ -295,12 +312,293 @@ def get_indented_docstring_snippet(d, indent=4):
return ret_val

def get_rst_header_snippet(t, header_level='='):
'''Get rst formatted heading
'''
ret_val = t + '\n'
ret_val += header_level * len(t)
return ret_val

def get_rst_table_snippet(d, config, indent=0, make_link=True):
'''Returns an rst table snippet if table_header and/or table_body are in the dictionary'''
if 'table_body' in d:
table_body = d['table_body']
else:
return ''

header = False
# If there is no body, then we ignore the header
if 'table_header' in d:
table_header = d['table_header']
header = True

table_contents = []
if header:
header_contents = []
for i in table_header:
contents = fix_references(i, config, make_link)
header_contents.append(contents)
table_contents.append(header_contents)

for t in table_body:
line_contents = []
for i in t:
contents = fix_references(i, config, make_link)
line_contents.append(contents)
table_contents.append(line_contents)

table = as_rest_table(table_contents, full=True, header=header)
return get_indented_docstring_snippet(table, indent)


def get_rst_admonition_snippet(admonition, d, config, indent=0):
'''Returns a rst formatted admonition if the given admonition ('note', 'caution') exists in the dictionary'''
if admonition in d:
a = '\n\n' + (' ' * indent) + '.. {0}:: '.format(admonition) + get_indented_docstring_snippet(fix_references(d[admonition], config, make_link=True), indent + 4)
return a
else:
return ''

def get_documentation_for_node_rst(node, config, indent=0):
'''Returns any documentaion information formatted for rst
Documentation will be in the following order (if existing)
- 'caution' admonition
- 'description'
- table made of 'table_header' and 'table_body'
- 'note' admonition
Args:
node (dict) - Node possibly containing documentation
config (dict) - build configuration
indent (int) - how much each line should be indented
Returns:
str - formatted documentation, empty string if none
'''
doc = ''
if 'documentation' not in node:
return doc

nd = node['documentation']
doc += get_rst_admonition_snippet('caution', nd, config, indent)
if 'description' in nd:
doc += '\n\n' + (' ' * indent) + get_indented_docstring_snippet(fix_references(nd['description'], config, make_link=True), indent)

doc += '\n\n' + (' ' * indent) + get_rst_table_snippet(nd, config, indent)
doc += get_rst_admonition_snippet('note', nd, config, indent)
doc += '\n'

return doc

def get_documentation_for_node_docstring(node, config, indent=0):
'''Returns any documentaion information formatted for docstring
Documentation will be in the following order (if existing)
- 'caution' admonition
- 'description'
- table made of 'table_header' and 'table_body'
- 'note' admonition
Args:
node (dict) - Node possibly containing documentation
config (dict) - build configuration
indent (int) - how much each line should be indented
Returns:
str - formatted documentation, empty string if none
'''
doc = ''
if 'documentation' not in node:
return doc

nd = node['documentation']
extra_newline = ''
if 'caution' in nd:
doc += '\n' + extra_newline + (' ' * indent) + get_indented_docstring_snippet(fix_references('Caution: ' + nd['caution'], config, make_link=False), indent)
extra_newline = '\n'

if 'description' in nd:
doc += '\n' + extra_newline + (' ' * indent) + get_indented_docstring_snippet(fix_references(nd['description'], config, make_link=False), indent)
extra_newline = '\n'

tbl = get_rst_table_snippet(nd, config, indent, make_link=False)
if len(tbl) > 0:
doc += '\n' + extra_newline + (' ' * indent) + tbl
extra_newline = '\n'

if 'note' in nd:
doc += '\n' + extra_newline + (' ' * indent) + get_indented_docstring_snippet(fix_references('Note: ' + nd['note'], config, make_link=False), indent)

return doc

# We need this in the global namespace so we can reference it from the sub() callback
config = None

def find_attribute_by_name(attributes, name):
'''Returns the attribute with the given name if there is one
There should only be one so return that individual parameter and not a list
'''
attr = [attributes[x] for x in attributes if attributes[x]['name'] == name]
assert len(attr) <= 1, '{0} attributes with name {1}. No more than one is allowed'.format(len(attr), name)
if len(attr) == 0:
return None
return attr[0]

def replace_attribute_python_name(a_match):
'''callback function for regex sub command when link not needed
Args:
m (match object): Match object from the attribute substitution command
Returns:
str: python name of the attribute
'''
aname = "Unknown"
if a_match:
attr = find_attribute_by_name(config['attributes'], a_match.group(1))
aname = a_match.group(1)
if attr:
aname = attr['name'].lower()

if config['make_link']:
return ':py:data:`{0}.{1}`'.format(config['module_name'], aname)
else:
return '{0}'.format(aname)

def replace_func_python_name(f_match):
'''callback function for regex sub command when link needed
Args:
m (match object): Match object from the function substitution command
Returns:
str: rst link to function using python name
'''
fname = "Unknown"
if f_match:
fname = f_match.group(1).replace('.', '').replace(',', '')
fname = config['functions'][fname]['python_name']
else:
print('Unknown function name: {0}'.format(f_match.group(1)))
print(config['functions'])

if config['make_link']:
return ':py:func:`{0}.{1}`'.format(config['module_name'], fname)
else:
return '{0}'.format(fname)

def fix_references(doc, cfg, make_link=False):
'''Replace ATTR and function mentions in documentation
Args:
doc (str): documentation string to be updated
config (dict): config dictionary from metadata
make_link (bool): Default False
True - references are replaced with a rst style link
False - references are replaced with just the python name
Returns:
str: documentation with references replaces based on make_link
'''

global config
config = cfg

config['make_link'] = make_link

before = doc

attr_re = re.compile('{0}\\\\_ATTR\\\\_([A-Z0-9\\\\_]+)'.format(config['module_name'].upper()))
func_re = re.compile('{0}\\\\_([A-Za-z0-9\\\\_]+)'.format(config['c_function_prefix'].replace('_', '')))

doc = attr_re.sub(replace_attribute_python_name, doc)
doc = func_re.sub(replace_func_python_name, doc)

if doc.find('niDMM') != -1:
print('Found niDMM: ' + doc)

if not make_link:
doc = doc.replace('\_', '_')
return doc

def get_function_rst(fname, config, indent=0):
'''Gets rst formatted documentation for given function
Args:
fname (str): Function name - key in function dictionary
function (dict): function entry correcsponding to fname in function dictionary
Returns:
str: rst formatted documentation
'''
function = config['functions'][fname]
rst = '.. function:: ' + function['python_name'] + '('
rst += get_function_parameters_snippet(function['parameters'], session_name='vi') + ')'
indent += 4
rst += get_documentation_for_node_rst(function, config, indent)

input_params = extract_input_parameters(function['parameters'])
if len(input_params) > 0:
rst += '\n'
for p in input_params:
rst += '\n' + (' ' * indent) + ':param {0}:'.format(p['python_name']) + '\n'
rst += get_documentation_for_node_rst(p, config, indent + 4)

p_type = p['python_type']
if p_type.startswith('enums.'):
p_type = p_type.replace('enums.', '')
p_type = ':py:data:`{0}.{1}`'.format(config['module_name'], p_type)
rst += '\n' + (' ' * indent) + ':type {0}:'.format(p['python_name']) + p_type


output_params = extract_output_parameters(function['parameters'])
if len(output_params) > 1:
rst += '\n\n' + (' ' * indent) + ':rtype: tuple ('+ ', '.join([p['python_name'] for p in output_params]) + ')\n\n'
rst += (' ' * (indent + 4)) + 'WHERE\n'
for p in output_params:
rst += '\n' + (' ' * (indent + 4)) + '{0} ({1}):'.format(p['python_name'], p['python_type']) + '\n'
rst += get_documentation_for_node_rst(p, config, indent + 8)
elif len(output_params) == 1:
p = output_params[0]
rst += '\n\n' + (' ' * indent) + ':rtype: '+ p['python_type'] + '\n'
rst += get_documentation_for_node_rst(p, config, indent + 8)

return rst

def get_function_docstring(fname, config, indent=0):
'''Gets formatted documentation for given function that can be used as a docstring
Args:
fname (str): Function name - key in function dictionary
function (dict): function entry correcsponding to fname in function dictionary
Returns:
str: docstring formatted documentation
'''
docstring = ''
function = config['functions'][fname]
docstring += get_documentation_for_node_docstring(function, config, indent)

input_params = extract_input_parameters(function['parameters'])
if len(input_params) > 0:
docstring += '\n\n' + (' ' * indent) + 'Args:'
for p in input_params:
docstring += '\n' + (' ' * (indent + 4)) + '{0} ({1}):'.format(p['python_name'], p['python_type'])
docstring += get_documentation_for_node_docstring(p, config, indent + 8)

output_params = extract_output_parameters(function['parameters'])
if len(output_params) > 0:
docstring += '\n\n' + (' ' * indent) + 'Returns:'
for p in output_params:
docstring += '\n' + (' ' * (indent + 4)) + '{0} ({1}):'.format(p['python_name'], p['python_type'])
docstring += get_documentation_for_node_docstring(p, config, indent + 8)

return docstring


# From http://code.activestate.com/recipes/579054-generate-sphinx-table/
def as_rest_table(data, full=False):
def as_rest_table(data, full=False, header=True):
"""
>>> from report_table import as_rest_table
>>> data = [('what', 'how', 'who'),
Expand Down Expand Up @@ -380,7 +678,10 @@ def as_rest_table(data, full=False):
# set table header
titles = data[0]
table.append(template.format(*titles))
table.append(th_separator)
if header:
table.append(th_separator)
else:
table.append(separator)

for d in data[1:-1]:
table.append(template.format(*d))
Expand Down Expand Up @@ -414,3 +715,4 @@ def get_python_type_from_visa_type(visa_type):

return p_type


2 changes: 1 addition & 1 deletion build/templates/ctypes_library.py.mako
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ${module_name.title()}CtypesLibrary(object):
c_func_name = c_function_prefix + func_name
params = f['parameters']
param_names_method = helper.get_method_parameters_snippet(params, skip_session_handle = False, skip_output_parameters = False, skip_ivi_dance_size_parameter = False)
param_names_function = helper.get_function_parameters_snippet(params)
param_names_function = helper.get_function_parameters_snippet(params, session_name=False)
%>\

def ${c_func_name}(${param_names_method}): # noqa: N802
Expand Down
Loading

0 comments on commit feb6060

Please sign in to comment.