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()