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. --help Show this message and exit.
Lightning App Commands Lightning App Commands
add Description add Add a name.
To find the arguments of the commands: 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. --help Show this message and exit.
Lightning App Commands Lightning App Commands
run notebook Description run notebook Run a Notebook.
To find the arguments of the commands: To find the arguments of the commands:

View File

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

View File

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

View File

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

View File

@ -10,6 +10,9 @@ class CustomConfig(BaseModel):
class CustomCommand(ClientCommand): class CustomCommand(ClientCommand):
DESCRIPTION = "A command with a client."
def run(self): def run(self):
parser = ArgumentParser() parser = ArgumentParser()
parser.add_argument("--name", type=str) 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 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 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 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 ### Fixed

View File

@ -1,3 +1,4 @@
import json
import os import os
import shutil import shutil
from typing import List, Optional, Tuple 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): if not os.path.exists(commands_folder):
os.makedirs(commands_folder) os.makedirs(commands_folder)
_write_commands_metadata(api_commands)
for command_name, metadata in api_commands.items(): for command_name, metadata in api_commands.items():
if "cls_path" in metadata: if "cls_path" in metadata:
target_file = os.path.join(commands_folder, f"{command_name.replace(' ','_')}.py") 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): if not os.path.exists(commands_folder):
os.makedirs(commands_folder) os.makedirs(commands_folder)
_write_commands_metadata(api_commands)
for command_name, metadata in api_commands.items(): for command_name, metadata in api_commands.items():
if "cls_path" in metadata: if "cls_path" in metadata:
target_file = os.path.join(commands_folder, f"{command_name}.py") 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") 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: def _resolve_command_path(command: str) -> str:
return os.path.join(_get_commands_folder(), f"{command}.py") return os.path.join(_get_commands_folder(), f"{command}.py")
def _list_app_commands() -> List[str]: def _list_app_commands() -> List[str]:
command_names = sorted( metadata = _get_commands_metadata()
n.replace(".py", "").replace(".txt", "").replace("_", " ") metadata = {key.replace("_", " "): value for key, value in metadata.items()}
for n in os.listdir(_get_commands_folder())
if n != "__pycache__" command_names = list(sorted(metadata.keys()))
)
if not command_names: if not command_names:
click.echo("The current Lightning App doesn't have commands.") click.echo("The current Lightning App doesn't have commands.")
return [] return []
@ -196,5 +213,5 @@ def _list_app_commands() -> List[str]:
max_length = max(len(n) for n in command_names) max_length = max(len(n) for n in command_names)
for command_name in command_names: for command_name in command_names:
padding = (max_length + 1 - len(command_name)) * " " 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 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] tag = paths[path]["post"].get("tags", [None])[0]
cls_path = paths[path]["post"].get("cls_path", None) cls_path = paths[path]["post"].get("cls_path", None)
cls_name = paths[path]["post"].get("cls_name", None) cls_name = paths[path]["post"].get("cls_name", None)
description = paths[path]["post"].get("description", None)
metadata = {"tag": tag, "parameters": {}} metadata = {"tag": tag, "parameters": {}}
@ -65,6 +66,9 @@ def _get_metadata_from_openapi(paths: Dict, path: str):
if cls_name: if cls_name:
metadata["cls_name"] = cls_name metadata["cls_name"] = cls_name
if description:
metadata["description"] = description
if not parameters: if not parameters:
return metadata return metadata

View File

@ -35,6 +35,8 @@ class ClientCommand:
def __init__(self, method: Callable, requirements: Optional[List[str]] = None) -> None: def __init__(self, method: Callable, requirements: Optional[List[str]] = None) -> None:
self.method = method self.method = method
if not self.DESCRIPTION:
self.DESCRIPTION = self.method.__doc__ or ""
flow = getattr(method, "__self__", None) flow = getattr(method, "__self__", None)
self.owner = flow.name if flow else None self.owner = flow.name if flow else None
self.requirements = requirements self.requirements = requirements
@ -218,10 +220,13 @@ def _process_requests(app, request: Union[APIRequest, CommandRequest]) -> None:
def _collect_open_api_extras(command) -> Dict: def _collect_open_api_extras(command) -> Dict:
if not isinstance(command, ClientCommand): if not isinstance(command, ClientCommand):
if command.__doc__ is not None:
return {"description": command.__doc__}
return {} return {}
return { return {
"cls_path": inspect.getfile(command.__class__), "cls_path": inspect.getfile(command.__class__),
"cls_name": command.__class__.__name__, "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.", " --help Show this message and exit.",
"", "",
"Lightning App Commands", "Lightning App Commands",
" command with client Description", " command with client A command with a client.",
" command without client Description", " command without client A command without a client.",
" nested command Description", " nested command A nested command.",
] ]
assert messages == expected assert messages == expected
@ -168,9 +168,9 @@ def test_connect_disconnect_cloud(monkeypatch):
" --help Show this message and exit.", " --help Show this message and exit.",
"", "",
"Lightning App Commands", "Lightning App Commands",
" command with client Description", " command with client A command with a client.",
" command without client Description", " command without client A command without a client.",
" nested command Description", " nested command A nested command.",
] ]
assert messages == expected assert messages == expected