From 4861758fdafb114ec4c1590910e18d254414fce4 Mon Sep 17 00:00:00 2001 From: Chilipp Date: Mon, 26 Mar 2018 13:45:47 +0200 Subject: [PATCH] Improved epilog and description interpretation --- CHANGELOG.rst | 28 +++++ docs/conf.py | 5 - docs/docstring_interpretation.rst | 75 +++++++++++++ docs/index.rst | 2 - funcargparse/__init__.py | 172 ++++++++++++++++++++++++++++-- setup.py | 4 +- tests/test_funcargparse.py | 54 ++++++++++ 7 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..2857ef9 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,28 @@ +v0.2.0 +====== +This release just adds some new interpretation features to extract the +parser description and the epilog from the parsed function. They might +changed how your parser looks drastically! + +Changed +------- +* The default formatter_class for the :class:`FuncArgParser` is now the + :class:`argparse.RawHelpFormatter`, which makes sense since we expect that + the documentations are already nicely formatted +* When calling the :meth:`FuncArgParser.setup_args` method, we also look for + the *Notes* and *References* sections which will be included in the epilog + of the parser. If you want to disable this feature, just initialize the + parser with:: + + parser = FuncArgParser(epilog_sections=[]) + + This feature might cause troubles when being used in sphinx documentations + in conjunction with the sphinx-argparse_ package. For this, you can change + the formatting of the heading with the :attr:`FuncArgParser.epilog_formatter` + attribute + +.. _sphinx-argparse: http://sphinx-argparse.readthedocs.io/en/latest/ + +Added +----- +* Changelog diff --git a/docs/conf.py b/docs/conf.py index cb789a4..ac36b25 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -146,11 +146,6 @@ html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - # Add any paths that contain custom static files (such as style sheets) here, - # relative to this directory. They are copied after the builtin static files, - # so a file named "default.css" will overwrite the builtin "default.css". - html_static_path = ['_static'] - # otherwise, readthedocs.org uses their theme by default, so no need to specify # Theme options are theme-specific and customize the look and feel of a theme diff --git a/docs/docstring_interpretation.rst b/docs/docstring_interpretation.rst index c8cd71b..65266a1 100644 --- a/docs/docstring_interpretation.rst +++ b/docs/docstring_interpretation.rst @@ -105,3 +105,78 @@ You can always disable the points 2-4 by setting ``interprete=False`` in the :meth:`~FuncArgParser.setup_args` and :meth:`~FuncArgParser.setup_subparser` call or you change the arguments by yourself by modifying the :attr:`~FuncArgParser.unfinished_arguments` attribute, etc. + + +Epilog and descriptions +----------------------- +When calling the :meth:`FuncArgParser.setup_args` method or the +:meth:`FuncArgParser.setup_subparser` method, we interprete the `Notes` and +`References` methods as part of the epilog. And we interprete the description +of the function (i.e. the summary and the extended summary) as the description +of the parser. This is illustrated by this small example: + +.. ipython:: + + In [1]: from funcargparse import FuncArgParser + + In [2]: def do_something(a=1): + ...: """This is the summary and will go to the description + ...: + ...: This is the extended summary and will go to the description + ...: + ...: Parameters + ...: ---------- + ...: a: int + ...: This is a parameter that will be accessible as `-a` option + ...: + ...: Notes + ...: ----- + ...: This section will appear in the epilog""" + + In [3]: parser = FuncArgParser(prog='do-something') + + In [4]: parser.setup_args(do_something) + ...: parser.create_arguments() + + In [5]: parser.print_help() + +Which section goes into the epilog is defined by the +:attr:`FuncArgParser.epilog_sections` attribute (specified in the +`epilog_sections` parameter of the :class:`FuncArgParser` class). By default, +we use the `Notes` and `References` section. + +Also the way how the section is formatted can be specified, using the +:attr:`FuncArgParser.epilog_formatter` attribute or the +`epilog_formatter` parameter of the :class:`FuncArgParser` class. By default, +each section will be included with the section header (e.g. *Notes*) followed +by a line of hyphens (``'-'``). But you can also specify a rubric section +formatting (which would be better when being used with the sphinx-argparse_ +package) or any other callable. See the following example: + +.. ipython:: + + In [6]: parser = FuncArgParser() + ...: print(repr(parser.epilog_formatter)) + + In [7]: parser.setup_args(do_something) + ...: print(parser.epilog) + + # Use the bold formatter + In [8]: parser.epilog_formatter = 'bold' + ...: parser.setup_args(do_something, overwrite=True) + ...: print(parser.epilog) + + # Use the rubric directive + In [9]: parser.epilog_formatter = 'rubric' + ...: parser.setup_args(do_something, overwrite=True) + ...: print(parser.epilog) + + # Use a custom function + In [10]: def uppercase_section(section, text): + ....: return section.upper() + '\n' + text + ....: parser.epilog_formatter = uppercase_section + ....: parser.setup_args(do_something, overwrite=True) + ....: print(parser.epilog) + + +.. _sphinx-argparse: http://sphinx-argparse.readthedocs.io/en/latest/ diff --git a/docs/index.rst b/docs/index.rst index c7df3ad..8233194 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,7 +73,6 @@ Content getting_started docstring_interpretation - examples/index api/funcargparse @@ -111,4 +110,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/funcargparse/__init__.py b/funcargparse/__init__.py index cfddbfc..95adfdd 100644 --- a/funcargparse/__init__.py +++ b/funcargparse/__init__.py @@ -1,4 +1,5 @@ from __future__ import print_function, division +import os import six import sys import inspect @@ -22,12 +23,15 @@ import builtins -__version__ = '0.1.2' +__version__ = '0.2.0' docstrings = DocstringProcessor() +_on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + + class FuncArgParser(ArgumentParser): """Subclass of an argument parser that get's parts of the information from a given function""" @@ -37,14 +41,89 @@ class FuncArgParser(ArgumentParser): #: The unfinished arguments after the setup unfinished_arguments = {} + #: The sections to extract from a function docstring that should be used + #: in the epilog of this parser. See also the :meth:`setup_args` method + epilog_sections = ['Notes', 'References'] + + #: The formatter specification for the epilog. This can either be a string + #: out of 'header', 'bold', or + #: 'rubric' or a callable (i.e. function) that takes two arguments, + #: the section title and the section text, and returns a string. + #: + #: 'heading' + #: Use section headers such as:: + #: + #: Notes + #: ----- + #: 'bold' + #: Just make a bold header for the section, e.g. ``**Notes**`` + #: 'rubric' + #: Use a rubric rst directive, e.g. ``.. rubric:: Notes`` + #: + #: .. warning:: + #: + #: When building a sphinx documentation using the sphinx-argparse + #: module, this value should be set to ``'bold'`` or ``'rubric'``! Just + #: add this two lines to your conf.py: + #: + #: .. code-block:: python + #: + #: import funcargparse + #: funcargparse.FuncArgParser.epilog_formatter = 'rubric' + epilog_formatter = 'heading' + def __init__(self, *args, **kwargs): + """ + Parameters + ---------- + ``*args,**kwargs`` + Theses arguments are determined by the + :class:`argparse.ArgumentParser` base class. Note that by default, + we use a :class:`argparse.RawTextHelpFormatter` class for the + `formatter_class` keyword, whereas the + :class:`argparse.ArgumentParser` uses a + :class:`argparse.HelpFormatter` + + Other Parameters + ---------------- + epilog_sections: list of str + The default sections to use for the epilog (see the + :attr:`epilog_sections` attribute). They can also be specified + each time the :meth:`setup_args` method is called + epilog_formatter: {'header', 'bold', 'rubric'} or function + Specify how the epilog sections should be formatted and defaults to + the :attr:`epilog_formatter` attribute. This can either be a string + out of 'header', 'bold', or 'rubric' or a callable (i.e. function) + that takes two arguments, the section title and the section text, + and returns a string. + + 'heading' + Use section headers such as:: + + Notes + ----- + 'bold' + Just make a bold header for the section, e.g. ``**Notes**`` + 'rubric' + Use a rubric rst directive, e.g. ``.. rubric:: Notes`` + """ self._subparsers_action = None + kwargs.setdefault('formatter_class', argparse.RawTextHelpFormatter) + epilog_sections = kwargs.pop('epilog_sections', None) + if epilog_sections is not None: + self.epilog_sections = epilog_sections + epilog_formatter = kwargs.pop('epilog_formatter', None) + if epilog_formatter is not None: + self.epilog_formatter = epilog_formatter super(FuncArgParser, self).__init__(*args, **kwargs) self.unfinished_arguments = OrderedDict() self._used_functions = [] self.__currentarg = None self._chain_subparsers = False self._setup_as = None + self._epilog_formatters = {'heading': self.format_heading, + 'bold': self.format_bold, + 'rubric': self.format_rubric} @staticmethod def get_param_doc(doc, param): @@ -81,7 +160,8 @@ def get_param_doc(doc, param): sections=['Parameters', 'Returns']) @docstrings.dedent def setup_args(self, func=None, setup_as=None, insert_at=None, - interprete=True): + interprete=True, epilog_sections=None, + overwrite=False, append_epilog=True): """ Add the parameters from the given `func` to the parameter settings @@ -102,6 +182,14 @@ def setup_args(self, func=None, setup_as=None, insert_at=None, If True (default), the docstrings are interpreted and switches and lists are automatically inserted (see the [interpretation-docs]_ + epilog_sections: list of str + The headers of the sections to extract. If None, the + :attr:`epilog_sections` attribute is used + overwrite: bool + If True, overwrite the existing epilog and the existing description + of the parser + append_epilog: bool + If True, append to the existing epilog Returns ------- @@ -167,10 +255,16 @@ def setup(func): # create arguments args, varargs, varkw, defaults = inspect.getargspec(func) full_doc = docstrings.dedents(inspect.getdoc(func)) - if not self.description: - summary = docstrings.get_summary(full_doc) - if summary: + + summary = docstrings.get_full_description(full_doc) + if summary: + if not self.description or overwrite: self.description = summary + full_doc = docstrings._remove_summary(full_doc) + + self.extract_as_epilog(full_doc, epilog_sections, overwrite, + append_epilog) + doc = docstrings._get_section(full_doc, 'Parameters') + '\n' doc += docstrings._get_section(full_doc, 'Other Parameters') doc = doc.rstrip() @@ -236,9 +330,10 @@ def add_subparsers(self, *args, **kwargs): return ret @docstrings.dedent - def setup_subparser(self, func=None, setup_as=None, insert_at=None, - interprete=True, return_parser=False, name=None, - **kwargs): + def setup_subparser( + self, func=None, setup_as=None, insert_at=None, interprete=True, + epilog_sections=None, overwrite=False, append_epilog=True, + return_parser=False, name=None, **kwargs): """ Create a subparser with the name of the given function @@ -292,8 +387,10 @@ def setup(func): kwargs.setdefault('help', docstrings.get_summary( docstrings.dedents(inspect.getdoc(func)))) parser = self._subparsers_action.add_parser(name2use, **kwargs) - parser.setup_args(func, setup_as=setup_as, insert_at=insert_at, - interprete=interprete) + parser.setup_args( + func, setup_as=setup_as, insert_at=insert_at, + interprete=interprete, epilog_sections=epilog_sections, + overwrite=overwrite, append_epilog=append_epilog) return func, parser if func is None: return lambda f: setup(f)[0] @@ -464,6 +561,61 @@ def append2helpf(self, arg, s): The string to append to the help""" return self._as_decorator('append2help', arg, s) + @staticmethod + def format_bold(section, text): + """Make a bold formatting for the section header""" + return '**%s**\n\n%s' % (section, text) + + @staticmethod + def format_rubric(section, text): + """Make a bold formatting for the section header""" + return '.. rubric:: %s\n\n%s' % (section, text) + + @staticmethod + def format_heading(section, text): + return '\n'.join([section, '-' * len(section), text]) + + def format_epilog_section(self, section, text): + """Format a section for the epilog by inserting a format""" + try: + func = self._epilog_formatters[self.epilog_formatter] + except KeyError: + if not callable(self.epilog_formatter): + raise + func = self.epilog_formatter + return func(section, text) + + def extract_as_epilog(self, text, sections=None, overwrite=False, + append=True): + """Extract epilog sections from the a docstring + + Parameters + ---------- + text + The docstring to use + sections: list of str + The headers of the sections to extract. If None, the + :attr:`epilog_sections` attribute is used + overwrite: bool + If True, overwrite the existing epilog + append: bool + If True, append to the existing epilog""" + if sections is None: + sections = self.epilog_sections + if ((not self.epilog or overwrite or append) and sections): + epilog_parts = [] + for sec in sections: + text = docstrings._get_section(text, sec).strip() + if text: + epilog_parts.append( + self.format_epilog_section(sec, text)) + if epilog_parts: + epilog = '\n\n'.join(epilog_parts) + if overwrite or not self.epilog: + self.epilog = epilog + else: + self.epilog += '\n\n' + epilog + def grouparg(self, arg, my_arg=None, parent_cmds=[]): """ Grouper function for chaining subcommands diff --git a/setup.py b/setup.py index 1d01e6e..03a962b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def readme(): setup(name='funcargparse', - version='0.1.2', + version='0.2.0', description=( 'Create an argparse.ArgumentParser from function docstrings'), long_description=readme(), @@ -34,7 +34,7 @@ def readme(): packages=find_packages(exclude=['docs', 'tests*', 'examples']), include_package_data=True, install_requires=[ - 'docrep', + 'docrep>=0.2.2', 'six', ], setup_requires=pytest_runner, diff --git a/tests/test_funcargparse.py b/tests/test_funcargparse.py index ec064ea..9b29a07 100755 --- a/tests/test_funcargparse.py +++ b/tests/test_funcargparse.py @@ -2,6 +2,7 @@ import unittest import six from funcargparse import FuncArgParser, docstrings +from docrep import dedents class ParserTest(unittest.TestCase): @@ -375,5 +376,58 @@ def test_func(a=1, b=2): self.assertNotIn('metavar', sp.unfinished_arguments['a']) self.assertIn('Default:', sp.unfinished_arguments['a']['help']) + def test_epilog(self): + """Test whether the epilog is extracted correctly""" + heading = 'Notes\n-----' + content = 'should be included in the parser epilog' + epilog = heading + '\n' + content + + @docstrings.dedent + def test_func(a=1): + """Test function + + Parameters + ---------- + a: int + A parameter""" + + test_func.__doc__ += '\n\n' + epilog + + # test standard heading formatter + parser = FuncArgParser() + parser.setup_args(test_func) + + self.assertEqual(parser.epilog, epilog) + + # test bold formatter + parser = FuncArgParser(epilog_formatter='bold') + parser.setup_args(test_func) + + self.assertEqual(parser.epilog, '**Notes**' + '\n\n' + content) + + # test rubric formatter + parser = FuncArgParser(epilog_formatter='rubric') + parser.setup_args(test_func) + + self.assertEqual(parser.epilog, '.. rubric:: Notes' + '\n\n' + content) + + def formatter(section, text): + return section + text + + # test function formatter + parser = FuncArgParser(epilog_formatter=formatter) + parser.setup_args(test_func) + + self.assertEqual(parser.epilog, 'Notes' + content) + + # test appending + parser.setup_args(test_func) + self.assertEqual(parser.epilog, '\n\n'.join(['Notes' + content] * 2)) + + # test overwrite + parser.setup_args(test_func, overwrite=True) + self.assertEqual(parser.epilog, 'Notes' + content) + + if __name__ == '__main__': unittest.main()