# Copyright 2014-2015 ARM Limited
#
# 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.
#
"""
Utilities for working with and formatting documentation.
"""
import os
import re
import inspect
from itertools import cycle
USER_HOME = os.path.expanduser('~')
BULLET_CHARS = '-*'
[docs]def get_summary(aclass):
"""
Returns the summary description for an extension class. The summary is the
first paragraph (separated by blank line) of the description taken either from
the ``descripton`` attribute of the class, or if that is not present, from the
class' docstring.
"""
return get_description(aclass).split('\n\n')[0]
[docs]def get_description(aclass):
"""
Return the description of the specified extension class. The description is taken
either from ``description`` attribute of the class or its docstring.
"""
if hasattr(aclass, 'description') and aclass.description:
return inspect.cleandoc(aclass.description)
if aclass.__doc__:
return inspect.getdoc(aclass)
else:
return 'no documentation found for {}'.format(aclass.__name__)
[docs]def get_type_name(obj):
"""Returns the name of the type object or function specified. In case of a lambda,
the definiition is returned with the parameter replaced by "value"."""
match = re.search(r"<(type|class|function) '?(.*?)'?>", str(obj))
if isinstance(obj, tuple):
name = obj[1]
elif match.group(1) == 'function':
text = str(obj)
name = text.split()[1]
if name == '<lambda>':
source = inspect.getsource(obj).strip().replace('\n', ' ')
match = re.search(r'lambda\s+(\w+)\s*:\s*(.*?)\s*[\n,]', source)
if not match:
raise ValueError('could not get name for {}'.format(obj))
name = match.group(2).replace(match.group(1), 'value')
else:
name = match.group(2)
if '.' in name:
name = name.split('.')[-1]
return name
[docs]def count_leading_spaces(text):
"""
Counts the number of leading space characters in a string.
TODO: may need to update this to handle whitespace, but shouldn't
be necessary as there should be no tabs in Python source.
"""
nspaces = 0
for c in text:
if c == ' ':
nspaces += 1
else:
break
return nspaces
[docs]def format_body(text, width):
"""
Format the specified text into a column of specified width. The text is
assumed to be a "body" of one or more paragraphs separated by one or more
blank lines. The initial indentation of the first line of each paragraph
will be presevered, but any other formatting may be clobbered.
"""
text = re.sub('\n\\s*\n', '\n\n', text.strip('\n')) # get rid of all-whitespace lines
paragraphs = re.split('\n\n+', text)
formatted_paragraphs = []
for p in paragraphs:
if p.strip() and p.strip()[0] in BULLET_CHARS:
formatted_paragraphs.append(format_bullets(p, width))
else:
formatted_paragraphs.append(format_paragraph(p, width))
return '\n\n'.join(formatted_paragraphs)
[docs]def strip_inlined_text(text):
"""
This function processes multiline inlined text (e.g. form docstrings)
to strip away leading spaces and leading and trailing new lines.
"""
text = text.strip('\n')
lines = [ln.rstrip() for ln in text.split('\n')]
# first line is special as it may not have the indet that follows the
# others, e.g. if it starts on the same as the multiline quote (""").
nspaces = count_leading_spaces(lines[0])
if len([ln for ln in lines if ln]) > 1:
to_strip = min(count_leading_spaces(ln) for ln in lines[1:] if ln)
if nspaces >= to_strip:
stripped = [lines[0][to_strip:]]
else:
stripped = [lines[0][nspaces:]]
stripped += [ln[to_strip:] for ln in lines[1:]]
else:
stripped = [lines[0][nspaces:]]
return '\n'.join(stripped).strip('\n')
[docs]def indent(text, spaces=4):
"""Indent the lines i the specified text by ``spaces`` spaces."""
indented = []
for line in text.split('\n'):
if line:
indented.append(' ' * spaces + line)
else: # do not indent emtpy lines
indented.append(line)
return '\n'.join(indented)
[docs]def get_params_rst(ext):
text = ''
for param in ext.parameters:
text += '{} : {} {}\n'.format(param.name, get_type_name(param.kind),
param.mandatory and '(mandatory)' or ' ')
desc = strip_inlined_text(param.description or '')
text += indent('{}\n'.format(desc))
if param.allowed_values:
text += indent('\nallowed values: {}\n'.format(', '.join(map(format_literal, param.allowed_values))))
elif param.constraint:
text += indent('\nconstraint: ``{}``\n'.format(get_type_name(param.constraint)))
if param.default:
value = param.default
if isinstance(value, basestring) and value.startswith(USER_HOME):
value = value.replace(USER_HOME, '~')
text += indent('\ndefault: {}\n'.format(format_literal(value)))
text += '\n'
return text
[docs]def underline(text, symbol='='):
return '{}\n{}\n\n'.format(text, symbol * len(text))
[docs]def get_rst_from_extension(ext):
text = underline(ext.name, '-')
if hasattr(ext, 'description'):
desc = strip_inlined_text(ext.description or '')
elif ext.__doc__:
desc = strip_inlined_text(ext.__doc__)
else:
desc = ''
text += desc + '\n\n'
params_rst = get_params_rst(ext)
if params_rst:
text += underline('parameters', '~') + params_rst
return text + '\n'