Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 44b824b

Browse files
add methods_description and extend decorators
to allow driver-provided help text for exported methods. Map od methods and their descriptions is added to GetReport to save on RPC calls.
1 parent d02013a commit 44b824b

File tree

4 files changed

+102
-16
lines changed

4 files changed

+102
-16
lines changed

packages/jumpstarter/jumpstarter/client/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ class DriverClient(AsyncDriverClient):
3636
description: str | None = None
3737
"""Driver description from GetReport(), used for CLI help text"""
3838

39+
methods_description: dict[str, str] = field(default_factory=dict)
40+
"""Map of method names to their help descriptions from GetReport()"""
41+
3942
def call(self, method, *args):
4043
"""
4144
Invoke driver call

packages/jumpstarter/jumpstarter/client/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ async def client_from_channel(
6060
stack=stack.enter_context(ExitStack()),
6161
children={reports[k].labels["jumpstarter.dev/name"]: clients[k] for k in topo[index]},
6262
description=getattr(report, 'description', None) or None,
63+
methods_description=getattr(report, 'methods_description', {}) or {},
6364
)
6465

6566
clients[index] = client

packages/jumpstarter/jumpstarter/driver/base.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from .decorators import (
2525
MARKER_DRIVERCALL,
26+
MARKER_HELP,
2627
MARKER_MAGIC,
2728
MARKER_STREAMCALL,
2829
MARKER_STREAMING_DRIVERCALL,
@@ -75,6 +76,9 @@ class Driver(
7576
description: str | None = None
7677
"""Custom description for the driver (shown in CLI help)"""
7778

79+
methods_description: dict[str, str] = field(default_factory=dict, init=False)
80+
"""Map of method names to their help descriptions"""
81+
7882
log_level: str = "INFO"
7983
logger: logging.Logger = field(init=False)
8084

@@ -85,6 +89,23 @@ def __post_init__(self):
8589
self.logger = logging.getLogger(self.__class__.__name__)
8690
self.logger.setLevel(self.log_level)
8791

92+
# Collect help texts from decorated methods
93+
self._collect_methods_description()
94+
95+
def _collect_methods_description(self):
96+
"""Collect help texts from @export and @exportstream decorated methods"""
97+
for name in dir(self):
98+
if name.startswith('_'):
99+
continue
100+
101+
try:
102+
method = getattr(self, name)
103+
if callable(method) and hasattr(method, MARKER_HELP):
104+
help_text = getattr(method, MARKER_HELP)
105+
self.methods_description[name] = help_text
106+
except Exception:
107+
continue
108+
88109
def close(self):
89110
for child in self.children.values():
90111
child.close()
@@ -210,6 +231,7 @@ def report(self, *, root=None, parent=None, name=None):
210231
| ({"jumpstarter.dev/client": self.client()})
211232
| ({"jumpstarter.dev/name": name} if name else {}),
212233
description=self.description or None,
234+
methods_description=self.methods_description or {},
213235
)
214236

215237
def enumerate(self, *, root=None, parent=None, name=None):
Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,88 @@
11
from inspect import isasyncgenfunction, iscoroutinefunction, isfunction, isgeneratorfunction
2-
from typing import Final
2+
from typing import Any, Callable, Final
3+
4+
import click
35

46
MARKER_MAGIC: Final[str] = "07c9b9cc"
57
MARKER_DRIVERCALL: Final[str] = "marker_drivercall"
68
MARKER_STREAMCALL: Final[str] = "marker_streamcall"
79
MARKER_STREAMING_DRIVERCALL: Final[str] = "marker_streamingdrivercall"
10+
MARKER_HELP: Final[str] = "marker_help"
811

912

10-
def export(func):
11-
"""
12-
Decorator for exporting method as driver call
13-
"""
14-
if isasyncgenfunction(func) or isgeneratorfunction(func):
15-
setattr(func, MARKER_STREAMING_DRIVERCALL, MARKER_MAGIC)
16-
elif iscoroutinefunction(func) or isfunction(func):
17-
setattr(func, MARKER_DRIVERCALL, MARKER_MAGIC)
18-
else:
19-
raise ValueError(f"unsupported exported function {func}")
20-
return func
13+
def export(func: Callable | None = None, *, help: str | None = None) -> Callable:
14+
"""Decorator for exporting method as driver call"""
15+
def decorator(f: Callable) -> Callable:
16+
if isasyncgenfunction(f) or isgeneratorfunction(f):
17+
setattr(f, MARKER_STREAMING_DRIVERCALL, MARKER_MAGIC)
18+
elif iscoroutinefunction(f) or isfunction(f):
19+
setattr(f, MARKER_DRIVERCALL, MARKER_MAGIC)
20+
else:
21+
raise ValueError(f"unsupported exported function {f}")
22+
if help is not None:
23+
setattr(f, MARKER_HELP, help)
24+
return f
25+
return decorator(func) if func else decorator
26+
2127

28+
def exportstream(func: Callable | None = None, *, help: str | None = None) -> Callable:
29+
"""Decorator for exporting method as stream"""
30+
def decorator(f: Callable) -> Callable:
31+
setattr(f, MARKER_STREAMCALL, MARKER_MAGIC)
32+
if help is not None:
33+
setattr(f, MARKER_HELP, help)
34+
return f
35+
return decorator(func) if func else decorator
2236

23-
def exportstream(func):
37+
38+
class DriverClickGroup(click.Group):
2439
"""
25-
Decorator for exporting method as stream
40+
Custom Click Group to use methods_description for command help.
41+
42+
Usage:
43+
def cli(self):
44+
base = DriverClickGroup(self, help="Default driver description")
45+
46+
@base.command() # Automatically uses string from methods_description['on']
47+
def on():
48+
self.on()
49+
50+
return base
2651
"""
27-
setattr(func, MARKER_STREAMCALL, MARKER_MAGIC)
28-
return func
52+
53+
def __init__(self, client: Any, *args: Any, **kwargs: Any) -> None:
54+
"""
55+
Initialize a DriverClickGroup bound to a client.
56+
57+
:param client: DriverClient instance (provides methods_description)
58+
:param args: Arguments passed to click.Group
59+
:param kwargs: Keyword arguments passed to click.Group
60+
"""
61+
super().__init__(*args, **kwargs)
62+
self.client = client
63+
64+
def command(self, *args: Any, **kwargs: Any) -> Callable:
65+
"""
66+
Decorator to register a client-side command with automatic help text.
67+
68+
Priority order for help text:
69+
1. methods_description from server (highest priority)
70+
2. help= parameter explicitly provided in @base.command(help="...")
71+
3. Empty string (fallback)
72+
"""
73+
def decorator(f: Callable) -> click.Command:
74+
# Determine command name (from kwargs or function name)
75+
name = kwargs.get('name') or f.__name__
76+
77+
# Priority order for help text:
78+
# 1. methods_description from server
79+
# 2. help= parameter explicitly provided by client
80+
# 3. Empty string
81+
if name in self.client.methods_description:
82+
kwargs['help'] = self.client.methods_description[name]
83+
elif 'help' not in kwargs:
84+
kwargs['help'] = ''
85+
86+
return super(DriverClickGroup, self).command(*args, **kwargs)(f)
87+
88+
return decorator

0 commit comments

Comments
 (0)