gwpy/utils/sphinx/ex2rst.py

Summary

Maintainability
B
5 hrs
Test Coverage
# -*- coding: utf-8 -*-
# Copyright (C) Louisiana State University (2014-2017)
#               Cardiff University (2017-2021)
#
# This file is part of GWpy.
#
# GWpy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GWpy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GWpy.  If not, see <http://www.gnu.org/licenses/>.

"""Convert GWpy example python files into rst files for sphinx documentation
"""

__author__ = 'Duncan Macleod <duncan.macleod@ligo.org'

import argparse
import sys
from pathlib import Path

METADATA = {
    # replacement for __metadata__ variables, use None to ignore
    'author': 'sectionauthor',
    'credits': None,
}


def postprocess_code(code, context):
    if any('plot.show()' in line for line in code):
        ctx = "close-figs"
    else:
        code.insert(2, '   :nofigs:')
        ctx = ""

    code.insert(2, '   :context: {}'.format(context).rstrip())
    code.append('')

    return code, ctx


def ex2rst(infile):
    """Convert a Python example script into RST

    Returns
    -------
    rst : `str`
        the fully rendered RST text block
    """
    infile = Path(infile)
    lines = infile.read_text().splitlines()
    ref = '-'.join((infile.parent.name, infile.with_suffix("").name))

    output = []
    header = ['.. _gwpy-example-%s:\n' % ref]

    indoc = False
    incode = False
    code = []
    context = "reset"

    for i, line in enumerate(lines):
        # skip file header
        if len(output) == 0 and line.startswith('#'):
            continue

        # hide lines
        if line.endswith('# hide'):
            continue

        # find block docs
        if '"""' in line:
            indoc = not indoc
            line = line.strip('"')
            if not line:  # start/end of block quote
                continue

        # skip empty lines not in a block quote
        if not line and not indoc:
            if output:
                output.append('')
            continue

        # finish code block
        if incode and line.startswith(('"', '#', '__')):
            incode = False
            code, context = postprocess_code(code, context)
            output.extend(code)

        # comments
        if line.startswith('#'):
            output.append(line[2:])
        # metadata
        elif line.startswith('__'):
            key, value = map(lambda x: x.strip(' _="\'').rstrip(' _="\''),
                             line.split('=', 1))
            try:
                metakey = METADATA[key]
            except KeyError:
                header.append('.. %s:: %s\n' % (key, value))
            else:
                if metakey is not None:
                    header.append('.. %s:: %s\n' % (METADATA[key], value))
        # block quote
        elif indoc:
            output.append(line.strip('"').rstrip('"'))
        # code
        else:
            if not incode:  # restart code block
                code = [
                    '',
                    '.. plot::',
                    '   :include-source:',
                    '',
                ]
            code.append('   %s' % line)
            incode = True

        # end block quote
        if line == '"""' and indoc:
            indoc = False
        elif line.endswith('"""') and indoc:
            output.append('')
            indoc = False

        if len(output) == 1:
            output.append('#'*len(output[0]))

    if incode:
        output.extend(postprocess_code(code, context)[0])

    output = header + output
    return '\n'.join(output).replace('\n\n\n', '\n\n')


def create_parser():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "infile",
        type=Path,
        metavar="example.py",
        help="python file to convert",
    )
    parser.add_argument(
        "outfile",
        type=Path,
        metavar="example.rst",
        nargs="?",
        help="rst file to write, default: print to screen",
    )
    return parser


def main(args=None):
    # parse command line
    parser = create_parser()
    args = parser.parse_args(args)

    # convert python to RST
    rst = ex2rst(args.infile)

    # write output
    if args.outfile:
        with open(args.outfile, "w") as f:
            print(rst, file=f)
    else:
        print(rst)


if __name__ == "__main__":
    sys.exit(main())