Add support for command descriptions (#15193)
This commit is contained in:
parent
24c26f7db2
commit
4acb10f981
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue