#!/usr/bin/env python
# Copyright (C) 2020 Collin Capano
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Prints an RST table of data options for inference models.
"""
from io import StringIO
import re
import textwrap
from pycbc.inference.models import data_utils
# wrapper for long metavars
metavar_txtwrap = textwrap.TextWrapper(width=34, break_long_words=False)
# convenience class for storing row data
class Row(object):
def __init__(self, divider=None, lpad=0, wrap_option=True):
if divider is None:
divider = ' | '
self.divider = divider
self.lborder = ' '*lpad + divider[1:]
self.rborder = divider[:-1]
self._groupmsg = ''
self.wrap_option = wrap_option
self._option = ''
self._metavar = ''
self._helpmsg = ''
@property
def option(self):
return self._option
@option.setter
def option(self, option):
if self.wrap_option:
# add `` around option string
option = '``'+option+'``'
self._option = option
@property
def metavar(self):
return self._metavar
@metavar.setter
def metavar(self, metavar):
# text wrap if metavar is more than 34 characters wide
self._metavar = '\n'.join(metavar_txtwrap.wrap(metavar))
@staticmethod
def replace_doubledash(str_):
"""Replaces all instances of --arg with ``arg`` in a string."""
pattern = r"--\w+-?\w*"
for s in re.findall(pattern, str_):
rep = '``' + s.replace('--', '') + '``'
str_ = re.sub(s, rep, str_)
return str_
@property
def helpmsg(self):
return self._helpmsg
@helpmsg.setter
def helpmsg(self, msg):
# replace all instances of --arg with ``arg``
self._helpmsg = self.replace_doubledash(msg)
@property
def groupmsg(self):
return self._groupmsg
@groupmsg.setter
def groupmsg(self, msg):
# replace all instances of --arg with ``arg``
self._groupmsg = self.replace_doubledash(msg)
@property
def isgroup(self):
return self.groupmsg != ''
@property
def grouplen(self):
return max(map(len, self.groupmsg.split('\n')))
@property
def metavarlen(self):
return max(map(len, self.metavar.split('\n')))
@property
def helplen(self):
return max(map(len, self.helpmsg.split('\n')))
def format(self, maxlen, optlen, metalen, helplen):
if self.isgroup:
out = ['{lbdr}{msg:<{width}}{rbdr}'.format(lbdr=self.lborder,
msg=msg, width=maxlen,
rbdr=self.rborder)
for msg in self.groupmsg.split('\n')]
else:
tmplt = '{msg:<{rpad}}'
out = []
metavar = self.metavar.split('\n')
helpmsg = self.helpmsg.split('\n')
nlines = max(len(metavar), len(helpmsg))
for ii in range(nlines):
if ii == 0:
optstr = self.option
else:
optstr = ''
if ii < len(metavar):
metastr = metavar[ii]
else:
metastr = ''
if ii < len(helpmsg):
helpstr = helpmsg[ii]
else:
helpstr = ''
optstr = tmplt.format(msg=optstr, rpad=optlen)
metastr = tmplt.format(msg=metastr, rpad=metalen)
helpstr = tmplt.format(msg=helpstr, rpad=helplen)
#rowstr = self.divider.join([optstr, helpstr, metastr])
rowstr = self.divider.join([optstr, metastr, helpstr])
# add borders
rowstr = '{}{}{}'.format(self.lborder, rowstr, self.rborder)
out.append(rowstr)
return '\n'.join(out)
def __len__(self):
if self.isgroup:
baselen = self.grouplen
else:
baselen = len(self.option) + self.metavarlen + self.helplen
return baselen + 2*len(self.divider)
# create a data parser that has the options
parser = data_utils.create_data_parser()
# dump the help message to a file buffer
fp = StringIO()
parser.print_help(file=fp)
# Regular expressions to interpret the help message:
# Lines with a "--option stuff" (i.e., a single space after the option) include
# metadata. Lines with "--option msg" (i.e., multiple spaces after the
# option) contain no metadata, and just go straight to the help message.
regx_optmeta = re.compile(
r'^\s+((-\S, )*)--(?P<option>\S+)\s(?P<metavar>\S.+)')
regx_optmsg = re.compile(r'^\s+((-\S, )*)--(?P<option>\S+)\s+(?P<msg>.+)')
# Note: optmsg will match optmeta lines, so need to test for optmeta first.
# Lines that start with whitespace but do not match optmeta or optmsg will be
# assumed to be the rest of the help message for either an option or a group.
regx_helpmsg = re.compile(r'^\s+(?P<msg>.+)')
# Note: this will match both optmeta and optmsg lines, so need to test for
# for those before this.
# Lines that do not start with whitespace will be considered to be the start of
# a new option group. This is mutually exclusive of all the previous regxs,
# since they all require whitespace at the start of a line.
regx_newgroup = re.compile(r'^(?P<msg>\S.+)')
# now format the string buffer into a rst table
fp.seek(0)
# we want to skip everything up to the "optional arguments:"
skip = True
while skip:
line = fp.readline()
m = regx_newgroup.match(line)
if m is not None:
skip = m.group('msg') not in ['optional arguments:', 'options:']
# advance past the 'optional arguments:' and the 'help' line
line = fp.readline()
line = fp.readline()
# now read through the rest of the lines, converting options into a list of
# tuples with order (option, meta data, help message), grouped by option groups
# add a header row
header = Row(wrap_option=False)
header.option = 'Name'
header.metavar = 'Syntax'
header.helpmsg = 'Description'
table = [header]
while line:
# determine if the line is a new group
newgroup = regx_newgroup.match(line)
if newgroup:
groupmsg = [newgroup.group('msg')]
# continue reading until we get a blank line or an option
line = fp.readline()
while not (line == '' or line == '\n' or regx_optmsg.match(line)):
groupmsg.append(regx_helpmsg.match(line).group('msg'))
line = fp.readline()
# compile the group message
row = Row()
row.groupmsg = '\n'.join(groupmsg)
table.append(row)
# check if the line contains an option
m = regx_optmsg.match(line)
if m:
row = Row()
helpmsg = []
row.option = m.group('option')
# check if the line is an option with metadata
meta = regx_optmeta.match(line)
if meta:
row.metavar = meta.group('metavar')
else:
helpmsg.append(m.group('msg'))
# continue reading to get the help message
line = fp.readline()
while not (line == '' or line == '\n' or regx_optmsg.match(line)):
helpmsg.append(regx_helpmsg.match(line).group('msg'))
line = fp.readline()
# compile the help message
row.helpmsg = '\n'.join(helpmsg)
# remove the list of PSDs from the fake strain and psd model arguments,
# referring instead to the psd table
if (m.group('option') == 'fake-strain' or
m.group('option') == 'psd-model'):
# strip off everything after the "Choose from"
helpmsg = row.helpmsg.replace('\n', ' ')
idx = re.search(r"Choose +from", helpmsg).end()
helpmsg = helpmsg[:idx] + " any available PSD model"
# for fake strain, add zero Noise
if m.group('option') == 'fake-strain':
helpmsg += ', or ``zeroNoise``.'
else:
helpmsg += '.'
row.helpmsg = textwrap.fill(helpmsg, width=54)
# add the row to the table
table.append(row)
else:
# read the next line for the loop
line = fp.readline()
# Now format the table
# get the maximum length of each column
optlen = max([len(row.option) for row in table])
metalen = max([row.metavarlen for row in table])
helplen = max([row.helplen for row in table])
maxlen = optlen + metalen + helplen + 6 # the 6 is for the 2 dividers
# create row separators
# "major" will have == lines
majorline = Row(divider='=+=', wrap_option=False)
majorline.option = '='*optlen
majorline.metavar = '='*metalen
majorline.helpmsg = '='*helplen
# "minor" will have -- lines
minorline = Row(divider='-+-', wrap_option=False)
minorline.option = '-'*optlen
minorline.metavar = '-'*metalen
minorline.helpmsg = '-'*helplen
formatargs = [maxlen, optlen, metalen, helplen]
# Write the formatted table to file
filename = 'inference_data_opts-table.rst'
out = open(filename, 'w')
print(minorline.format(*formatargs), file=out)
# print the header
print(header.format(*formatargs), file=out)
print(majorline.format(*formatargs), file=out)
# print everything else in the table
for ii in range(1, len(table)):
row = table[ii]
print(row.format(*formatargs), file=out)
print(minorline.format(*formatargs), file=out)
out.close()