Skip to content

Commit

Permalink
Improved epilog and description interpretation
Browse files Browse the repository at this point in the history
  • Loading branch information
Chilipp committed Mar 26, 2018
1 parent 40d51ad commit 4861758
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 19 deletions.
28 changes: 28 additions & 0 deletions 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
5 changes: 0 additions & 5 deletions docs/conf.py
Expand Up @@ -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
Expand Down
75 changes: 75 additions & 0 deletions docs/docstring_interpretation.rst
Expand Up @@ -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/
2 changes: 0 additions & 2 deletions docs/index.rst
Expand Up @@ -73,7 +73,6 @@ Content

getting_started
docstring_interpretation
examples/index
api/funcargparse


Expand Down Expand Up @@ -111,4 +110,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

172 changes: 162 additions & 10 deletions funcargparse/__init__.py
@@ -1,4 +1,5 @@
from __future__ import print_function, division
import os
import six
import sys
import inspect
Expand All @@ -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"""
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
-------
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Expand Up @@ -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(),
Expand All @@ -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,
Expand Down

0 comments on commit 4861758

Please sign in to comment.