Skip to content

Commit fc013da

Browse files
new: Improve generated command help page formatting using Rich (#585)
1 parent 5f5bf72 commit fc013da

File tree

8 files changed

+496
-319
lines changed

8 files changed

+496
-319
lines changed

linodecli/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
from linodecli import plugins
1616

1717
from .arg_helpers import (
18-
action_help,
1918
bake_command,
20-
help_with_ops,
2119
register_args,
2220
register_plugin,
2321
remove_plugin,
2422
)
2523
from .cli import CLI
2624
from .completion import bake_completions, get_completions
2725
from .configuration import ENV_TOKEN_NAME
26+
from .help_pages import print_help_action, print_help_default
2827
from .helpers import handle_url_overrides
2928
from .output import OutputMode
3029

@@ -155,7 +154,7 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
155154
# handle a help for the CLI
156155
if parsed.command is None or (parsed.command is None and parsed.help):
157156
parser.print_help()
158-
help_with_ops(cli.ops, cli.config)
157+
print_help_default(cli.ops, cli.config)
159158
sys.exit(0)
160159

161160
# configure
@@ -257,6 +256,6 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
257256

258257
if parsed.command is not None and parsed.action is not None:
259258
if parsed.help:
260-
action_help(cli, parsed.command, parsed.action)
259+
print_help_action(cli, parsed.command, parsed.action)
261260
sys.exit(0)
262261
cli.handle_command(parsed.command, parsed.action, args)

linodecli/arg_helpers.py

Lines changed: 0 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,10 @@
55

66
import os
77
import sys
8-
import textwrap
98
from importlib import import_module
109

1110
import requests
1211
import yaml
13-
from rich import box
14-
from rich import print as rprint
15-
from rich.table import Table
1612

1713
from linodecli import plugins
1814

@@ -247,176 +243,6 @@ def remove_plugin(plugin_name, config):
247243
return f"Plugin {plugin_name} removed", 0
248244

249245

250-
# pylint: disable=too-many-locals
251-
def help_with_ops(ops, config):
252-
"""
253-
Prints help output with options from the API spec
254-
"""
255-
256-
# Environment variables overrides
257-
print("\nEnvironment variables:")
258-
env_variables = {
259-
"LINODE_CLI_TOKEN": "A Linode Personal Access Token for the CLI to make requests with. "
260-
"If specified, the configuration step will be skipped.",
261-
"LINODE_CLI_CA": "The path to a custom Certificate Authority file to verify "
262-
"API requests against.",
263-
"LINODE_CLI_API_HOST": "Overrides the target host for API requests. "
264-
"(e.g. 'api.linode.com')",
265-
"LINODE_CLI_API_VERSION": "Overrides the target Linode API version for API requests. "
266-
"(e.g. 'v4beta')",
267-
"LINODE_CLI_API_SCHEME": "Overrides the target scheme used for API requests. "
268-
"(e.g. 'https')",
269-
}
270-
271-
table = Table(show_header=True, header_style="", box=box.SQUARE)
272-
table.add_column("Name")
273-
table.add_column("Description")
274-
275-
for k, v in env_variables.items():
276-
table.add_row(k, v)
277-
278-
rprint(table)
279-
280-
# commands to manage CLI users (don't call out to API)
281-
print("\nCLI user management commands:")
282-
um_commands = [["configure", "set-user", "show-users"], ["remove-user"]]
283-
table = Table(show_header=False)
284-
for cmd in um_commands:
285-
table.add_row(*cmd)
286-
rprint(table)
287-
288-
# commands to manage plugins (don't call out to API)
289-
print("\nCLI Plugin management commands:")
290-
pm_commands = [["register-plugin", "remove-plugin"]]
291-
table = Table(show_header=False)
292-
for cmd in pm_commands:
293-
table.add_row(*cmd)
294-
rprint(table)
295-
296-
# other CLI commands
297-
print("\nOther CLI commands:")
298-
other_commands = [["completion"]]
299-
table = Table(show_header=False)
300-
for cmd in other_commands:
301-
table.add_row(*cmd)
302-
rprint(table)
303-
304-
# commands generated from the spec (call the API directly)
305-
print("\nAvailable commands:")
306-
307-
content = list(sorted(ops.keys()))
308-
proc = []
309-
for i in range(0, len(content), 3):
310-
proc.append(content[i : i + 3])
311-
if content[i + 3 :]:
312-
proc.append(content[i + 3 :])
313-
314-
table = Table(show_header=False)
315-
for cmd in proc:
316-
table.add_row(*cmd)
317-
rprint(table)
318-
319-
# plugins registered to the CLI (do arbitrary things)
320-
if plugins.available(config):
321-
# only show this if there are any available plugins
322-
print("Available plugins:")
323-
324-
plugin_content = list(plugins.available(config))
325-
plugin_proc = []
326-
327-
for i in range(0, len(plugin_content), 3):
328-
plugin_proc.append(plugin_content[i : i + 3])
329-
if plugin_content[i + 3 :]:
330-
plugin_proc.append(plugin_content[i + 3 :])
331-
332-
plugin_table = Table(show_header=False)
333-
for plugin in plugin_proc:
334-
plugin_table.add_row(*plugin)
335-
rprint(plugin_table)
336-
337-
print("\nTo reconfigure, call `linode-cli configure`")
338-
print(
339-
"For comprehensive documentation,"
340-
"visit https://www.linode.com/docs/api/"
341-
)
342-
343-
344-
def action_help(cli, command, action): # pylint: disable=too-many-branches
345-
"""
346-
Prints help relevant to the command and action
347-
"""
348-
try:
349-
op = cli.find_operation(command, action)
350-
except ValueError:
351-
return
352-
print(f"linode-cli {command} {action}", end="")
353-
354-
for param in op.params:
355-
pname = param.name.upper()
356-
print(f" [{pname}]", end="")
357-
358-
print()
359-
print(op.summary)
360-
361-
if op.docs_url:
362-
rprint(f"API Documentation: [link={op.docs_url}]{op.docs_url}[/link]")
363-
364-
if len(op.samples) > 0:
365-
print()
366-
print(f"Example Usage{'s' if len(op.samples) > 1 else ''}: ")
367-
368-
rprint(
369-
*[
370-
# Indent all samples for readability; strip and trailing newlines
371-
textwrap.indent(v.get("source").rstrip(), " ")
372-
for v in op.samples
373-
],
374-
sep="\n\n",
375-
)
376-
377-
print()
378-
if op.method == "get" and op.action == "list":
379-
filterable_attrs = [
380-
attr for attr in op.response_model.attrs if attr.filterable
381-
]
382-
383-
if filterable_attrs:
384-
print("You may filter results with:")
385-
for attr in filterable_attrs:
386-
print(f" --{attr.name}")
387-
print(
388-
"Additionally, you may order results using --order-by and --order."
389-
)
390-
return
391-
if op.args:
392-
print("Arguments:")
393-
for arg in sorted(op.args, key=lambda s: not s.required):
394-
if arg.read_only:
395-
continue
396-
is_required = (
397-
"(required) "
398-
if op.method in {"post", "put"} and arg.required
399-
else ""
400-
)
401-
402-
extensions = []
403-
404-
if arg.format == "json":
405-
extensions.append("JSON")
406-
407-
if arg.nullable:
408-
extensions.append("nullable")
409-
410-
if arg.is_parent:
411-
extensions.append("conflicts with children")
412-
413-
suffix = (
414-
f" ({', '.join(extensions)})" if len(extensions) > 0 else ""
415-
)
416-
417-
print(f" --{arg.path}: {is_required}{arg.description}{suffix}")
418-
419-
420246
def bake_command(cli, spec_loc):
421247
"""
422248
Handle a bake command from args

linodecli/baked/request.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def __init__(
1616
prefix=None,
1717
is_parent=False,
1818
parent=None,
19+
depth=0,
1920
): # pylint: disable=too-many-arguments
2021
"""
2122
Parses a single Schema node into a argument the CLI can use when making
@@ -33,6 +34,8 @@ def __init__(
3334
:type is_parent: bool
3435
:param parent: If applicable, the path to the parent list for this argument.
3536
:type parent: Optional[str]
37+
:param depth: The depth of this argument, or how many parent arguments this argument has.
38+
:type depth: int
3639
"""
3740
#: The name of this argument, mostly used for display and docs
3841
self.name = name
@@ -85,6 +88,10 @@ def __init__(
8588
#: e.g. --interfaces.ipv4.nat_1_1
8689
self.parent = parent
8790

91+
#: The depth of this argument, or how many parent arguments this argument has.
92+
#: This is useful when formatting help pages.
93+
self.depth = depth
94+
8895
#: The path of the path element in the schema.
8996
self.prefix = prefix
9097

@@ -103,7 +110,7 @@ def __init__(
103110
)
104111

105112

106-
def _parse_request_model(schema, prefix=None, parent=None):
113+
def _parse_request_model(schema, prefix=None, parent=None, depth=0):
107114
"""
108115
Parses a schema into a list of OpenAPIRequest objects
109116
:param schema: The schema to parse as a request model
@@ -130,6 +137,9 @@ def _parse_request_model(schema, prefix=None, parent=None):
130137
v,
131138
prefix=pref,
132139
parent=parent,
140+
# NOTE: We do not increment the depth because dicts do not have
141+
# parent arguments.
142+
depth=depth,
133143
)
134144
elif (
135145
v.type == "array"
@@ -150,10 +160,16 @@ def _parse_request_model(schema, prefix=None, parent=None):
150160
prefix=prefix,
151161
is_parent=True,
152162
parent=parent,
163+
depth=depth,
153164
)
154165
)
155166

156-
args += _parse_request_model(v.items, prefix=pref, parent=pref)
167+
args += _parse_request_model(
168+
v.items,
169+
prefix=pref,
170+
parent=pref,
171+
depth=depth + 1,
172+
)
157173
else:
158174
# required fields are defined in the schema above the property, so
159175
# we have to check here if required fields are defined/if this key
@@ -163,7 +179,12 @@ def _parse_request_model(schema, prefix=None, parent=None):
163179
required = k in schema.required
164180
args.append(
165181
OpenAPIRequestArg(
166-
k, v, required, prefix=prefix, parent=parent
182+
k,
183+
v,
184+
required,
185+
prefix=prefix,
186+
parent=parent,
187+
depth=depth,
167188
)
168189
)
169190

0 commit comments

Comments
 (0)