Add support for command descriptions (#15193)

This commit is contained in:
Ethan Harris 2022-10-19 17:34:35 +01:00 committed by GitHub
parent 24c26f7db2
commit 4acb10f981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 50 additions and 15 deletions

View File

@ -64,7 +64,7 @@ To see a list of available commands:
--help Show this message and exit.
Lightning App Commands
add Description
add Add a name.
To find the arguments of the commands:

View File

@ -80,7 +80,7 @@ To see a list of available commands:
--help Show this message and exit.
Lightning App Commands
run notebook Description
run notebook Run a Notebook.
To find the arguments of the commands:

View File

@ -13,6 +13,8 @@ class RunNotebookConfig(BaseModel):
class RunNotebook(ClientCommand):
DESCRIPTION = "Run a Notebook."
def run(self):
# 1. Define your own argument parser. You can use argparse, click, etc...
parser = ArgumentParser(description='Run Notebook Parser')

View File

@ -10,6 +10,7 @@ class Flow(LightningFlow):
print(self.names)
def add_name(self, name: str):
"""Add a name."""
print(f"Received name: {name}")
self.names.append(name)

View File

@ -7,6 +7,7 @@ from lightning_app.core.app import LightningApp
class ChildFlow(LightningFlow):
def nested_command(self, name: str):
"""A nested command."""
print(f"Hello {name}")
def configure_commands(self):
@ -24,6 +25,7 @@ class FlowCommands(LightningFlow):
print(self.names)
def command_without_client(self, name: str):
"""A command without a client."""
self.names.append(name)
def command_with_client(self, config: CustomConfig):

View File

@ -10,6 +10,9 @@ class CustomConfig(BaseModel):
class CustomCommand(ClientCommand):
DESCRIPTION = "A command with a client."
def run(self):
parser = ArgumentParser()
parser.add_argument("--name", type=str)

View File

@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
- Added a `--secret` option to CLI to allow binding secrets to app environment variables when running in the cloud ([#14612](https://github.com/Lightning-AI/lightning/pull/14612))
- Added support for running the works without cloud compute in the default container ([#14819](https://github.com/Lightning-AI/lightning/pull/14819))
- Added an HTTPQueue as an optional replacement for the default redis queue ([#14978](https://github.com/Lightning-AI/lightning/pull/14978)
- Added support for adding descriptions to commands either through a docstring or the `DESCRIPTION` attribute ([#15193](https://github.com/Lightning-AI/lightning/pull/15193)
### Fixed

View File

@ -1,3 +1,4 @@
import json
import os
import shutil
from typing import List, Optional, Tuple
@ -50,6 +51,8 @@ def connect(app_name_or_id: str, yes: bool = False):
if not os.path.exists(commands_folder):
os.makedirs(commands_folder)
_write_commands_metadata(api_commands)
for command_name, metadata in api_commands.items():
if "cls_path" in metadata:
target_file = os.path.join(commands_folder, f"{command_name.replace(' ','_')}.py")
@ -99,6 +102,8 @@ def connect(app_name_or_id: str, yes: bool = False):
if not os.path.exists(commands_folder):
os.makedirs(commands_folder)
_write_commands_metadata(api_commands)
for command_name, metadata in api_commands.items():
if "cls_path" in metadata:
target_file = os.path.join(commands_folder, f"{command_name}.py")
@ -174,16 +179,28 @@ def _get_commands_folder() -> str:
return os.path.join(lightning_folder, "commands")
def _write_commands_metadata(api_commands):
metadata = {command_name: metadata for command_name, metadata in api_commands.items()}
metadata_path = os.path.join(_get_commands_folder(), ".meta.json")
with open(metadata_path, "w") as f:
json.dump(metadata, f)
def _get_commands_metadata():
metadata_path = os.path.join(_get_commands_folder(), ".meta.json")
with open(metadata_path) as f:
return json.load(f)
def _resolve_command_path(command: str) -> str:
return os.path.join(_get_commands_folder(), f"{command}.py")
def _list_app_commands() -> List[str]:
command_names = sorted(
n.replace(".py", "").replace(".txt", "").replace("_", " ")
for n in os.listdir(_get_commands_folder())
if n != "__pycache__"
)
metadata = _get_commands_metadata()
metadata = {key.replace("_", " "): value for key, value in metadata.items()}
command_names = list(sorted(metadata.keys()))
if not command_names:
click.echo("The current Lightning App doesn't have commands.")
return []
@ -196,5 +213,5 @@ def _list_app_commands() -> List[str]:
max_length = max(len(n) for n in command_names)
for command_name in command_names:
padding = (max_length + 1 - len(command_name)) * " "
click.echo(f" {command_name}{padding}Description")
click.echo(f" {command_name}{padding}{metadata[command_name].get('description', '')}")
return command_names

View File

@ -56,6 +56,7 @@ def _get_metadata_from_openapi(paths: Dict, path: str):
tag = paths[path]["post"].get("tags", [None])[0]
cls_path = paths[path]["post"].get("cls_path", None)
cls_name = paths[path]["post"].get("cls_name", None)
description = paths[path]["post"].get("description", None)
metadata = {"tag": tag, "parameters": {}}
@ -65,6 +66,9 @@ def _get_metadata_from_openapi(paths: Dict, path: str):
if cls_name:
metadata["cls_name"] = cls_name
if description:
metadata["description"] = description
if not parameters:
return metadata

View File

@ -35,6 +35,8 @@ class ClientCommand:
def __init__(self, method: Callable, requirements: Optional[List[str]] = None) -> None:
self.method = method
if not self.DESCRIPTION:
self.DESCRIPTION = self.method.__doc__ or ""
flow = getattr(method, "__self__", None)
self.owner = flow.name if flow else None
self.requirements = requirements
@ -218,10 +220,13 @@ def _process_requests(app, request: Union[APIRequest, CommandRequest]) -> None:
def _collect_open_api_extras(command) -> Dict:
if not isinstance(command, ClientCommand):
if command.__doc__ is not None:
return {"description": command.__doc__}
return {}
return {
"cls_path": inspect.getfile(command.__class__),
"cls_name": command.__class__.__name__,
"description": command.DESCRIPTION,
}

File diff suppressed because one or more lines are too long

View File

@ -65,9 +65,9 @@ def test_connect_disconnect_local(monkeypatch):
" --help Show this message and exit.",
"",
"Lightning App Commands",
" command with client Description",
" command without client Description",
" nested command Description",
" command with client A command with a client.",
" command without client A command without a client.",
" nested command A nested command.",
]
assert messages == expected
@ -168,9 +168,9 @@ def test_connect_disconnect_cloud(monkeypatch):
" --help Show this message and exit.",
"",
"Lightning App Commands",
" command with client Description",
" command without client Description",
" nested command Description",
" command with client A command with a client.",
" command without client A command without a client.",
" nested command A nested command.",
]
assert messages == expected