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.
|
--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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue