Skip to content

Commit e31c727

Browse files
Hugo Osvaldo Barrerastephenfin
authored andcommitted
Add typing hints
This is an attempt at add type hints to the codebase. I'm mostly doing this since I want to better understand some of the helper functions so as to expose a few and make them reusable. There's still a issues in `_format_envvars` and `_generate_nodes`. Both of them take an argument and pass it to a conflicting function.
1 parent 4b8376f commit e31c727

File tree

2 files changed

+69
-53
lines changed

2 files changed

+69
-53
lines changed

sphinx_click/ext.py

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import inspect
22
import re
33
import traceback
4+
import typing as ty
45
import warnings
56

67
import click
78
from docutils import nodes
9+
from docutils import statemachine
10+
from docutils.nodes import section
811
from docutils.parsers import rst
912
from docutils.parsers.rst import directives
10-
from docutils import statemachine
1113
from sphinx.util import logging
1214
from sphinx.util import nodes as sphinx_nodes
15+
from sphinx import application
1316

1417
LOG = logging.getLogger(__name__)
1518

@@ -20,25 +23,25 @@
2023
ANSI_ESC_SEQ_RE = re.compile(r'\x1B\[\d+(;\d+){0,2}m', flags=re.MULTILINE)
2124

2225

23-
def _indent(text, level=1):
26+
def _indent(text: str, level: int = 1) -> str:
2427
prefix = ' ' * (4 * level)
2528

26-
def prefixed_lines():
29+
def prefixed_lines() -> ty.Generator[str, None, None]:
2730
for line in text.splitlines(True):
2831
yield (prefix + line if line.strip() else line)
2932

3033
return ''.join(prefixed_lines())
3134

3235

33-
def _get_usage(ctx):
36+
def _get_usage(ctx: click.Context) -> str:
3437
"""Alternative, non-prefixed version of 'get_usage'."""
3538
formatter = ctx.make_formatter()
3639
pieces = ctx.command.collect_usage_pieces(ctx)
3740
formatter.write_usage(ctx.command_path, ' '.join(pieces), prefix='')
38-
return formatter.getvalue().rstrip('\n')
41+
return formatter.getvalue().rstrip('\n') # type: ignore
3942

4043

41-
def _get_help_record(opt):
44+
def _get_help_record(opt: click.Option) -> ty.Tuple[str, str]:
4245
"""Re-implementation of click.Opt.get_help_record.
4346
4447
The variant of 'get_help_record' found in Click makes uses of slashes to
@@ -49,14 +52,14 @@ def _get_help_record(opt):
4952
[1] http://www.sphinx-doc.org/en/stable/domains.html#directive-option
5053
"""
5154

52-
def _write_opts(opts):
55+
def _write_opts(opts: ty.List[str]) -> str:
5356
rv, _ = click.formatting.join_options(opts)
5457
if not opt.is_flag and not opt.count:
5558
name = opt.name
5659
if opt.metavar:
5760
name = opt.metavar.lstrip('<[{($').rstrip('>]})$')
5861
rv += ' <{}>'.format(name)
59-
return rv
62+
return rv # type: ignore
6063

6164
rv = [_write_opts(opt.opts)]
6265
if opt.secondary_opts:
@@ -101,7 +104,7 @@ def _write_opts(opts):
101104
return ', '.join(rv), '\n'.join(out)
102105

103106

104-
def _format_help(help_string):
107+
def _format_help(help_string: str) -> ty.Generator[str, None, None]:
105108
help_string = inspect.cleandoc(ANSI_ESC_SEQ_RE.sub('', help_string))
106109

107110
bar_enabled = False
@@ -118,7 +121,7 @@ def _format_help(help_string):
118121
yield ''
119122

120123

121-
def _format_description(ctx):
124+
def _format_description(ctx: click.Context) -> ty.Generator[str, None, None]:
122125
"""Format the description for a given `click.Command`.
123126
124127
We parse this as reStructuredText, allowing users to embed rich
@@ -129,7 +132,7 @@ def _format_description(ctx):
129132
yield from _format_help(help_string)
130133

131134

132-
def _format_usage(ctx):
135+
def _format_usage(ctx: click.Context) -> ty.Generator[str, None, None]:
133136
"""Format the usage for a `click.Command`."""
134137
yield '.. code-block:: shell'
135138
yield ''
@@ -138,20 +141,20 @@ def _format_usage(ctx):
138141
yield ''
139142

140143

141-
def _format_option(opt):
144+
def _format_option(opt: click.Option) -> ty.Generator[str, None, None]:
142145
"""Format the output for a `click.Option`."""
143-
opt = _get_help_record(opt)
146+
opt_help = _get_help_record(opt)
144147

145-
yield '.. option:: {}'.format(opt[0])
146-
if opt[1]:
148+
yield '.. option:: {}'.format(opt_help[0])
149+
if opt_help[1]:
147150
yield ''
148151
for line in statemachine.string2lines(
149-
ANSI_ESC_SEQ_RE.sub('', opt[1]), tab_width=4, convert_whitespace=True
152+
ANSI_ESC_SEQ_RE.sub('', opt_help[1]), tab_width=4, convert_whitespace=True
150153
):
151154
yield _indent(line)
152155

153156

154-
def _format_options(ctx):
157+
def _format_options(ctx: click.Context) -> ty.Generator[str, None, None]:
155158
"""Format all `click.Option` for a `click.Command`."""
156159
# the hidden attribute is part of click 7.x only hence use of getattr
157160
params = [
@@ -166,7 +169,7 @@ def _format_options(ctx):
166169
yield ''
167170

168171

169-
def _format_argument(arg):
172+
def _format_argument(arg: click.Argument) -> ty.Generator[str, None, None]:
170173
"""Format the output of a `click.Argument`."""
171174
yield '.. option:: {}'.format(arg.human_readable_name)
172175
yield ''
@@ -177,7 +180,7 @@ def _format_argument(arg):
177180
)
178181

179182

180-
def _format_arguments(ctx):
183+
def _format_arguments(ctx: click.Context) -> ty.Generator[str, None, None]:
181184
"""Format all `click.Argument` for a `click.Command`."""
182185
params = [x for x in ctx.command.params if isinstance(x, click.Argument)]
183186

@@ -187,7 +190,9 @@ def _format_arguments(ctx):
187190
yield ''
188191

189192

190-
def _format_envvar(param):
193+
def _format_envvar(
194+
param: ty.Union[click.Option, click.Argument]
195+
) -> ty.Generator[str, None, None]:
191196
"""Format the envvars of a `click.Option` or `click.Argument`."""
192197
yield '.. envvar:: {}'.format(param.envvar)
193198
yield ' :noindex:'
@@ -202,9 +207,9 @@ def _format_envvar(param):
202207
yield _indent('Provide a default for :option:`{}`'.format(param_ref))
203208

204209

205-
def _format_envvars(ctx):
210+
def _format_envvars(ctx: click.Context) -> ty.Generator[str, None, None]:
206211
"""Format all envvars for a `click.Command`."""
207-
params = [x for x in ctx.command.params if getattr(x, 'envvar')]
212+
params = [x for x in ctx.command.params if x.envvar]
208213

209214
for param in params:
210215
yield '.. _{command_name}-{param_name}-{envvar}:'.format(
@@ -218,7 +223,7 @@ def _format_envvars(ctx):
218223
yield ''
219224

220225

221-
def _format_subcommand(command):
226+
def _format_subcommand(command: click.Command) -> ty.Generator[str, None, None]:
222227
"""Format a sub-command of a `click.Command` or `click.Group`."""
223228
yield '.. object:: {}'.format(command.name)
224229

@@ -232,7 +237,7 @@ def _format_subcommand(command):
232237
yield _indent(line)
233238

234239

235-
def _format_epilog(ctx):
240+
def _format_epilog(ctx: click.Context) -> ty.Generator[str, None, None]:
236241
"""Format the epilog for a given `click.Command`.
237242
238243
We parse this as reStructuredText, allowing users to embed rich
@@ -242,15 +247,18 @@ def _format_epilog(ctx):
242247
yield from _format_help(ctx.command.epilog)
243248

244249

245-
def _get_lazyload_commands(ctx):
250+
def _get_lazyload_commands(ctx: click.Context) -> ty.Dict[str, click.Command]:
246251
commands = {}
247252
for command in ctx.command.list_commands(ctx):
248253
commands[command] = ctx.command.get_command(ctx.command, command)
249254

250255
return commands
251256

252257

253-
def _filter_commands(ctx, commands=None):
258+
def _filter_commands(
259+
ctx: click.Context,
260+
commands: ty.Optional[ty.List[str]] = None,
261+
) -> ty.List[click.Command]:
254262
"""Return list of used commands."""
255263
lookup = getattr(ctx.command, 'commands', {})
256264
if not lookup and isinstance(ctx.command, click.MultiCommand):
@@ -259,14 +267,17 @@ def _filter_commands(ctx, commands=None):
259267
if commands is None:
260268
return sorted(lookup.values(), key=lambda item: item.name)
261269

262-
names = [name.strip() for name in commands.split(',')]
263-
return [lookup[name] for name in names if name in lookup]
270+
return [lookup[command] for command in commands if command in lookup]
264271

265272

266-
def _format_command(ctx, nested, commands=None):
273+
def _format_command(
274+
ctx: click.Context,
275+
nested: str,
276+
commands: ty.Optional[ty.List[str]] = None,
277+
) -> ty.Generator[str, None, None]:
267278
"""Format the output of `click.Command`."""
268279
if ctx.command.hidden:
269-
return
280+
return None
270281

271282
# description
272283

@@ -321,26 +332,24 @@ def _format_command(ctx, nested, commands=None):
321332
if nested in (NESTED_FULL, NESTED_NONE):
322333
return
323334

324-
commands = _filter_commands(ctx, commands)
335+
command_objs = _filter_commands(ctx, commands)
325336

326-
if commands:
337+
if command_objs:
327338
yield '.. rubric:: Commands'
328339
yield ''
329340

330-
for command in commands:
341+
for command_obj in command_objs:
331342
# Don't show hidden subcommands
332-
if command.hidden:
343+
if command_obj.hidden:
333344
continue
334345

335-
for line in _format_subcommand(command):
346+
for line in _format_subcommand(command_obj):
336347
yield line
337348
yield ''
338349

339350

340-
def nested(argument):
341-
values = (NESTED_FULL, NESTED_SHORT, NESTED_NONE)
342-
if not argument:
343-
return None
351+
def nested(argument: ty.Optional[str]) -> ty.Optional[str]:
352+
values = (NESTED_FULL, NESTED_SHORT, NESTED_NONE, None)
344353

345354
if argument not in values:
346355
raise ValueError(
@@ -362,11 +371,8 @@ class ClickDirective(rst.Directive):
362371
'show-nested': directives.flag,
363372
}
364373

365-
def _load_module(self, module_path):
374+
def _load_module(self, module_path: str) -> ty.Union[click.Command, click.Group]:
366375
"""Load the module."""
367-
# __import__ will fail on unicode,
368-
# so we ensure module path is a string here.
369-
module_path = str(module_path)
370376

371377
try:
372378
module_name, attr_name = module_path.split(':', 1)
@@ -395,16 +401,22 @@ def _load_module(self, module_path):
395401

396402
parser = getattr(mod, attr_name)
397403

398-
if not isinstance(parser, click.BaseCommand):
404+
if not isinstance(parser, (click.Command, click.Group)):
399405
raise self.error(
400-
'"{}" of type "{}" is not derived from '
406+
'"{}" of type "{}" is not click.Command or click.Group.'
401407
'"click.BaseCommand"'.format(type(parser), module_path)
402408
)
403409
return parser
404410

405411
def _generate_nodes(
406-
self, name, command, parent, nested, commands=None, semantic_group=False
407-
):
412+
self,
413+
name: str,
414+
command: click.Command,
415+
parent: ty.Optional[click.Context],
416+
nested: str,
417+
commands: ty.Optional[ty.List[str]] = None,
418+
semantic_group: bool = False,
419+
) -> ty.List[section]:
408420
"""Generate the relevant Sphinx nodes.
409421
410422
Format a `click.Group` or `click.Command`.
@@ -416,7 +428,7 @@ def _generate_nodes(
416428
:param commands: Display only listed commands or skip the section if
417429
empty
418430
:param semantic_group: Display command as title and description for
419-
CommandCollection.
431+
`click.CommandCollection`.
420432
:returns: A list of nested docutil nodes
421433
"""
422434
ctx = click.Context(command, info_name=name, parent=parent)
@@ -474,7 +486,7 @@ def _generate_nodes(
474486

475487
return [section]
476488

477-
def run(self):
489+
def run(self) -> ty.Iterable[section]:
478490
self.env = self.state.document.settings.env
479491

480492
command = self._load_module(self.arguments[0])
@@ -498,12 +510,14 @@ def run(self):
498510
)
499511
nested = NESTED_FULL if show_nested else NESTED_SHORT
500512

501-
commands = self.options.get('commands')
513+
commands = [
514+
command.strip() for command in self.options.get('commands', '').split(',')
515+
]
502516

503517
return self._generate_nodes(prog_name, command, None, nested, commands)
504518

505519

506-
def setup(app):
520+
def setup(app: application.Sphinx) -> ty.Dict[str, ty.Any]:
507521
app.add_directive('click', ClickDirective)
508522

509523
return {

tests/test_formatter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ def test_no_commands(self):
695695
"""Validate an empty command group."""
696696

697697
ctx = self._get_ctx()
698-
output = list(ext._format_command(ctx, nested='short', commands=''))
698+
output = list(ext._format_command(ctx, nested='short', commands=[]))
699699

700700
self.assertEqual(
701701
textwrap.dedent(
@@ -715,7 +715,9 @@ def test_order_of_commands(self):
715715
"""Validate the order of commands."""
716716

717717
ctx = self._get_ctx()
718-
output = list(ext._format_command(ctx, nested='short', commands='world, hello'))
718+
output = list(
719+
ext._format_command(ctx, nested='short', commands=['world', 'hello'])
720+
)
719721

720722
self.assertEqual(
721723
textwrap.dedent(

0 commit comments

Comments
 (0)