diff --git a/README.rst b/README.rst index 86cce4b..60ff3a6 100644 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ documentation. .. click:: module:parser :prog: hello-world - :show-nested: + :nested: full Detailed information on the various options available is provided in the `documentation `_. diff --git a/docs/usage.rst b/docs/usage.rst index b5cc7ac..dc5c045 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -20,7 +20,7 @@ Once enabled, *sphinx-click* enables automatic documentation for .. click:: module:parser :prog: hello-world - :show-nested: + :nested: full The directive takes the import name of a *click* object as its sole argument. This should be a subclass of |click.core.BaseCommand|_, such as @@ -35,12 +35,26 @@ Once enabled, *sphinx-click* enables automatic documentation for The following options are optional: - ``:show-nested:`` - Enable full documentation for sub-commands. + ``:nested:`` + Whether subcommands should also be shown. One of: + + ``full`` + List sub-commands with full documentation. + + ``short`` + List sub-commands with short documentation. + + ``none`` + Do not list sub-commands. + + Defaults to ``short`` unless ``show-nested`` (deprecated) is set. ``:commands:`` Document only listed commands. + ``:show-nested:`` + This option is deprecated; use ``nested`` instead. + The generated documentation includes anchors for the generated commands, their options and their environment variables using the `Sphinx standard domain`_. @@ -84,14 +98,28 @@ To document this, use the following: .. click:: hello_world:greet :prog: hello-world -If you wish to include full documentation for the subcommand, ``hello``, in the -output, add the ``show-nested`` flag. +By default, the subcommand, ``hello``, is listed but no documentation provided. +If you wish to include full documentation for the subcommand in the output, +configure the ``nested`` flag to ``full``. + +.. code-block:: rst + + .. click:: hello_world:greet + :prog: hello-world + :nested: full + +.. note:: + + The ``nested`` flag replaces the deprecated ``show-nested`` flag. + +Conversely, if you do not wish to list these subcommands or wish to handle them +separately, configure the ``nested`` flag to ``none``. .. code-block:: rst .. click:: hello_world:greet :prog: hello-world - :show-nested: + :nested: none You can also document only selected commands by using ``:commands:`` option. @@ -123,7 +151,7 @@ Modifying ``sys.path`` ---------------------- If the application or script you wish to document is not installed (i.e. you -have not installed it with `pip` or run ``python setup.py``), then you may need +have not installed it with *pip* or run ``python setup.py``), then you may need to modify ``sys.path``. For example, given the following application:: git @@ -150,7 +178,7 @@ the application: .. click:: git.git:cli :prog: git - :show-nested: + :nested: full assuming the group or command in ``git.git`` is named ``cli``. diff --git a/sphinx_click/ext.py b/sphinx_click/ext.py index eabdb8e..0456f61 100644 --- a/sphinx_click/ext.py +++ b/sphinx_click/ext.py @@ -1,4 +1,5 @@ import traceback +import warnings import click from docutils import nodes @@ -11,6 +12,10 @@ LOG = logging.getLogger(__name__) CLICK_VERSION = tuple(int(x) for x in click.__version__.split('.')[0:2]) +NESTED_FULL = 'full' +NESTED_SHORT = 'short' +NESTED_NONE = 'none' + def _indent(text, level=1): prefix = ' ' * (4 * level) @@ -263,7 +268,7 @@ def _filter_commands(ctx, commands=None): return [lookup[name] for name in names if name in lookup] -def _format_command(ctx, show_nested, commands=None): +def _format_command(ctx, nested, commands=None): """Format the output of `click.Command`.""" if CLICK_VERSION >= (7, 0) and ctx.command.hidden: return @@ -318,7 +323,7 @@ def _format_command(ctx, show_nested, commands=None): yield line # if we're nesting commands, we need to do this slightly differently - if show_nested: + if nested in (NESTED_FULL, NESTED_NONE): return commands = _filter_commands(ctx, commands) @@ -337,14 +342,29 @@ def _format_command(ctx, show_nested, commands=None): yield '' +def nested(argument): + values = (NESTED_FULL, NESTED_SHORT, NESTED_NONE) + if not argument: + return None + + if argument not in values: + raise ValueError( + "%s is not a valid value for ':nested:'; allowed values: %s" + % directives.format_values(values) + ) + + return argument + + class ClickDirective(rst.Directive): has_content = False required_arguments = 1 option_spec = { 'prog': directives.unchanged_required, - 'show-nested': directives.flag, + 'nested': nested, 'commands': directives.unchanged, + 'show-nested': directives.flag, } def _load_module(self, module_path): @@ -387,9 +407,7 @@ def _load_module(self, module_path): ) return parser - def _generate_nodes( - self, name, command, parent=None, show_nested=False, commands=None - ): + def _generate_nodes(self, name, command, parent, nested, commands=None): """Generate the relevant Sphinx nodes. Format a `click.Group` or `click.Command`. @@ -397,7 +415,7 @@ def _generate_nodes( :param name: Name of command, as used on the command line :param command: Instance of `click.Group` or `click.Command` :param parent: Instance of `click.Context`, or None - :param show_nested: Whether subcommands should be included in output + :param nested: The granularity of subcommand details. :param commands: Display only listed commands or skip the section if empty :returns: A list of nested docutil nodes @@ -421,7 +439,7 @@ def _generate_nodes( source_name = ctx.command_path result = statemachine.ViewList() - lines = _format_command(ctx, show_nested, commands) + lines = _format_command(ctx, nested, commands) for line in lines: LOG.debug(line) result.append(line, source_name) @@ -430,12 +448,10 @@ def _generate_nodes( # Subcommands - if show_nested: + if nested == NESTED_FULL: commands = _filter_commands(ctx, commands) for command in commands: - section.extend( - self._generate_nodes(command.name, command, ctx, show_nested) - ) + section.extend(self._generate_nodes(command.name, command, ctx, nested)) return [section] @@ -449,9 +465,23 @@ def run(self): prog_name = self.options.get('prog') show_nested = 'show-nested' in self.options + nested = self.options.get('nested') + + if show_nested: + if nested: + raise self.error( + "':nested:' and ':show-nested:' are mutually exclusive" + ) + else: + warnings.warn( + "':show-nested:' is deprecated; use ':nested: full'", + DeprecationWarning, + ) + nested = NESTED_FULL if show_nested else NESTED_SHORT + commands = self.options.get('commands') - return self._generate_nodes(prog_name, command, None, show_nested, commands) + return self._generate_nodes(prog_name, command, None, nested, commands) def setup(app): diff --git a/tests/test_formatter.py b/tests/test_formatter.py index a5756f6..f81bcb6 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -21,7 +21,7 @@ def foobar(): pass ctx = click.Context(foobar, info_name='foobar') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -58,7 +58,7 @@ def foobar(bar): pass ctx = click.Context(foobar, info_name='foobar') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -121,7 +121,7 @@ def foobar(bar): pass ctx = click.Context(foobar, info_name='foobar') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -164,7 +164,7 @@ def foobar(bar): pass ctx = click.Context(foobar, info_name='foobar') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -202,7 +202,7 @@ def foobar(): pass ctx = click.Context(foobar, info_name='foobar') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual('', '\n'.join(output)) @@ -223,7 +223,7 @@ def hello(name): """ ctx = click.Context(hello, info_name='hello') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -269,7 +269,7 @@ def cli(): pass ctx = click.Context(cli, info_name='cli') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -300,7 +300,7 @@ def cli(): pass ctx = click.Context(cli, info_name='cli') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -365,7 +365,7 @@ def cli(): pass ctx = click.Context(cli, info_name='cli') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -406,14 +406,15 @@ def hello(): return click.Context(cli, info_name='cli') - def test_hide_nested(self): - """Validate a nested command without show_nested. + def test_nested_short(self): + """Validate a nested command with 'nested' of 'short' (default). - If we're not showing sub-commands separately, we should list them. + We should list minimal help texts for sub-commands since they're not + being handled separately. """ ctx = self._get_ctx() - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -435,14 +436,37 @@ def test_hide_nested(self): '\n'.join(output), ) - def test_show_nested(self): - """Validate a nested command with show_nested. + def test_nested_full(self): + """Validate a nested command with 'nested' of 'full'. - If we're not showing sub-commands separately, we should not list them. + We should not list sub-commands since they're being handled separately. """ ctx = self._get_ctx() - output = list(ext._format_command(ctx, show_nested=True)) + output = list(ext._format_command(ctx, nested='full')) + + self.assertEqual( + textwrap.dedent( + """ + A sample command group. + + .. program:: cli + .. code-block:: shell + + cli [OPTIONS] COMMAND [ARGS]... + """ + ).lstrip(), + '\n'.join(output), + ) + + def test_nested_none(self): + """Validate a nested command with 'nested' of 'none'. + + We should not list sub-commands. + """ + + ctx = self._get_ctx() + output = list(ext._format_command(ctx, nested='none')) self.assertEqual( textwrap.dedent( @@ -482,7 +506,7 @@ def test_no_commands(self): """Validate an empty command group.""" ctx = self._get_ctx() - output = list(ext._format_command(ctx, show_nested=False, commands='')) + output = list(ext._format_command(ctx, nested='short', commands='')) self.assertEqual( textwrap.dedent( @@ -502,9 +526,7 @@ def test_order_of_commands(self): """Validate the order of commands.""" ctx = self._get_ctx() - output = list( - ext._format_command(ctx, show_nested=False, commands='world, hello') - ) + output = list(ext._format_command(ctx, nested='short', commands='world, hello')) self.assertEqual( textwrap.dedent( @@ -563,7 +585,7 @@ def get_command(self, ctx, name): cli = MyCLI(help='A sample custom multicommand.') ctx = click.Context(cli, info_name='cli') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) self.assertEqual( textwrap.dedent( @@ -622,7 +644,7 @@ def get_command(self, ctx, name): cli = MyCLI(help='A sample custom multicommand.') ctx = click.Context(cli, info_name='cli') - output = list(ext._format_command(ctx, show_nested=False)) + output = list(ext._format_command(ctx, nested='short')) # Note that we do NOT expect this to show the 'hidden' command self.assertEqual(