Skip to content

Commit ab7b5cf

Browse files
committed
Support asyncclick and click
1. only click is installed 2. only asyncclick is installed 3. asyncclick and click are installed Signed-off-by: Jürgen Löhel <[email protected]>
1 parent dc11fe1 commit ab7b5cf

File tree

12 files changed

+252
-34
lines changed

12 files changed

+252
-34
lines changed

Diff for: sphinx_click/ext.py

+105-32
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import inspect
24
import functools
35
import re
@@ -6,10 +8,21 @@
68
import warnings
79

810
try:
9-
import asyncclick as click
11+
import asyncclick
12+
13+
ASYNCCLICK_SUPPORT = True
1014
except ImportError:
15+
ASYNCCLICK_SUPPORT = False
16+
try:
1117
import click
12-
import click.core
18+
19+
CLICK_SUPPORT = True
20+
except ImportError as err:
21+
CLICK_SUPPORT = False
22+
if ASYNCCLICK_SUPPORT:
23+
pass
24+
else:
25+
raise err
1326
from docutils import nodes
1427
from docutils.parsers import rst
1528
from docutils.parsers.rst import directives
@@ -28,13 +41,48 @@
2841

2942
ANSI_ESC_SEQ_RE = re.compile(r'\x1B\[\d+(;\d+){0,2}m', flags=re.MULTILINE)
3043

31-
_T_Formatter = ty.Callable[[click.Context], ty.Generator[str, None, None]]
44+
if ASYNCCLICK_SUPPORT and CLICK_SUPPORT:
45+
click_context_type = asyncclick.Context | click.Context
46+
click_option_type = asyncclick.core.Option | click.core.Option
47+
click_choice_type = asyncclick.Choice | click.Choice
48+
click_argument_type = asyncclick.Argument | click.Argument
49+
click_command_type = asyncclick.Command | click.Command
50+
click_multicommand_type = asyncclick.MultiCommand | click.MultiCommand
51+
click_group_type = asyncclick.Group | click.Group
52+
click_command_collection_type = (
53+
asyncclick.CommandCollection | click.CommandCollection
54+
)
55+
join_options = click.formatting.join_options
56+
elif ASYNCCLICK_SUPPORT:
57+
click_context_type = asyncclick.Context
58+
click_option_type = asyncclick.core.Option
59+
click_choice_type = asyncclick.Choice
60+
click_argument_type = asyncclick.Argument
61+
click_command_type = asyncclick.Command
62+
click_multicommand_type = asyncclick.MultiCommand
63+
click_group_type = asyncclick.Group
64+
click_command_collection_type = asyncclick.CommandCollection
65+
join_options = asyncclick.formatting.join_options
66+
else:
67+
click_context_type = click.Context
68+
click_option_type = click.core.Option
69+
click_choice_type = click.Choice
70+
click_argument_type = click.Argument
71+
click_command_type = click.Command
72+
click_multicommand_type = click.MultiCommand
73+
click_group_type = click.Group
74+
click_command_collection_type = click.CommandCollection
75+
join_options = click.formatting.join_options
76+
77+
_T_Formatter = ty.Callable[[click_context_type], ty.Generator[str, None, None]]
3278

3379

3480
def _process_lines(event_name: str) -> ty.Callable[[_T_Formatter], _T_Formatter]:
3581
def decorator(func: _T_Formatter) -> _T_Formatter:
3682
@functools.wraps(func)
37-
def process_lines(ctx: click.Context) -> ty.Generator[str, None, None]:
83+
def process_lines(
84+
ctx: click_context_type,
85+
) -> ty.Generator[str, None, None]:
3886
lines = list(func(ctx))
3987
if "sphinx-click-env" in ctx.meta:
4088
ctx.meta["sphinx-click-env"].app.events.emit(event_name, ctx, lines)
@@ -56,15 +104,18 @@ def prefixed_lines() -> ty.Generator[str, None, None]:
56104
return ''.join(prefixed_lines())
57105

58106

59-
def _get_usage(ctx: click.Context) -> str:
107+
def _get_usage(ctx: click_context_type) -> str:
60108
"""Alternative, non-prefixed version of 'get_usage'."""
61109
formatter = ctx.make_formatter()
62110
pieces = ctx.command.collect_usage_pieces(ctx)
63111
formatter.write_usage(ctx.command_path, ' '.join(pieces), prefix='')
64112
return formatter.getvalue().rstrip('\n') # type: ignore
65113

66114

67-
def _get_help_record(ctx: click.Context, opt: click.core.Option) -> ty.Tuple[str, str]:
115+
def _get_help_record(
116+
ctx: click_context_type,
117+
opt: click_option_type,
118+
) -> ty.Tuple[str, str]:
68119
"""Re-implementation of click.Opt.get_help_record.
69120
70121
The variant of 'get_help_record' found in Click makes uses of slashes to
@@ -76,7 +127,7 @@ def _get_help_record(ctx: click.Context, opt: click.core.Option) -> ty.Tuple[str
76127
"""
77128

78129
def _write_opts(opts: ty.List[str]) -> str:
79-
rv, _ = click.formatting.join_options(opts)
130+
rv, _ = join_options(opts)
80131
if not opt.is_flag and not opt.count:
81132
name = opt.name
82133
if opt.metavar:
@@ -120,7 +171,7 @@ def _write_opts(opts: ty.List[str]) -> str:
120171
)
121172
)
122173

123-
if isinstance(opt.type, click.Choice):
174+
if isinstance(opt.type, click_choice_type):
124175
extras.append(':options: %s' % ' | '.join(str(x) for x in opt.type.choices))
125176

126177
if extras:
@@ -150,7 +201,9 @@ def _format_help(help_string: str) -> ty.Generator[str, None, None]:
150201

151202

152203
@_process_lines("sphinx-click-process-description")
153-
def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]:
204+
def _format_description(
205+
ctx: click_context_type,
206+
) -> ty.Generator[str, None, None]:
154207
"""Format the description for a given `click.Command`.
155208
156209
We parse this as reStructuredText, allowing users to embed rich
@@ -162,7 +215,9 @@ def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]:
162215

163216

164217
@_process_lines("sphinx-click-process-usage")
165-
def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]:
218+
def _format_usage(
219+
ctx: click_context_type,
220+
) -> ty.Generator[str, None, None]:
166221
"""Format the usage for a `click.Command`."""
167222
yield '.. code-block:: shell'
168223
yield ''
@@ -172,7 +227,8 @@ def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]:
172227

173228

174229
def _format_option(
175-
ctx: click.Context, opt: click.core.Option
230+
ctx: click_context_type,
231+
opt: click_option_type,
176232
) -> ty.Generator[str, None, None]:
177233
"""Format the output for a `click.core.Option`."""
178234
opt_help = _get_help_record(ctx, opt)
@@ -194,13 +250,15 @@ def _format_option(
194250

195251

196252
@_process_lines("sphinx-click-process-options")
197-
def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]:
253+
def _format_options(
254+
ctx: click_context_type,
255+
) -> ty.Generator[str, None, None]:
198256
"""Format all `click.Option` for a `click.Command`."""
199257
# the hidden attribute is part of click 7.x only hence use of getattr
200258
params = [
201259
param
202260
for param in ctx.command.params
203-
if isinstance(param, click.core.Option) and not getattr(param, 'hidden', False)
261+
if isinstance(param, click_option_type) and not getattr(param, 'hidden', False)
204262
]
205263

206264
for param in params:
@@ -209,7 +267,9 @@ def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]:
209267
yield ''
210268

211269

212-
def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]:
270+
def _format_argument(
271+
arg: click_argument_type,
272+
) -> ty.Generator[str, None, None]:
213273
"""Format the output of a `click.Argument`."""
214274
yield '.. option:: {}'.format(arg.human_readable_name)
215275
yield ''
@@ -228,9 +288,11 @@ def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]:
228288

229289

230290
@_process_lines("sphinx-click-process-arguments")
231-
def _format_arguments(ctx: click.Context) -> ty.Generator[str, None, None]:
291+
def _format_arguments(
292+
ctx: click_context_type,
293+
) -> ty.Generator[str, None, None]:
232294
"""Format all `click.Argument` for a `click.Command`."""
233-
params = [x for x in ctx.command.params if isinstance(x, click.Argument)]
295+
params = [x for x in ctx.command.params if isinstance(x, click_argument_type)]
234296

235297
for param in params:
236298
for line in _format_argument(param):
@@ -239,13 +301,13 @@ def _format_arguments(ctx: click.Context) -> ty.Generator[str, None, None]:
239301

240302

241303
def _format_envvar(
242-
param: ty.Union[click.core.Option, click.Argument],
304+
param: click_option_type | click_argument_type,
243305
) -> ty.Generator[str, None, None]:
244306
"""Format the envvars of a `click.Option` or `click.Argument`."""
245307
yield '.. envvar:: {}'.format(param.envvar)
246308
yield ' :noindex:'
247309
yield ''
248-
if isinstance(param, click.Argument):
310+
if isinstance(param, click_argument_type):
249311
param_ref = param.human_readable_name
250312
else:
251313
# if a user has defined an opt with multiple "aliases", always use the
@@ -256,7 +318,9 @@ def _format_envvar(
256318

257319

258320
@_process_lines("sphinx-click-process-envars")
259-
def _format_envvars(ctx: click.Context) -> ty.Generator[str, None, None]:
321+
def _format_envvars(
322+
ctx: click_context_type,
323+
) -> ty.Generator[str, None, None]:
260324
"""Format all envvars for a `click.Command`."""
261325

262326
auto_envvar_prefix = ctx.auto_envvar_prefix
@@ -281,7 +345,9 @@ def _format_envvars(ctx: click.Context) -> ty.Generator[str, None, None]:
281345
yield ''
282346

283347

284-
def _format_subcommand(command: click.Command) -> ty.Generator[str, None, None]:
348+
def _format_subcommand(
349+
command: click_command_type,
350+
) -> ty.Generator[str, None, None]:
285351
"""Format a sub-command of a `click.Command` or `click.Group`."""
286352
yield '.. object:: {}'.format(command.name)
287353

@@ -296,7 +362,9 @@ def _format_subcommand(command: click.Command) -> ty.Generator[str, None, None]:
296362

297363

298364
@_process_lines("sphinx-click-process-epilog")
299-
def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]:
365+
def _format_epilog(
366+
ctx: click_context_type,
367+
) -> ty.Generator[str, None, None]:
300368
"""Format the epilog for a given `click.Command`.
301369
302370
We parse this as reStructuredText, allowing users to embed rich
@@ -306,7 +374,9 @@ def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]:
306374
yield from _format_help(ctx.command.epilog)
307375

308376

309-
def _get_lazyload_commands(ctx: click.Context) -> ty.Dict[str, click.Command]:
377+
def _get_lazyload_commands(
378+
ctx: click_context_type,
379+
) -> ty.Dict[str, click_command_type]:
310380
commands = {}
311381
for command in ctx.command.list_commands(ctx):
312382
commands[command] = ctx.command.get_command(ctx, command)
@@ -315,12 +385,12 @@ def _get_lazyload_commands(ctx: click.Context) -> ty.Dict[str, click.Command]:
315385

316386

317387
def _filter_commands(
318-
ctx: click.Context,
388+
ctx: click_context_type,
319389
commands: ty.Optional[ty.List[str]] = None,
320-
) -> ty.List[click.Command]:
390+
) -> ty.List[click_command_type]:
321391
"""Return list of used commands."""
322392
lookup = getattr(ctx.command, 'commands', {})
323-
if not lookup and isinstance(ctx.command, click.MultiCommand):
393+
if not lookup and isinstance(ctx.command, click_multicommand_type):
324394
lookup = _get_lazyload_commands(ctx)
325395

326396
if commands is None:
@@ -330,7 +400,7 @@ def _filter_commands(
330400

331401

332402
def _format_command(
333-
ctx: click.Context,
403+
ctx: click_context_type,
334404
nested: NestedT,
335405
commands: ty.Optional[ty.List[str]] = None,
336406
) -> ty.Generator[str, None, None]:
@@ -429,7 +499,7 @@ class ClickDirective(rst.Directive):
429499
'show-nested': directives.flag,
430500
}
431501

432-
def _load_module(self, module_path: str) -> ty.Union[click.Command, click.Group]:
502+
def _load_module(self, module_path: str) -> click_command_type | click_group_type:
433503
"""Load the module."""
434504

435505
try:
@@ -460,7 +530,7 @@ def _load_module(self, module_path: str) -> ty.Union[click.Command, click.Group]
460530

461531
parser = getattr(mod, attr_name)
462532

463-
if not isinstance(parser, (click.Command, click.Group)):
533+
if not isinstance(parser, click_command_type | click_group_type):
464534
raise self.error(
465535
'"{}" of type "{}" is not click.Command or click.Group.'
466536
'"click.BaseCommand"'.format(type(parser), module_path)
@@ -470,8 +540,8 @@ def _load_module(self, module_path: str) -> ty.Union[click.Command, click.Group]
470540
def _generate_nodes(
471541
self,
472542
name: str,
473-
command: click.Command,
474-
parent: ty.Optional[click.Context],
543+
command: click_command_type,
544+
parent: ty.Optional[click_context_type],
475545
nested: NestedT,
476546
commands: ty.Optional[ty.List[str]] = None,
477547
semantic_group: bool = False,
@@ -490,7 +560,10 @@ def _generate_nodes(
490560
`click.CommandCollection`.
491561
:returns: A list of nested docutil nodes
492562
"""
493-
ctx = click.Context(command, info_name=name, parent=parent)
563+
if ASYNCCLICK_SUPPORT and isinstance(command, asyncclick.Command):
564+
ctx = asyncclick.Context(command, info_name=name, parent=parent)
565+
else:
566+
ctx = click.Context(command, info_name=name, parent=parent)
494567

495568
if command.hidden:
496569
return []
@@ -523,7 +596,7 @@ def _generate_nodes(
523596
# Subcommands
524597

525598
if nested == NESTED_FULL:
526-
if isinstance(command, click.CommandCollection):
599+
if isinstance(command, click_command_collection_type):
527600
for source in command.sources:
528601
section.extend(
529602
self._generate_nodes(

Diff for: tests/roots/async_basics/conf.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
import sys
3+
4+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
5+
6+
extensions = ['sphinx_click']
7+
8+
autodoc_mock_imports = ["fake_dependency"]

Diff for: tests/roots/async_basics/greet.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""The greet example taken from the README."""
2+
3+
import asyncclick as click
4+
import fake_dependency # Used to test that mocking works
5+
6+
7+
@click.group()
8+
async def greet():
9+
"""A sample command group."""
10+
fake_dependency.do_stuff("hello!")
11+
12+
13+
@greet.command()
14+
@click.argument("user", envvar="USER")
15+
async def hello(user):
16+
"""Greet a user."""
17+
click.echo("Hello %s" % user)
18+
19+
20+
@greet.command()
21+
async def world():
22+
"""Greet the world."""
23+
click.echo("Hello world!")

Diff for: tests/roots/async_basics/index.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Basics
2+
======
3+
4+
.. click:: greet:greet
5+
:prog: greet

Diff for: tests/roots/async_commands/conf.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import os
2+
import sys
3+
4+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
5+
6+
extensions = ['sphinx_click']

Diff for: tests/roots/async_commands/greet.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""The greet example taken from the README."""
2+
3+
import asyncclick as click
4+
5+
6+
@click.group()
7+
async def greet():
8+
"""A sample command group."""
9+
pass
10+
11+
12+
@greet.command()
13+
@click.argument("user", envvar="USER")
14+
async def hello(user):
15+
"""Greet a user."""
16+
click.echo("Hello %s" % user)
17+
18+
19+
@greet.command()
20+
async def world():
21+
"""Greet the world."""
22+
click.echo("Hello world!")

0 commit comments

Comments
 (0)