Source code for gcudm.docstrings
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Created by pat on 4/6/18
"""
.. currentmodule:: docstrings
.. moduleauthor:: Pat Daburu <pat@daburu.net>
This module contains utilities for setting up docstrings
"""
from .meta import ColumnMeta, COLUMN_META_ATTR, Usage, Requirement
from .modes import Modes
import inspect
from sqlalchemy import Column
import sys
from titlecase import titlecase
from typing import Any, List, Set, Tuple, Type, Union
[docs]class ModelRstFormatter(object):
"""
This class supports a number of specialized methods for creating
reStructuredText docstrings for classes in this project.
"""
[docs] @staticmethod
def simplify_docstring(s: str):
"""
Simplify a docstring by removing leading and trailing spaces,
:param s:
:return:
"""
# Make sure we're working with an actual value.
_s = s if s is not None else ''
# Remove leading and trailing whitespace.
return _s.strip()
[docs] @staticmethod
def format_line(line: str, indent: int=1, wrap: bool=True):
"""
Format a line of reStructuredText.
:param line: the line to format
:param indent: the indentation level of the formatted line
:param wrap: Should a newline be placed at the end?
:return: the formatted line
"""
return '{}{}{}'.format('\t' * indent, line, '\n' if wrap else '')
[docs] def enum2tbl(self,
enum_cls: Type[Union[Requirement, Usage]],
meta: ColumnMeta,
excluded: Set[Any],
indent: int = 1):
# Get all of the enumerated values that aren't in the exclusion
# set.
vals = [v for v in enum_cls if v not in excluded]
# Let's start off with the column specification for the table.
colspec = f"|{'|'.join(['c'] * len(vals))}|"
lines = [
f'.. tabularcolumns:: {colspec}', ''
]
# We're going to be formatting fixed-width text. Let's do so with
# three lists...
tbl_hborders = [''] * len(vals) # the horizontal borders
tbl_headers = [''] * len(vals) # the table headers
tbl_values = [''] * len(vals) # the values
# Let's look at each of the values.
for i in range(0, len(vals)):
# We need the name.
enum_name = vals[i].name
# The character width of the column is the length of the name
# plus one (1) padding space on each side.
colwidth = (len(enum_name) + 2)
# Now that we know the width, the border for this index can be
# defined.
tbl_hborders[i] = '-' * colwidth
# Title-case the numeration name and place it in the headers
# list at the current index.
tbl_headers[i] = f' {titlecase(enum_name)} '
# The yes-or-no indicator will only take up a single character,
# but we need to pad it to maintain the fixed width.
xo = [' '] * colwidth
# Leaving one space on the left, put a yes-or-no indicator in
# the column. (We're using ASCII characters which we'll
# replace in a moment. For some reason, the extended characters
# seem to pad the list with an extra space.)
xo[1] = (
u'Y' if meta.get_enum(enum_cls) & vals[i].value else u'N'
)
# Build the string.
xos = ''.join(xo)
# Update the string with visual symbols.
xos = xos.replace('N', '✘')
xos = xos.replace('Y', '✔')
# That's the text for the values list at this index.
tbl_values[i] = xos
# Construct the table.
hborder = f"+{'+'.join(tbl_hborders)}+"
lines.append(hborder)
lines.append(f"|{'|'.join(tbl_headers)}|")
lines.append(hborder)
lines.append(f"|{'|'.join(tbl_values)}|")
lines.append(hborder)
# Indent the entire table.
lines = [
self.format_line(line, indent=indent, wrap=False)
for line in lines
]
lines.append('') # A blank line must follow the table.
# Put it all together, and...
rst = '\n'.join(lines)
return rst # ...that's that.
[docs] def col2section(self,
table_name: str,
column_name: str,
meta: ColumnMeta) -> str:
"""
Format a block of reStructuredText to represent a column.
:param table_name: the name of the table to which the column belongs
:param column_name: the name of the column
:param meta: the column's meta data
:return: a block of reStructuredText
"""
# Start by creating an internal bookmark for the column.
lines = [f'.. _ref_{table_name}_{column_name}:']
# Create the name of the inline image used to represent the column.
col_img_sub = f'img_{table_name}_{column_name}'
# Add the image definition.
lines.append(f'.. |{col_img_sub}| image:: _static/images/column.svg')
lines.append(self.format_line(':width: 24px', wrap=False))
lines.append(self.format_line(':height: 24px', wrap=False))
lines.append('')
# Create the heading.
heading = f'|{col_img_sub}| **{column_name}**'
lines.append(heading)
lines.append('^' * len(heading))
# Add the label.
lines.append(self.format_line(f'**{meta.label}** - ', wrap=False))
# Add the description.
lines.append(self.format_line(self.simplify_docstring(meta.description)))
# Add the table of Usage values.
lines.append(
self.enum2tbl(enum_cls=Usage, meta=meta, excluded={Usage.NONE},
indent=1))
# Add the table of Requirement values.
lines.append(
self.enum2tbl(enum_cls=Requirement, meta=meta,
excluded={Requirement.NONE}, indent=1))
# If the meta-data indicates there is a related NENA field...
if meta.nena is not None:
# ...we'll include it!
lines.append(self.format_line(f':NENA: *{meta.nena}*'))
# Append a blank line to separate this section from the next one.
lines.append('')
# Put it all together.
rst = '\n'.join(lines)
# Congratulations, we have a formatted reStructuredText string.
return rst
[docs] def cls2rst(self, cls, heading: str, preamble: str=None):
"""
Create a docstring for a model class.
:param cls: the class
:param heading: the heading for the reStructuredText section
:param preamble: everything that should preceed the generated docstring
:return: a reStructuredText docstring
"""
lines = ['']
# If a preamble was supplied...
if preamble is not None:
# ...add it to the top.
lines.append(preamble)
# Figure out what we're going to call the in-line table image.
tbl_img_sub = f'img_tbl_{cls.__name__}'
# Define the in-line table image.
lines.append(f'.. |{tbl_img_sub}| image:: _static/images/table.svg')
lines.append(self.format_line(':width: 24px', wrap=False))
lines.append(self.format_line(':height: 24px', wrap=False))
# We need a couple of blank lines.
lines.append('')
lines.append('')
# Create the heading.
table_name_header = f'|{tbl_img_sub}| {heading}'
lines.append('-' * len(table_name_header))
lines.append(table_name_header)
lines.append('-' * len(table_name_header))
# If the class has it's own docstring...
if cls.__doc__ is not None:
# ...append it now.
lines.append(cls.__doc__)
lines.append('')
# Now add the values.
lines.append(self.format_line(f':Table Name: {cls.__tablename__}'))
lines.append(self.format_line(f':Geometry Type: {cls.geometry_type()}'))
lines.append('') # We need a blank line.
# We're going to go find all the members within the class hierarchy that
# seem to be columns with metadata.
column_members: List[Tuple[str, Column]] = []
# Let's go through every class in the hierarchy...
for mro in inspect.getmro(cls):
# ...updating our list with information about all the members.
column_members.extend(
[
member for member in inspect.getmembers(mro)
if hasattr(member[1], COLUMN_META_ATTR)
]
)
# Eliminate duplicates.
column_members = list(set(column_members))
column_members.sort(key=lambda i: i[0])
# Create the RST documentation for all the column members.
cm_docstrings = [
self.col2section(
table_name=cls.__tablename__,
column_name=cm[0],
meta=cm[1].__meta__)
for cm in column_members
]
cm_docstring = '\n'.join(cm_docstrings)
# Add the collected docstrings for the tables.
lines.append(cm_docstring)
# Put it all together...
rst = '\n'.join(lines)
# ...and that's a block of reStructuredText.
return rst
[docs]def model(label: str):
"""
Use this decorator to provide meta-data for your model class.
:param label: the friendly label for the class
"""
def docstring(cls):
# We'll need a docstring formatter for this job.
docstring_formatter = ModelRstFormatter()
# Get the class' module.
mod = sys.modules[cls.__module__]
mod.__doc__ = docstring_formatter.cls2rst(
cls=cls, heading=label, preamble=mod.__doc__)
# Return the class.
return cls
def modelify(cls):
# If the label parameter hasn't already been specified...
if not hasattr(cls, '__label__'):
# ...update it now.
cls.__label__ = label
# If we're doing a documentation run...
if Modes().sphinx:
# ...update the docstrings.
docstring(cls)
# return the original class to the caller.
return cls
# Return the inner function.
return modelify