Skip to content

Commit 79193c6

Browse files
authored
CLI Rework tbots.py (#3548)
* init cli added * cleanup * [pre-commit.ci lite] apply automatic fixes
1 parent 30bb2c7 commit 79193c6

7 files changed

Lines changed: 257 additions & 134 deletions

File tree

environment_setup/ubuntu22_requirements.txt

Lines changed: 0 additions & 14 deletions
This file was deleted.

environment_setup/ubuntu24_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ ruff==0.5.5
99
pyqt-toast-notification==1.3.2
1010
md-toc==9.0.0
1111
grpcio-tools==1.71.0
12+
typer==0.21.0

src/cli/cli_params.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from typer import Option
2+
from enum import Enum
3+
from typing import Annotated
4+
5+
from cli.multi_option import MultiOption
6+
7+
8+
class ActionArgument(str, Enum):
9+
build = "build"
10+
test = "test"
11+
run = "run"
12+
13+
14+
class DebugBinary(str, Enum):
15+
sim = "sim"
16+
blue = "blue"
17+
yellow = "yellow"
18+
19+
20+
class Platform(str, Enum):
21+
PI = "PI"
22+
NANO = "NANO"
23+
LIMITED = "LIMITED"
24+
25+
26+
PrintCommandOption: type[bool] = Annotated[
27+
bool, Option("-p", "--print_command", help="Print the generated Bazel command")
28+
]
29+
30+
NoOptimizedBuildOption: type[bool] = Annotated[
31+
bool,
32+
Option(
33+
"-no", "--no_optimized_build", help="Compile binaries without -O3 optimizations"
34+
),
35+
]
36+
37+
DebugBuildOption: type[bool] = Annotated[
38+
bool,
39+
Option(
40+
"-d",
41+
"--debug",
42+
help="Compile binaries with debug symbols",
43+
),
44+
]
45+
46+
SelectDebugBinariesOption = Annotated[
47+
list[DebugBinary] | None,
48+
MultiOption(
49+
"-ds",
50+
"--select_debug_binaries",
51+
help="Select all binaries which are running separately in debug mode",
52+
),
53+
]
54+
55+
FlashRobotsOption = Annotated[
56+
list[int] | None,
57+
MultiOption(
58+
"-f",
59+
"--flash_robots",
60+
help="A list of space separated integers representing the robot IDs "
61+
"that should be flashed by the deploy_robot_software Ansible playbook",
62+
),
63+
]
64+
65+
SSHPasswordOption = Annotated[
66+
str,
67+
Option(
68+
"-pwd", "--pwd", help="Password used by Ansible when SSHing into the robots"
69+
),
70+
]
71+
72+
InteractiveModeOption = Annotated[
73+
bool,
74+
Option(
75+
"-i",
76+
"--interactive",
77+
help="Enables interactive searching for bazel targets",
78+
),
79+
]
80+
81+
TracyOption = Annotated[
82+
bool, Option("--tracy", help="Run the binary with the TRACY_ENABLE macro defined")
83+
]
84+
85+
PlatformOption = Annotated[
86+
Platform, Option("-pl", "--platform", help="The platform to build Thunderloop for")
87+
]
88+
89+
EnableThunderscopeOption = Annotated[bool, Option("-t", "--enable_thunderscope")]
90+
EnableVisualizerOption = Annotated[bool, Option("-v", "--enable_visualizer")]
91+
StopAIOnStartOption = Annotated[bool, Option("-t", "--stop_ai_on_start")]

src/cli/multi_option.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from typing import Any
2+
import click
3+
import typer
4+
import typer.core
5+
import typer.main
6+
from typer.models import OptionInfo
7+
8+
9+
# ======= Patch to support nargs functionality in typer =======
10+
# nargs usage is considered bad practice in typer and many CLIs,
11+
# but since we likely want to be able to parse many optional args at once, this patch allows us to do so.
12+
# See https://github.com/fastapi/typer/issues/110
13+
# ==============================================================
14+
15+
16+
class OptionEatAll(typer.core.TyperOption):
17+
"""Click option that consumes arguments until next option flag."""
18+
19+
def __init__(self, *args: Any, **kwargs: Any) -> None:
20+
kwargs["multiple"] = True
21+
kwargs["is_flag"] = False
22+
super().__init__(*args, **kwargs)
23+
self._previous_parser_process = None
24+
self._eat_all_parser = None
25+
26+
def add_to_parser(self, parser: Any, ctx: click.Context) -> Any:
27+
def parser_process(value: str, state: Any) -> None:
28+
values = [value]
29+
done = False
30+
while state.rargs and not done:
31+
for prefix in self._eat_all_parser.prefixes:
32+
if state.rargs[0].startswith(prefix):
33+
done = True
34+
break
35+
if not done:
36+
values.append(state.rargs.pop(0))
37+
for val in values:
38+
self._previous_parser_process(val, state)
39+
40+
retval = super().add_to_parser(parser, ctx)
41+
for name in self.opts:
42+
our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
43+
if our_parser:
44+
self._eat_all_parser = our_parser
45+
self._previous_parser_process = our_parser.process
46+
our_parser.process = parser_process
47+
break
48+
return retval
49+
50+
51+
class MultiOptionInfo(OptionInfo):
52+
"""Marker for multi-argument options."""
53+
54+
is_multi_option: bool = True
55+
56+
def __init__(self, option_info: OptionInfo) -> None:
57+
super().__init__(**option_info.__dict__)
58+
59+
60+
def MultiOption(
61+
default: Any = ..., *param_decls: str, **kwargs: Any
62+
) -> MultiOptionInfo:
63+
"""Create option that accepts multiple values."""
64+
return MultiOptionInfo(typer.Option(default, *param_decls, **kwargs))
65+
66+
67+
# Monkey-patch to detect MultiOptionInfo and use OptionEatAll
68+
_original_get_click_param = typer.main.get_click_param
69+
70+
71+
def _patched_get_click_param(param: typer.models.ParamMeta):
72+
click_param, converter = _original_get_click_param(param)
73+
if isinstance(param.default, MultiOptionInfo):
74+
option_eat_all = object.__new__(OptionEatAll)
75+
option_eat_all.__dict__.update(click_param.__dict__)
76+
option_eat_all._previous_parser_process = None
77+
option_eat_all._eat_all_parser = None
78+
return (option_eat_all, converter)
79+
return (click_param, converter)
80+
81+
82+
typer.main.get_click_param = _patched_get_click_param

src/software/embedded/robot_diagnostics_cli/BUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ load("@robot_diagnostics_cli_deps//:requirements.bzl", "requirement")
33
load("@rules_pkg//:pkg.bzl", "pkg_tar")
44
load("@rules_python//python:defs.bzl", "py_binary")
55
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
6-
load("//:defs.bzl", "pkg_executable")
6+
load("//:starlark/defs.bzl", "pkg_executable")
77

88
package(default_visibility = ["//visibility:public"])
99

src/defs.bzl renamed to src/starlark/defs.bzl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
load("@rules_pkg//pkg:providers.bzl", "PackageFilegroupInfo", "PackageFilesInfo", "PackageSymlinkInfo")
22

3-
# No idea how this works, refer to:
4-
# https://gist.github.com/pauldraper/7bc811ffbef6d3f3d4a4bb01afa9808f
3+
# ===================================================================
4+
# Custom Build Rules and Definitions
5+
# ===================================================================
56

6-
# For maintaining Bazel runfile tree structure during packaging
7+
# For maintaining Bazel runfile tree structure during packaging. Used in the onboard python CLI
8+
# See for reference https://gist.github.com/pauldraper/7bc811ffbef6d3f3d4a4bb01afa9808f
79
def _runfile_path(workspace_name, file):
810
path = file.short_path
911
return path[len("../"):] if path.startswith("../") else "%s/%s" % (workspace_name, path)

0 commit comments

Comments
 (0)