click-contrib / sphinx-click

A Sphinx plugin to automatically document click-based applications
MIT License
212 stars 57 forks source link

Add support for Flask's `with_appcontext` decorator #139

Open azmeuk opened 2 months ago

azmeuk commented 2 months ago

I would like to generate documentation for dynamically generated click commands, depending on a Flask context. Running sphinx-build raises RuntimeError: There is no active click context., while running the commands works as expected (for instance python commands.py sub foo)

The code consists in a ParametrizedCommand class that dynamically generate a list of commands. For the sake of simplicity on this snippet, this is not quite dynamical, but at least this generate the very same exception I meet with my more complex real world use case.

Removing @with_appcontext would make sphinx-build work, but I cannot do this since I need the application context in get_command in my real use case.

commands.py

import click
from flask import Flask
from flask.cli import FlaskGroup
from flask.cli import with_appcontext

def create_app():
    return Flask(__name__)

class ParametrizedCommand(click.Group):
    valid_command_names = ["foo", "bar", "baz"]

    def list_commands(self, ctx):
        base = super().list_commands(ctx)
        return base + self.valid_command_names

    @with_appcontext
    def get_command(self, ctx, cmd_name):
        @click.command(name=cmd_name, help=f"Help for {cmd_name}")
        def command(*args, **kwargs):
            click.echo(f"Executing {cmd_name}")

        return command

@click.group(cls=FlaskGroup, create_app=create_app)
def cli():
    ...

@cli.command(cls=ParametrizedCommand)
def sub():
    ...

if __name__ == "__main__":
    cli()

index.rst

.. click:: commands:cli
   :prog: cli
   :nested: full

traceback

# Platform:         linux; (Linux-6.8.9-arch1-2-x86_64-with-glibc2.39)
# Sphinx version:   7.3.7
# Python version:   3.12.3 (CPython)
# Docutils version: 0.21.2
# Jinja2 version:   3.1.4
# Pygments version: 2.18.0

# Last messages:
#   writing output...
#
#   building [html]: targets for 1 source files that are out of date
#   updating environment:
#   [new config]
#   1 added, 0 changed, 0 removed
#
#   reading sources... [100%]
#   index
#

# Loaded extensions:
#   sphinx.ext.mathjax (7.3.7)
#   alabaster (0.7.16)
#   sphinxcontrib.applehelp (1.0.8)
#   sphinxcontrib.devhelp (1.0.6)
#   sphinxcontrib.htmlhelp (2.0.5)
#   sphinxcontrib.serializinghtml (1.1.10)
#   sphinxcontrib.qthelp (1.0.7)
#   sphinx.ext.autodoc.preserve_defaults (7.3.7)
#   sphinx.ext.autodoc.type_comment (7.3.7)
#   sphinx.ext.autodoc.typehints (7.3.7)
#   sphinx.ext.autodoc (7.3.7)
#   sphinx.ext.autosectionlabel (7.3.7)
#   sphinx.ext.doctest (7.3.7)
#   sphinx.ext.graphviz (7.3.7)
#   sphinx.ext.intersphinx (7.3.7)
#   sphinx.ext.todo (7.3.7)
#   sphinx.ext.viewcode (7.3.7)
#   sphinx_click (unknown version)

# Traceback:
Traceback (most recent call last):
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/click/globals.py", line 37, in get_current_context
    return t.cast("Context", _local.stack[-1])
                             ^^^^^^^^^^^^
AttributeError: '_thread._local' object has no attribute 'stack'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/cmd/build.py", line 337, in build_main
    app.build(args.force_all, args.filenames)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/application.py", line 351, in build
    self.builder.build_update()
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 293, in build_update
    self.build(to_build,
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 313, in build
    updated_docnames = set(self.read())
                           ^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 419, in read
    self._read_serial(docnames)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 440, in _read_serial
    self.read_doc(docname)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 497, in read_doc
    publisher.publish()
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/core.py", line 234, in publish
    self.document = self.reader.read(self.source, self.parser,
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/io.py", line 107, in read
    self.parse()
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/readers/__init__.py", line 76, in parse
    self.parser.parse(self.input, document)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx/parsers.py", line 83, in parse
    self.statemachine.run(inputlines, document, inliner=self.inliner)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 169, in run
    results = StateMachineWS.run(self, input_lines, input_offset,
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/statemachine.py", line 233, in run
    context, next_state, result = self.check_line(
                                  ^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/statemachine.py", line 445, in check_line
    return method(match, context, next_state)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 2790, in underline
    self.section(title, source, style, lineno - 1, messages)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 325, in section
    self.new_subsection(title, lineno, messages)
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 391, in new_subsection
    newabsoffset = self.nested_parse(
                   ^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 279, in nested_parse
    state_machine.run(block, input_offset, memo=self.memo,
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 195, in run
    results = StateMachineWS.run(self, input_lines, input_offset)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/statemachine.py", line 233, in run
    context, next_state, result = self.check_line(
                                  ^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/statemachine.py", line 445, in check_line
    return method(match, context, next_state)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 2357, in explicit_markup
    nodelist, blank_finish = self.explicit_construct(match)
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 2369, in explicit_construct
    return method(self, expmatch)
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 2106, in directive
    return self.run_directive(
           ^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/docutils/parsers/rst/states.py", line 2156, in run_directive
    result = directive_instance.run()
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx_click/ext.py", line 563, in run
    return self._generate_nodes(prog_name, command, None, nested, commands)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx_click/ext.py", line 526, in _generate_nodes
    self._generate_nodes(
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx_click/ext.py", line 522, in _generate_nodes
    commands = _filter_commands(ctx, commands)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx_click/ext.py", line 308, in _filter_commands
    lookup = _get_lazyload_commands(ctx)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/sphinx_click/ext.py", line 296, in _get_lazyload_commands
    commands[command] = ctx.command.get_command(ctx, command)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/home/eloi/.virtualenvs/test-soai/lib/python3.12/site-packages/click/globals.py", line 40, in get_current_context
    raise RuntimeError("There is no active click context.") from e
RuntimeError: There is no active click context.
stephenfin commented 2 months ago

I rarely use flask and have never written a flask CLI plugin, but I'm happy to accept PRs to fix this if you can figure out what needs to be done.

stephenfin commented 2 months ago

As a workaround, you might be able to mock that decorator (or the things it's calling). More information on mocking here.