Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added output format options for --help #180

Merged
merged 14 commits into from
Aug 7, 2015
72 changes: 72 additions & 0 deletions lib/vsc/utils/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# #
# Copyright 2015-2015 Ghent University
#
# This file is part of vsc-base,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
# the Hercules foundation (http://www.herculesstichting.be/in_English)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# http://github.com/hpcugent/vsc-base
#
# vsc-base is free software: you can redistribute it and/or modify
# it under the terms of the GNU Library General Public License as
# published by the Free Software Foundation, either version 2 of
# the License, or (at your option) any later version.
#
# vsc-base 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 Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with vsc-base. If not, see <http://www.gnu.org/licenses/>.
# #
"""
Functions for generating rst documentation

@author: Caroline De Brouwer (Ghent University)
"""

INDENT_4SPACES = ' ' * 4


class LengthNotEqualException(ValueError):
pass


def mk_rst_table(titles, columns):
"""
Returns an rst table with given titles and columns (a nested list of string columns for each column)
"""
title_cnt, col_cnt = len(titles), len(columns)
if title_cnt != col_cnt:
msg = "Number of titles/columns should be equal, found %d titles and %d columns" % (title_cnt, col_cnt)
raise LengthNotEqualException, msg
table = []
col_widths = []
tmpl = []
line= []

# figure out column widths
for i, title in enumerate(titles):
width = max(map(len, columns[i] + [title]))

# make line template
tmpl.append('{%s:{c}<%s}' % (i, width))

line = [''] * col_cnt
line_tmpl = INDENT_4SPACES.join(tmpl)
table_line = line_tmpl.format(*line, c='=')

table.append(table_line)
table.append(line_tmpl.format(*titles, c=' '))
table.append(table_line)

for row in map(list, zip(*columns)):
table.append(line_tmpl.format(*row, c=' '))

table.extend([table_line, ''])

return table
84 changes: 74 additions & 10 deletions lib/vsc/utils/generaloption.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#
#
# #
# Copyright 2011-2014 Ghent University
#
# This file is part of vsc-base,
Expand Down Expand Up @@ -46,11 +45,15 @@
from optparse import SUPPRESS_HELP as nohelp # supported in optparse of python v2.4
from optparse import _ as _gettext # this is gettext normally
from vsc.utils.dateandtime import date_parser, datetime_parser
from vsc.utils.docs import mk_rst_table
from vsc.utils.fancylogger import getLogger, setLogLevel, getDetailsLogLevels
from vsc.utils.missing import shell_quote, nub
from vsc.utils.optcomplete import autocomplete, CompleterOption


HELP_OUTPUTOPTIONS = ['', 'rst', 'short', 'config',]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename + drop trailing ,

HELP_OUTPUT_FORMAT_OPTIONS = ['', 'rst', 'short', 'config']

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or better: HELP_OUTPUT_FORMATS



def set_columns(cols=None):
"""Set os.environ COLUMNS variable
- only if it is not set already
Expand Down Expand Up @@ -162,9 +165,9 @@ class ExtOption(CompleterOption):
DISABLE = 'disable' # inverse action

EXTOPTION_EXTRA_OPTIONS = ('date', 'datetime', 'regex', 'add', 'add_first', 'add_flex',)
EXTOPTION_STORE_OR = ('store_or_None',) # callback type
EXTOPTION_STORE_OR = ('store_or_None', 'help') # callback type
EXTOPTION_LOG = ('store_debuglog', 'store_infolog', 'store_warninglog',)
EXTOPTION_HELP = ('shorthelp', 'confighelp',)
EXTOPTION_HELP = ('shorthelp', 'confighelp', 'help')

ACTIONS = Option.ACTIONS + EXTOPTION_EXTRA_OPTIONS + EXTOPTION_STORE_OR + EXTOPTION_LOG + EXTOPTION_HELP
STORE_ACTIONS = Option.STORE_ACTIONS + EXTOPTION_EXTRA_OPTIONS + EXTOPTION_LOG + ('store_or_None',)
Expand Down Expand Up @@ -211,9 +214,10 @@ def store_or(option, opt_str, value, parser, *args, **kwargs):
self.callback = store_or
self.callback_kwargs = {
'orig_default': copy.deepcopy(self.default),
}
}
self.action = 'callback' # act as callback
if self.store_or == 'store_or_None':

if self.store_or in self.EXTOPTION_STORE_OR:
self.default = None
else:
self.log.raiseException("_set_attrs: unknown store_or %s" % self.store_or, exception=ValueError)
Expand All @@ -222,7 +226,16 @@ def take_action(self, action, dest, opt, value, values, parser):
"""Extended take_action"""
orig_action = action # keep copy

if action == 'shorthelp':
# dest is None for actions like shorthelp and confighelp
if dest and getattr(parser._long_opt.get('--' + dest, ''), 'store_or', '') == 'help':
Option.take_action(self, action, dest, opt, value, values, parser)
fn = getattr(parser, 'print_%shelp' % values.help)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a default or check that the method/attribute exists

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking can be done with hasattr

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a default like

fn = getattr(parser, 'print_%shelp' % values.help, None)
if fn is None:
  ... some error
else:
    fn()
parser.exit()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Caylo: you forgot the , None bit on this line

if fn is None:
self.log.raiseException("Unknown option for help: %s" % value.help, exception=ValueError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Unknown option/Unsupported output format/

else:
fn()
parser.exit()
elif action == 'shorthelp':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open an issue to convert shorthelp and confighelp in aliases of resp help=short and help=config (it's not straightforward, so a bit out of the scope of this PR)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #181

parser.print_shorthelp()
parser.exit()
elif action == 'confighelp':
Expand Down Expand Up @@ -567,8 +580,7 @@ def print_shorthelp(self, fh=None):

self.print_help(fh)

def print_help(self, fh=None):
"""Intercept print to file to print to string and remove the ENABLE/DISABLE options from help"""
def check_help(self, fh):
if self.help_to_string:
self.help_to_file = StringIO.StringIO()
if fh is None:
Expand All @@ -583,9 +595,53 @@ def _is_enable_disable(x):
for opt in self._get_all_options():
# remove all long_opts with ENABLE/DISABLE naming
opt._long_opts = [x for x in opt._long_opts if not _is_enable_disable(x)]
return fh

def print_help(self, fh=None):
"""Intercept print to file to print to string and remove the ENABLE/DISABLE options from help"""
fh = self.check_help(fh)
OptionParser.print_help(self, fh)

def print_rsthelp(self, fh=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just drop the fh all together, we're not using it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm

""" Print help in rst format """
fh = self.check_help(fh)
result = []
if self.usage:
result.append("Usage: ``%s``" % self.get_usage().replace("Usage: ", '').strip())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dedicated Usage section?

result.append('')
if self.description:
result.append(self.description)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description section?

result.append('')

result.append(self.format_option_rsthelp())

rsthelptxt = '\n'.join(result)
if fh is None:
fh = sys.stdout
fh.write(rsthelptxt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just print instead, no need to support printing to a file handle (since print_rsthelp is only called without arguments)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm


def format_option_rsthelp(self, formatter=None):
""" Formatting for help in rst format """
if not formatter:
formatter = self.formatter
formatter.store_option_strings(self)

res = []
titles = ["Option", "Help"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with

titles = ["Option flag", "Option description"]


all_opts = [("Options", self.option_list)] + [(group.title, group.option_list) for group in self.option_groups]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.option_list only contains --help and co + --version, maybe rename "Options" to "Help options" ?

for title, opts in all_opts:
values = []
res.extend([title, '-' * len(title)])
for opt in opts:
if not opt.help is nohelp:
values.append(['``%s``' % formatter.option_strings[opt], formatter.expand_default(opt)])

res.extend(mk_rst_table(titles, map(list, zip(*values))))
res.append('')

return '\n'.join(res)

def print_confighelp(self, fh=None):
"""Print help as a configfile."""

Expand Down Expand Up @@ -634,6 +690,9 @@ def _add_help_option(self):
self.add_option("-%s" % self.longhelp[0],
self.longhelp[1], # *self.longhelp[1:], syntax error in Python 2.4
action="help",
type="choice",
choices=HELP_OUTPUTOPTIONS,
default='',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this also needs a choice to limit the supproted help formats (and unittests)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needed to become a choice thingie?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a choice with possible suppoted help formats

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HELP_OUTPUT_FORMATS[0]?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add this:

metavar='OUTPUT_FORMAT',

help=_gettext("show full help message and exit"))
self.add_option("--confighelp",
action="confighelp",
Expand Down Expand Up @@ -792,6 +851,7 @@ def __init__(self, **kwargs):
'usage': kwargs.get('usage', self.USAGE),
'version': self.VERSION,
})

self.parser = self.PARSER(**kwargs)
self.parser.allow_interspersed_args = self.INTERSPERSED

Expand Down Expand Up @@ -1423,6 +1483,10 @@ def generate_cmd_line(self, ignore=None, add_default=None):
opt_dests.sort()

for opt_dest in opt_dests:
# help is store_or_None, but is not a processed option, so skip it
if opt_dest in ExtOption.EXTOPTION_HELP:
continue

opt_value = self.options.__dict__[opt_dest]
# this is the action as parsed by the class, not the actual action set in option
# (eg action store_or_None is shown here as store_or_None, not as callback)
Expand Down Expand Up @@ -1453,7 +1517,7 @@ def generate_cmd_line(self, ignore=None, add_default=None):
(opt_name, opt_value))
continue

if action in ('store_or_None',):
if action in ExtOption.EXTOPTION_STORE_OR:
if opt_value == default:
self.log.debug("generate_cmd_line %s adding %s (value is default value %s)" %
(action, opt_name, opt_value))
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def remove_bdist_rpm_source_file():

PACKAGE = {
'name': 'vsc-base',
'version': '2.2.3',
'version': '2.2.4',
'author': [sdw, jt, ag, kh],
'maintainer': [sdw, jt, ag, kh],
'packages': ['vsc', 'vsc.utils', 'vsc.install'],
Expand Down
67 changes: 67 additions & 0 deletions test/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
##
# Copyright 2015-2015 Ghent University
#
# This file is part of vsc-base,
# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
# with support of Ghent University (http://ugent.be/hpc),
# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
# the Hercules foundation (http://www.herculesstichting.be/in_English)
# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
#
# http://github.com/hpcugent/vsc-base
#
# vsc-base 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 v2.
#
# vsc-base 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 EasyBuild. If not, see <http://www.gnu.org/licenses/>.
##
"""
Unit tests for the docs module.

@author: Kenneth Hoste (Ghent University)
@author: Caroline De Brouwer (Ghent University)
"""
import os
from unittest import TestLoader, TestCase, main
from vsc.utils.testing import EnhancedTestCase

from vsc.utils.docs import mk_rst_table


class DocsTest(EnhancedTestCase):
"""Tests for docs functions."""

def test_mk_rst_table(self):
"""Test mk_rst_table function."""
entries = [['one', 'two', 'three']]
t = 'This title is longer than the entries in the column'
titles = [t]

# small table
table = mk_rst_table(titles, entries)
check = [
'=' * len(t),
t,
'=' * len(t),
'one' + ' ' * (len(t) - 3),
'two' + ' ' * (len(t) -3),
'three' + ' ' * (len(t) - 5),
'=' * len(t),
'',
]

self.assertEqual(table, check)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a rst parser / validation tool for python?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://pypi.python.org/pypi/docutils probably has, but that may be overkill here?

a hard check is fine here imho

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok for now. but good to know we could use such a tool in more complex tests if ever needed

def suite():
""" returns all the testcases in this module """
return TestLoader().loadTestsFromTestCase(DocsTest)

if __name__ == '__main__':
main()
26 changes: 24 additions & 2 deletions test/generaloption.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from unittest import TestCase, TestLoader, main

from vsc.utils import fancylogger
from vsc.utils.generaloption import GeneralOption
from vsc.utils.generaloption import GeneralOption, HELP_OUTPUTOPTIONS
from vsc.utils.missing import shell_quote, shell_unquote
from vsc.utils.optcomplete import gen_cmdline
from vsc.utils.run import run_simple
Expand Down Expand Up @@ -126,6 +126,17 @@ def test_help_short(self):
self.assertEqual(topt.parser.help_to_file.getvalue().find("--level-longlevel"), -1,
"Long documentation not expanded in short help")

def test_help(self):
"""Generate (long) help message"""
topt = TestOption1(go_args=['--help'],
go_nosystemexit=True,
go_columns=100,
help_to_string=True,
prog='optiontest1',
)
self.assertTrue(topt.parser.help_to_file.getvalue().find("--level-longlevel") > -1,
"Long documentation expanded in long help")

def test_help_long(self):
"""Generate long help message"""
topt = TestOption1(go_args=['-H'],
Expand All @@ -137,6 +148,16 @@ def test_help_long(self):
self.assertTrue(topt.parser.help_to_file.getvalue().find("--level-longlevel") > -1,
"Long documentation expanded in long help")

def test_help_outputformats(self):
"""Generate (long) rst help message"""
for choice in HELP_OUTPUTOPTIONS:
topt = TestOption1(go_args=['--help=%s' % choice],
go_nosystemexit=True,
go_columns=100,
help_to_string=True,
prog='optiontest1',
)

def test_help_confighelp(self):
"""Generate long help message"""
topt = TestOption1(go_args=['--confighelp'],
Expand Down Expand Up @@ -187,6 +208,7 @@ def test_generate_cmdline(self):
])
self.assertEqual(topt.options.__dict__,
{
'help': None,
'store': 'some whitespace',
'debug': True,
'info': False,
Expand All @@ -195,7 +217,7 @@ def test_generate_cmdline(self):
'longbase': True,
'justatest': True,
'level_longlevel': True,
'store_with_dash':None,
'store_with_dash': None,
'level_prefix_and_dash':'YY', # this dict is about destinations
'ignoreconfigfiles': None,
'configfiles': ['/not/a/real/configfile'],
Expand Down
6 changes: 3 additions & 3 deletions test/missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,9 @@ def test_modules_in_pkg_path(self):
"""Test modules_in_pkg_path function."""
# real example
import vsc.utils
vsc_utils_modules = ['__init__', 'affinity', 'asyncprocess', 'daemon', 'dateandtime', 'exceptions', 'fancylogger',
'frozendict', 'generaloption', 'mail', 'missing', 'optcomplete', 'patterns', 'rest',
'run', 'testing', 'wrapper']
vsc_utils_modules = ['__init__', 'affinity', 'asyncprocess', 'daemon', 'dateandtime', 'docs',
'exceptions', 'fancylogger', 'frozendict', 'generaloption', 'mail', 'missing',
'optcomplete', 'patterns', 'rest', 'run', 'testing', 'wrapper']
self.assertEqual(sorted(modules_in_pkg_path(vsc.utils.__path__[0])), vsc_utils_modules)
self.assertEqual(sorted(modules_in_pkg_path('vsc/utils')), vsc_utils_modules)

Expand Down
Loading