From 18de60a73d28534e30ae5bddb888a20d22d8058e Mon Sep 17 00:00:00 2001 From: coloursofnoise Date: Wed, 9 Aug 2023 02:45:00 -0700 Subject: [PATCH] Process events (#126) --- docs/usage.rst | 32 ++++++++++++++++++++++++++++++++ sphinx_click/ext.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index e3238a9..bb171af 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -92,6 +92,38 @@ Programs __ https://github.com/sphinx-doc/sphinx/issues/880 +Docstring processing +-------------------- + +*sphinx-click* provides the following additional events: + +.. py:function:: sphinx-click-process-description(app, ctx, lines) +.. py:function:: sphinx-click-process-usage(app, ctx, lines) +.. py:function:: sphinx-click-process-options(app, ctx, lines) +.. py:function:: sphinx-click-process-arguments(app, ctx, lines) +.. py:function:: sphinx-click-process-envvars(app, ctx, lines) +.. py:function:: sphinx-click-process-epilog(app, ctx, lines) + + :param app: the Sphinx application object + :param ctx: the ``click.Context`` object used to generate the description + :param lines: the lines of the documentation, see below + +Events are emitted when sphinx-click has read and processed part of a +command's documentation. *lines* is a list of strings -- the lines of the +documentation that was processed -- that the event handler can +modify **in place** to change what Sphinx puts into the output. + +.. code-block:: python + + def process_description(app, ctx, lines): + """Append some text to the "example" command description.""" + if ctx.command.name == "example": + lines.extend(["Hello, World!", ""]) + + def setup(app): + app.connect("sphinx-click-process-description", process_description) + + Example ------- diff --git a/sphinx_click/ext.py b/sphinx_click/ext.py index f523ad4..158c2bc 100644 --- a/sphinx_click/ext.py +++ b/sphinx_click/ext.py @@ -1,4 +1,5 @@ import inspect +import functools import re import traceback import typing as ty @@ -24,6 +25,23 @@ ANSI_ESC_SEQ_RE = re.compile(r'\x1B\[\d+(;\d+){0,2}m', flags=re.MULTILINE) +_T_Formatter = ty.Callable[[click.Context], ty.Generator[str, None, None]] + + +def _process_lines(event_name: str) -> ty.Callable[[_T_Formatter], _T_Formatter]: + def decorator(func: _T_Formatter) -> _T_Formatter: + @functools.wraps(func) + def process_lines(ctx: click.Context) -> ty.Generator[str, None, None]: + lines = list(func(ctx)) + if "sphinx-click-env" in ctx.meta: + ctx.meta["sphinx-click-env"].app.events.emit(event_name, ctx, lines) + for line in lines: + yield line + + return process_lines + + return decorator + def _indent(text: str, level: int = 1) -> str: prefix = ' ' * (4 * level) @@ -123,6 +141,7 @@ def _format_help(help_string: str) -> ty.Generator[str, None, None]: yield '' +@_process_lines("sphinx-click-process-description") def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]: """Format the description for a given `click.Command`. @@ -134,6 +153,7 @@ def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]: yield from _format_help(help_string) +@_process_lines("sphinx-click-process-usage") def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]: """Format the usage for a `click.Command`.""" yield '.. code-block:: shell' @@ -163,6 +183,7 @@ def _format_option(opt: click.Option) -> ty.Generator[str, None, None]: yield _indent(line) +@_process_lines("sphinx-click-process-options") def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]: """Format all `click.Option` for a `click.Command`.""" # the hidden attribute is part of click 7.x only hence use of getattr @@ -189,6 +210,7 @@ def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]: ) +@_process_lines("sphinx-click-process-arguments") def _format_arguments(ctx: click.Context) -> ty.Generator[str, None, None]: """Format all `click.Argument` for a `click.Command`.""" params = [x for x in ctx.command.params if isinstance(x, click.Argument)] @@ -216,6 +238,7 @@ def _format_envvar( yield _indent('Provide a default for :option:`{}`'.format(param_ref)) +@_process_lines("sphinx-click-process-envars") def _format_envvars(ctx: click.Context) -> ty.Generator[str, None, None]: """Format all envvars for a `click.Command`.""" @@ -255,6 +278,7 @@ def _format_subcommand(command: click.Command) -> ty.Generator[str, None, None]: yield _indent(line) +@_process_lines("sphinx-click-process-epilog") def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]: """Format the epilog for a given `click.Command`. @@ -466,6 +490,7 @@ def _generate_nodes( source_name = ctx.command_path result = statemachine.ViewList() + ctx.meta["sphinx-click-env"] = self.env if semantic_group: lines = _format_description(ctx) else: @@ -539,6 +564,13 @@ def run(self) -> ty.Iterable[nodes.section]: def setup(app: application.Sphinx) -> ty.Dict[str, ty.Any]: app.add_directive('click', ClickDirective) + app.add_event("sphinx-click-process-description") + app.add_event("sphinx-click-process-usage") + app.add_event("sphinx-click-process-options") + app.add_event("sphinx-click-process-arguments") + app.add_event("sphinx-click-process-envvars") + app.add_event("sphinx-click-process-epilog") + return { 'parallel_read_safe': True, 'parallel_write_safe': True,