Skip to content

Commit 1021aee

Browse files
committed
Add directive to document CLI
Our CLI docs are very out of date. These used to be generated by the docs team with some tooling they had. Since the docs moved in-repo, that tooling has gone away, and for the most part no one has done any updates to the CLI docs. This adds a sphinx directive that will generate these docs every time the docs are built. This way, whenever someone makes a CLI change, they do not need to have to know to also edit a documentation file to match their change. Any code changes will automatically be picked up and reflected in the docs. Change-Id: I4406872ab6e9335e338b710e492171580df74fa5 Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
1 parent 185aa94 commit 1021aee

6 files changed

Lines changed: 193 additions & 4476 deletions

File tree

cinderclient/v2/contrib/list_extensions.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ def show_all(self):
3838

3939

4040
def do_list_extensions(client, _args):
41-
"""
42-
Lists all available os-api extensions.
43-
"""
41+
"""Lists all available os-api extensions."""
4442
extensions = client.list_extensions.show_all()
4543
fields = ["Name", "Summary", "Alias", "Updated"]
4644
utils.print_list(extensions, fields)

cinderclient/v3/shell.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,7 @@ def do_version_list(cs, args):
24102410
default='',
24112411
help='Prefix for the log. ie: "cinder.volume.drivers.".')
24122412
def do_service_set_log(cs, args):
2413+
"""Sets the service log level."""
24132414
cs.services.set_log_levels(args.level, args.binary, args.server,
24142415
args.prefix)
24152416

@@ -2427,6 +2428,7 @@ def do_service_set_log(cs, args):
24272428
default='',
24282429
help='Prefix for the log. ie: "sqlalchemy.".')
24292430
def do_service_get_log(cs, args):
2431+
"""Gets the service log level."""
24302432
log_levels = cs.services.get_log_levels(args.binary, args.server,
24312433
args.prefix)
24322434
columns = ('Binary', 'Host', 'Prefix', 'Level')

doc/ext/__init__.py

Whitespace-only changes.

doc/ext/cli.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
"""Sphinx extension to generate CLI documentation."""
14+
15+
from docutils import nodes
16+
from docutils.parsers import rst
17+
from docutils.parsers.rst import directives
18+
from docutils import statemachine as sm
19+
from sphinx.util import logging
20+
from sphinx.util import nested_parse_with_titles
21+
22+
from cinderclient import api_versions
23+
from cinderclient import shell
24+
25+
LOG = logging.getLogger(__name__)
26+
27+
28+
class CLIDocsDirective(rst.Directive):
29+
"""Directive to generate CLI details into docs output."""
30+
31+
def _get_usage_lines(self, usage, append_value=None):
32+
"""Breaks usage output into separate lines."""
33+
results = []
34+
lines = usage.split('\n')
35+
36+
indent = 0
37+
if '[' in lines[0]:
38+
indent = lines[0].index('[')
39+
40+
for line in lines:
41+
if line.strip():
42+
results.append(line)
43+
44+
if append_value:
45+
results.append(' {}{}'.format(' ' * indent, append_value))
46+
47+
return results
48+
49+
def _format_description_lines(self, description):
50+
"""Formats option description into formatted lines."""
51+
desc = description.split('\n')
52+
return [line.strip() for line in desc if line.strip() != '']
53+
54+
def run(self):
55+
"""Load and document the current config options."""
56+
57+
cindershell = shell.OpenStackCinderShell()
58+
parser = cindershell.get_base_parser()
59+
60+
api_version = api_versions.APIVersion(api_versions.MAX_VERSION)
61+
LOG.info('Generating CLI docs %s', api_version)
62+
63+
cindershell.get_subcommand_parser(api_version, False, [])
64+
65+
result = sm.ViewList()
66+
source = '<{}>'.format(__name__)
67+
68+
result.append('.. _cinder_command_usage:', source)
69+
result.append('', source)
70+
result.append('cinder usage', source)
71+
result.append('------------', source)
72+
result.append('', source)
73+
result.append('.. code-block:: console', source)
74+
result.append('', source)
75+
result.append('', source)
76+
usage = self._get_usage_lines(
77+
parser.format_usage(), '<subcommand> ...')
78+
for line in usage:
79+
result.append(' {}'.format(line), source)
80+
result.append('', source)
81+
82+
result.append('.. _cinder_command_options:', source)
83+
result.append('', source)
84+
result.append('Optional Arguments', source)
85+
result.append('~~~~~~~~~~~~~~~~~~', source)
86+
result.append('', source)
87+
88+
# This accesses a private variable from argparse. That's a little
89+
# risky, but since this is just for the docs and not "production" code,
90+
# and since this variable hasn't changed in years, it's a calculated
91+
# risk to make this documentation generation easier. But if something
92+
# suddenly breaks, check here first.
93+
actions = sorted(parser._actions, key=lambda x: x.option_strings[0])
94+
for action in actions:
95+
if action.help == '==SUPPRESS==':
96+
continue
97+
opts = ', '.join(action.option_strings)
98+
result.append('``{}``'.format(opts), source)
99+
result.append(' {}'.format(action.help), source)
100+
result.append('', source)
101+
102+
result.append('', source)
103+
result.append('.. _cinder_commands:', source)
104+
result.append('', source)
105+
result.append('Commands', source)
106+
result.append('~~~~~~~~', source)
107+
result.append('', source)
108+
109+
for cmd in cindershell.subcommands:
110+
if 'completion' in cmd:
111+
continue
112+
result.append('``{}``'.format(cmd), source)
113+
subcmd = cindershell.subcommands[cmd]
114+
description = self._format_description_lines(subcmd.description)
115+
result.append(' {}'.format(description[0]), source)
116+
result.append('', source)
117+
118+
result.append('', source)
119+
result.append('.. _cinder_command_details:', source)
120+
result.append('', source)
121+
result.append('Command Details', source)
122+
result.append('---------------', source)
123+
result.append('', source)
124+
125+
for cmd in cindershell.subcommands:
126+
if 'completion' in cmd:
127+
continue
128+
subcmd = cindershell.subcommands[cmd]
129+
result.append('.. _cinder{}:'.format(cmd), source)
130+
result.append('', source)
131+
result.append(subcmd.prog, source)
132+
result.append('~' * len(subcmd.prog), source)
133+
result.append('', source)
134+
result.append('.. code-block:: console', source)
135+
result.append('', source)
136+
usage = self._get_usage_lines(subcmd.format_usage())
137+
for line in usage:
138+
result.append(' {}'.format(line), source)
139+
result.append('', source)
140+
description = self._format_description_lines(subcmd.description)
141+
result.append(description[0], source)
142+
result.append('', source)
143+
144+
if len(subcmd._actions) == 0:
145+
continue
146+
147+
positional = []
148+
optional = []
149+
for action in subcmd._actions:
150+
if len(action.option_strings):
151+
if (action.option_strings[0] != '-h' and
152+
action.help != '==SUPPRESS=='):
153+
optional.append(action)
154+
else:
155+
positional.append(action)
156+
157+
if positional:
158+
result.append('**Positional arguments:**', source)
159+
result.append('', source)
160+
for action in positional:
161+
result.append('``{}``'.format(action.metavar), source)
162+
result.append(' {}'.format(action.help), source)
163+
result.append('', source)
164+
165+
if optional:
166+
result.append('**Optional arguments:**', source)
167+
result.append('', source)
168+
for action in optional:
169+
result.append('``{} {}``'.format(
170+
', '.join(action.option_strings), action.metavar),
171+
source)
172+
result.append(' {}'.format(action.help), source)
173+
result.append('', source)
174+
175+
node = nodes.section()
176+
node.document = self.state.document
177+
nested_parse_with_titles(self.state, result, node)
178+
return node.children
179+
180+
181+
def setup(app):
182+
app.add_directive('cli-docs', CLIDocsDirective)
183+
return {
184+
'version': '1.0',
185+
'parallel_read_safe': True,
186+
'parallel_write_safe': True,
187+
}

0 commit comments

Comments
 (0)