Skip to content

Commit

Permalink
switch tree command to --tree option
Browse files Browse the repository at this point in the history
  • Loading branch information
gpetretto committed Oct 23, 2024
1 parent f9e18c4 commit d2d5bc7
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 124 deletions.
3 changes: 3 additions & 0 deletions src/jobflow_remote/cli/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,9 @@ def report(
),
] = None,
):
"""
Generate a report about the Flows in the database.
"""
jc = get_job_controller()

timezone = datetime.now(tzlocal()).tzname()
Expand Down
71 changes: 3 additions & 68 deletions src/jobflow_remote/cli/jf.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
from typing import Annotated, Optional
from typing import Annotated

import typer
from rich.text import Text
from rich.tree import Tree

from jobflow_remote.cli.jfr_typer import JFRTyper
from jobflow_remote.cli.types import tree_opt
from jobflow_remote.cli.utils import (
cleanup_job_controller,
complete_profiling,
find_subcommand,
get_command_tree,
get_config_manager,
initialize_config_manager,
out_console,
Expand Down Expand Up @@ -68,6 +66,7 @@ def main(
hidden=True,
),
] = False,
print_tree: tree_opt = False, # If selected will print the tree of the CLI and exit
) -> None:
"""The controller CLI for jobflow-remote."""
from jobflow_remote import SETTINGS
Expand Down Expand Up @@ -99,67 +98,3 @@ def main(
except ConfigError:
# no warning printed if not needed as this seems to be confusing for the user
pass


@app.command()
def tree(
ctx: typer.Context,
start_path: Annotated[
Optional[list[str]],
typer.Argument(help="Path to the starting command. e.g. 'jf tree job set'"),
] = None,
show_options: Annotated[
bool,
typer.Option(
"--options",
"-o",
help="Show command options in the tree",
),
] = False,
show_docs: Annotated[
bool,
typer.Option(
"--docs",
"-D",
help="Show hidden commands",
),
] = False,
show_hidden: Annotated[
bool,
typer.Option(
"--hidden",
"-h",
help="Show hidden commands",
),
] = False,
max_depth: Annotated[
Optional[int],
typer.Option(
"--max-depth",
"-d",
help="Maximum depth of the tree to display",
),
] = None,
):
"""
Display a tree representation of the CLI command structure.
"""
# Get the top-level app
main_app = ctx.find_root().command

if start_path:
start_command = find_subcommand(main_app, start_path)
if start_command is None:
typer.echo(f"Error: Command '{' '.join(start_path)}' not found", err=True)
raise typer.Exit(code=1)
tree_title = f"[bold red]{' '.join(start_path)}[/bold red]"
else:
start_command = main_app
tree_title = "[bold red]CLI App[/bold red]"

tree = Tree(tree_title)
command_tree = get_command_tree(
start_command, tree, show_options, show_docs, show_hidden, max_depth
)

out_console.print(command_tree)
8 changes: 8 additions & 0 deletions src/jobflow_remote/cli/jfr_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typer
from typer.models import CommandFunctionType

from jobflow_remote.cli.types import tree_opt
from jobflow_remote.cli.utils import cli_error_handler


Expand All @@ -18,6 +19,9 @@ def __init__(self, *args, **kwargs) -> None:
if "rich_markup_mode" not in kwargs:
kwargs["rich_markup_mode"] = "rich"

if "callback" not in kwargs:
kwargs["callback"] = default_callback

# if "result_callback" not in kwargs:
# kwargs["result_callback"] = test_cb

Expand All @@ -44,3 +48,7 @@ def wrapper(fn):
return typer_wrapper(fn)

return wrapper


def default_callback(print_tree: tree_opt = False):
pass
8 changes: 7 additions & 1 deletion src/jobflow_remote/cli/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,9 @@ def report(
),
] = None,
):
"""
Generate a report about the Jobs in the database.
"""
jc = get_job_controller()

timezone = datetime.now(tzlocal()).tzname()
Expand All @@ -718,7 +721,7 @@ def report(


app_job_set = JFRTyper(
name="set", help="Commands for managing the jobs", no_args_is_help=True
name="set", help="Commands for setting properties for jobs", no_args_is_help=True
)
app_job.add_typer(app_job_set)

Expand Down Expand Up @@ -1162,6 +1165,9 @@ def files_get(
),
] = None,
) -> None:
"""
Retrieve files from the Job's execution folder.
"""
db_id, job_id = get_job_db_ids(job_db_id, job_index)

cm = get_config_manager()
Expand Down
18 changes: 16 additions & 2 deletions src/jobflow_remote/cli/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from jobflow_remote.cli.formatting import get_exec_config_table, get_worker_table
from jobflow_remote.cli.jf import app
from jobflow_remote.cli.jfr_typer import JFRTyper
from jobflow_remote.cli.types import force_opt, serialize_file_format_opt, verbosity_opt
from jobflow_remote.cli.types import (
force_opt,
serialize_file_format_opt,
tree_opt,
verbosity_opt,
)
from jobflow_remote.cli.utils import (
SerializeFileFormat,
check_incompatible_opt,
Expand Down Expand Up @@ -86,7 +91,10 @@ def list_projects(


@app_project.callback(invoke_without_command=True)
def current_project(ctx: typer.Context) -> None:
def current_project(
ctx: typer.Context,
print_tree: tree_opt = False, # If selected will print the tree of the CLI and exit
) -> None:
"""Print the list of the project currently selected."""
# only run if no other subcommand is executed
if ctx.invoked_subcommand is None:
Expand Down Expand Up @@ -266,6 +274,9 @@ def remove(
def list_exec_config(
verbosity: verbosity_opt = 0,
) -> None:
"""
The list of defined Execution configurations
"""
cm = get_config_manager()
project = cm.get_project()
table = get_exec_config_table(project.exec_config, verbosity)
Expand All @@ -289,6 +300,9 @@ def list_exec_config(
def list_worker(
verbosity: verbosity_opt = 0,
) -> None:
"""
The list of defined workers
"""
cm = get_config_manager()
project = cm.get_project()
table = get_worker_table(project.workers, verbosity)
Expand Down
11 changes: 11 additions & 0 deletions src/jobflow_remote/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@
SerializeFileFormat,
SortOption,
str_to_dict,
tree_callback,
)
from jobflow_remote.config.base import LogLevel
from jobflow_remote.jobs.state import FlowState, JobState

tree_opt = Annotated[
bool,
typer.Option(
"--tree",
help="Display a tree representation of the CLI command structure",
is_eager=True,
callback=tree_callback,
),
]

job_ids_indexes_opt = Annotated[
Optional[list[str]],
typer.Option(
Expand Down
73 changes: 41 additions & 32 deletions src/jobflow_remote/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm
from rich.text import Text
from rich.tree import Tree
from typer.core import TyperCommand, TyperGroup

from jobflow_remote import ConfigManager, JobController
Expand All @@ -26,8 +27,6 @@
if TYPE_CHECKING:
from cProfile import Profile

from rich.tree import Tree

from jobflow_remote.jobs.state import JobState

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -510,6 +509,7 @@ def get_command_tree(
show_hidden: bool,
max_depth: int | None,
current_depth: int = 0,
max_doc_lines: int | None = None,
) -> Tree:
"""
Recursively build a tree representation of the command structure.
Expand All @@ -530,6 +530,9 @@ def get_command_tree(
Maximum depth of the tree to display.
current_depth
Current depth in the tree (used for recursion).
max_doc_lines
If show_docs is True, the maximum number of lines showed from the
documentation of each command/option.
Returns
-------
Expand All @@ -544,8 +547,17 @@ def get_command_tree(
continue

command_str = f"[bold green]{command_name}[/bold green]"
if show_docs and isinstance(command, TyperCommand) and command.help:
if (
show_docs
and isinstance(command, (TyperCommand, TyperGroup))
and command.help
):
command_str += f": {command.help}"
if max_doc_lines:
lines = command_str.splitlines()
command_str = "\n".join(lines[:max_doc_lines])
if len(lines) > max_doc_lines:
command_str += " ..."
branch = tree.add(command_str)

if isinstance(command, TyperGroup):
Expand All @@ -557,6 +569,7 @@ def get_command_tree(
show_hidden,
max_depth,
current_depth + 1,
max_doc_lines,
)
elif show_options:
for param in command.params:
Expand All @@ -568,37 +581,33 @@ def get_command_tree(
param_str += " [red](required)[/red]"
if show_docs and param.help:
param_str += f": {param.help}"
if max_doc_lines:
lines = param_str.splitlines()
param_str = "\n".join(lines[:max_doc_lines])
if len(lines) > max_doc_lines:
param_str += " ..."
branch.add(param_str)
return tree


def find_subcommand(app: TyperGroup, path: list[str]) -> TyperGroup | None:
"""
Find a subcommand in the command tree based on the given path.
Parameters
----------
app
The root Typer app or command group.
path
List of command names forming the path to the desired subcommand.
Returns
-------
TyperGroup
The found subcommand, or None if not found.
"""
if not path:
return app

current_command = app.commands.get(path[0])
if current_command is None:
return None

if len(path) == 1:
return current_command if isinstance(current_command, TyperGroup) else None

if isinstance(current_command, TyperGroup):
return find_subcommand(current_command, path[1:])
def tree_callback(
ctx: typer.Context,
value: bool,
):
if value:
main_app = ctx.command
tree_title = f"[bold red]{ctx.command.name}[/bold red]"

tree = Tree(tree_title)
command_tree = get_command_tree(
main_app,
tree,
show_options=False,
show_docs=True,
show_hidden=False,
max_depth=None,
max_doc_lines=1,
)

return None
out_console.print(command_tree)
raise typer.Exit(0)
27 changes: 6 additions & 21 deletions tests/db/cli/test_jf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,10 @@ def test_jobs_list() -> None:
from jobflow_remote.testing.cli import run_check_cli

outputs = ["job", "set", "resources", "admin"]
excluded = ["execution", "start_date"] # hidden, option
run_check_cli(["tree"], required_out=outputs, excluded_out=excluded)
excluded = ["execution", "start_date"] # hidden, option
run_check_cli(["--tree"], required_out=outputs, excluded_out=excluded)

# max depth
outputs = ["job", "set"]
excluded = ["execution", "start_date", "resources"]
run_check_cli(["tree", "-d", "2"], required_out=outputs, excluded_out=excluded)

# hidden
outputs = ["job", "set", "resources", "execution"]
excluded = ["start_date"]
run_check_cli(["tree", "-h"], required_out=outputs, excluded_out=excluded)

# options
outputs = ["job", "set", "resources", "start_date"]
excluded = ["execution"]
run_check_cli(["tree", "-o"], required_out=outputs, excluded_out=excluded)

# start from
outputs = ["job set", "resources"]
excluded = ["execution", "start_date", "admin"]
run_check_cli(["tree", "job", "set"], required_out=outputs, excluded_out=excluded)
# test also on subcommands
outputs = ["job", "resources"]
excluded = ["─ execution", "start_date", "admin"]
run_check_cli(["job", "--tree"], required_out=outputs, excluded_out=excluded)

0 comments on commit d2d5bc7

Please sign in to comment.