Skip to content

Commit cc9ce93

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 cc9ce93

File tree

12 files changed

+252
-35
lines changed

12 files changed

+252
-35
lines changed

Diff for: sphinx_click/ext.py

+104-32
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44
import traceback
55
import typing as ty
66
import warnings
7+
from __future__ import annotations
78

89
try:
9-
import asyncclick as click
10+
import asyncclick
11+
12+
ASYNCCLICK_SUPPORT = True
1013
except ImportError:
14+
ASYNCCLICK_SUPPORT = False
15+
try:
1116
import click
12-
import click.core
17+
18+
CLICK_SUPPORT = True
19+
except ImportError as err:
20+
CLICK_SUPPORT = False
21+
if ASYNCCLICK_SUPPORT:
22+
pass
23+
else:
24+
raise err
1325
from docutils import nodes
1426
from docutils.parsers import rst
1527
from docutils.parsers.rst import directives
@@ -28,13 +40,48 @@
2840

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

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

3378

3479
def _process_lines(event_name: str) -> ty.Callable[[_T_Formatter], _T_Formatter]:
3580
def decorator(func: _T_Formatter) -> _T_Formatter:
3681
@functools.wraps(func)
37-
def process_lines(ctx: click.Context) -> ty.Generator[str, None, None]:
82+
def process_lines(
83+
ctx: click_context_type,
84+
) -> ty.Generator[str, None, None]:
3885
lines = list(func(ctx))
3986
if "sphinx-click-env" in ctx.meta:
4087
ctx.meta["sphinx-click-env"].app.events.emit(event_name, ctx, lines)
@@ -56,15 +103,18 @@ def prefixed_lines() -> ty.Generator[str, None, None]:
56103
return ''.join(prefixed_lines())
57104

58105

59-
def _get_usage(ctx: click.Context) -> str:
106+
def _get_usage(ctx: click_context_type) -> str:
60107
"""Alternative, non-prefixed version of 'get_usage'."""
61108
formatter = ctx.make_formatter()
62109
pieces = ctx.command.collect_usage_pieces(ctx)
63110
formatter.write_usage(ctx.command_path, ' '.join(pieces), prefix='')
64111
return formatter.getvalue().rstrip('\n') # type: ignore
65112

66113

67-
def _get_help_record(ctx: click.Context, opt: click.core.Option) -> ty.Tuple[str, str]:
114+
def _get_help_record(
115+
ctx: click_context_type,
116+
opt: click_option_type,
117+
) -> ty.Tuple[str, str]:
68118
"""Re-implementation of click.Opt.get_help_record.
69119
70120
The variant of 'get_help_record' found in Click makes uses of slashes to
@@ -76,7 +126,7 @@ def _get_help_record(ctx: click.Context, opt: click.core.Option) -> ty.Tuple[str
76126
"""
77127

78128
def _write_opts(opts: ty.List[str]) -> str:
79-
rv, _ = click.formatting.join_options(opts)
129+
rv, _ = join_options(opts)
80130
if not opt.is_flag and not opt.count:
81131
name = opt.name
82132
if opt.metavar:
@@ -120,7 +170,7 @@ def _write_opts(opts: ty.List[str]) -> str:
120170
)
121171
)
122172

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

126176
if extras:
@@ -150,7 +200,9 @@ def _format_help(help_string: str) -> ty.Generator[str, None, None]:
150200

151201

152202
@_process_lines("sphinx-click-process-description")
153-
def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]:
203+
def _format_description(
204+
ctx: click_context_type,
205+
) -> ty.Generator[str, None, None]:
154206
"""Format the description for a given `click.Command`.
155207
156208
We parse this as reStructuredText, allowing users to embed rich
@@ -162,7 +214,9 @@ def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]:
162214

163215

164216
@_process_lines("sphinx-click-process-usage")
165-
def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]:
217+
def _format_usage(
218+
ctx: click_context_type,
219+
) -> ty.Generator[str, None, None]:
166220
"""Format the usage for a `click.Command`."""
167221
yield '.. code-block:: shell'
168222
yield ''
@@ -172,7 +226,8 @@ def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]:
172226

173227

174228
def _format_option(
175-
ctx: click.Context, opt: click.core.Option
229+
ctx: click_context_type,
230+
opt: click_option_type,
176231
) -> ty.Generator[str, None, None]:
177232
"""Format the output for a `click.core.Option`."""
178233
opt_help = _get_help_record(ctx, opt)
@@ -194,13 +249,15 @@ def _format_option(
194249

195250

196251
@_process_lines("sphinx-click-process-options")
197-
def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]:
252+
def _format_options(
253+
ctx: click_context_type,
254+
) -> ty.Generator[str, None, None]:
198255
"""Format all `click.Option` for a `click.Command`."""
199256
# the hidden attribute is part of click 7.x only hence use of getattr
200257
params = [
201258
param
202259
for param in ctx.command.params
203-
if isinstance(param, click.core.Option) and not getattr(param, 'hidden', False)
260+
if isinstance(param, click_option_type) and not getattr(param, 'hidden', False)
204261
]
205262

206263
for param in params:
@@ -209,7 +266,9 @@ def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]:
209266
yield ''
210267

211268

212-
def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]:
269+
def _format_argument(
270+
arg: click_argument_type,
271+
) -> ty.Generator[str, None, None]:
213272
"""Format the output of a `click.Argument`."""
214273
yield '.. option:: {}'.format(arg.human_readable_name)
215274
yield ''
@@ -228,9 +287,11 @@ def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]:
228287

229288

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

235296
for param in params:
236297
for line in _format_argument(param):
@@ -239,13 +300,13 @@ def _format_arguments(ctx: click.Context) -> ty.Generator[str, None, None]:
239300

240301

241302
def _format_envvar(
242-
param: ty.Union[click.core.Option, click.Argument],
303+
param: click_option_type | click_argument_type,
243304
) -> ty.Generator[str, None, None]:
244305
"""Format the envvars of a `click.Option` or `click.Argument`."""
245306
yield '.. envvar:: {}'.format(param.envvar)
246307
yield ' :noindex:'
247308
yield ''
248-
if isinstance(param, click.Argument):
309+
if isinstance(param, click_argument_type):
249310
param_ref = param.human_readable_name
250311
else:
251312
# if a user has defined an opt with multiple "aliases", always use the
@@ -256,7 +317,9 @@ def _format_envvar(
256317

257318

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

262325
auto_envvar_prefix = ctx.auto_envvar_prefix
@@ -281,7 +344,9 @@ def _format_envvars(ctx: click.Context) -> ty.Generator[str, None, None]:
281344
yield ''
282345

283346

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

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

297362

298363
@_process_lines("sphinx-click-process-epilog")
299-
def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]:
364+
def _format_epilog(
365+
ctx: click_context_type,
366+
) -> ty.Generator[str, None, None]:
300367
"""Format the epilog for a given `click.Command`.
301368
302369
We parse this as reStructuredText, allowing users to embed rich
@@ -306,7 +373,9 @@ def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]:
306373
yield from _format_help(ctx.command.epilog)
307374

308375

309-
def _get_lazyload_commands(ctx: click.Context) -> ty.Dict[str, click.Command]:
376+
def _get_lazyload_commands(
377+
ctx: click_context_type,
378+
) -> ty.Dict[str, click_command_type]:
310379
commands = {}
311380
for command in ctx.command.list_commands(ctx):
312381
commands[command] = ctx.command.get_command(ctx, command)
@@ -315,12 +384,12 @@ def _get_lazyload_commands(ctx: click.Context) -> ty.Dict[str, click.Command]:
315384

316385

317386
def _filter_commands(
318-
ctx: click.Context,
387+
ctx: click_context_type,
319388
commands: ty.Optional[ty.List[str]] = None,
320-
) -> ty.List[click.Command]:
389+
) -> ty.List[click_command_type]:
321390
"""Return list of used commands."""
322391
lookup = getattr(ctx.command, 'commands', {})
323-
if not lookup and isinstance(ctx.command, click.MultiCommand):
392+
if not lookup and isinstance(ctx.command, click_multicommand_type):
324393
lookup = _get_lazyload_commands(ctx)
325394

326395
if commands is None:
@@ -330,7 +399,7 @@ def _filter_commands(
330399

331400

332401
def _format_command(
333-
ctx: click.Context,
402+
ctx: click_context_type,
334403
nested: NestedT,
335404
commands: ty.Optional[ty.List[str]] = None,
336405
) -> ty.Generator[str, None, None]:
@@ -429,7 +498,7 @@ class ClickDirective(rst.Directive):
429498
'show-nested': directives.flag,
430499
}
431500

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

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

461530
parser = getattr(mod, attr_name)
462531

463-
if not isinstance(parser, (click.Command, click.Group)):
532+
if not isinstance(parser, click_command_type | click_group_type):
464533
raise self.error(
465534
'"{}" of type "{}" is not click.Command or click.Group.'
466535
'"click.BaseCommand"'.format(type(parser), module_path)
@@ -470,8 +539,8 @@ def _load_module(self, module_path: str) -> ty.Union[click.Command, click.Group]
470539
def _generate_nodes(
471540
self,
472541
name: str,
473-
command: click.Command,
474-
parent: ty.Optional[click.Context],
542+
command: click_command_type,
543+
parent: ty.Optional[click_context_type],
475544
nested: NestedT,
476545
commands: ty.Optional[ty.List[str]] = None,
477546
semantic_group: bool = False,
@@ -490,7 +559,10 @@ def _generate_nodes(
490559
`click.CommandCollection`.
491560
:returns: A list of nested docutil nodes
492561
"""
493-
ctx = click.Context(command, info_name=name, parent=parent)
562+
if ASYNCCLICK_SUPPORT and isinstance(command, asyncclick.Command):
563+
ctx = asyncclick.Context(command, info_name=name, parent=parent)
564+
else:
565+
ctx = click.Context(command, info_name=name, parent=parent)
494566

495567
if command.hidden:
496568
return []
@@ -523,7 +595,7 @@ def _generate_nodes(
523595
# Subcommands
524596

525597
if nested == NESTED_FULL:
526-
if isinstance(command, click.CommandCollection):
598+
if isinstance(command, click_command_collection_type):
527599
for source in command.sources:
528600
section.extend(
529601
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)