-
Notifications
You must be signed in to change notification settings - Fork 128
A plugin for Typer command define within cmd2 #1530
-
I have written a plugin that lets you define all your commands like a Typer command. For example:
@with_category(CMD_FABRIC_FLOW) def do_gen_geometry( self, padding: Annotated[ int, typer.Argument( min=4, max=32, metavar="[4-32]", help="Padding value for geometry generation", ), ] = 8, ) -> None: pass @with_category(CMD_FABRIC_FLOW) def do_gen_tile( self, tiles: Annotated[ list[str], CompleterSpec(completer=complete_tile_names), typer.Argument(..., metavar="TILE...", help="Tiles to generate"), ], ) -> None: pass
I have added an extra construct, CompleterSpec, which allows you to add a completer for the command. This provides a way to add completer. I have only implemented it such that it now only works with single argument with one completer, as this is good enough for me for now. But likely can extend it to have completer for per argument command entry. I have no plan to maintain or extend it right now, but still a nice little thing to share.
The code
from __future__ import annotations import inspect from collections.abc import Callable from typing import Any, cast import click import typer from cmd2 import Cmd from cmd2.parsing import Statement from loguru import logger from FABulous.custom_exception import CommandError class CompleterSpec: """Specification for tab completion behavior. Used to declare completion functions that the plugin will wire up to both Click's autocompletion and cmd2's tab completion. """ def __init__(self, completer: Callable[[Cmd, str, str, int], list[str]]): """Initialize completion specification. Args: completer: Function that takes (app, text, line, begidx) and returns list of completion strings. Compatible with cmd2 completer signature. """ self.completer = completer class Cmd2TyperPlugin(Cmd): """Optional mixin supplying default Typer integration attributes. Inherit alongside your ``cmd2.Cmd`` subclass if you want convenient defaults, or simply define the same attributes on your class directly. """ typer_auto_enable: bool = True typer_skip_commands: set[str] = set() typer_command_kwargs: dict[str, dict[str, Any]] = {} typer_command_builder: ( Callable[[Callable[..., Any], str, dict[str, Any]], click.Command] | None ) = None __inner_app: typer.Typer standard_cmd2_commands = { "do_exit", "do_quit", "do_q", "do_help", "do_history", "do_edit", "do_shell", "do_alias", "do_unalias", "do_shortcuts", "do_macro", "do_run_pyscript", "do_run_script", "do_set", "do_settable", } def _extract_completion_spec( self, func: Callable[..., Any] ) -> CompleterSpec | None: """Extract CompletionSpec from function annotations if present.""" # Extract completion specification if present completion_spec = None sig = inspect.signature(func) for param in sig.parameters.values(): if param.name == "self": continue # Check if annotation contains CompletionSpec ann = param.annotation if ann is inspect.Signature.empty: continue # Handle Annotated types origin = getattr(ann, "__origin__", None) if origin is not None: # For Annotated[type, metadata...], check metadata metadata = getattr(ann, "__metadata__", ()) for item in metadata: if isinstance(item, CompleterSpec): completion_spec = item return completion_spec def cmd_regiseter(self) -> None: skip = set() # Always skip standard cmd2 commands regardless of where they're defined skip.update(self.standard_cmd2_commands) for attr in dir(self): if not attr.startswith("do_"): continue if attr in skip or attr[3:] in skip: continue func = getattr(type(self), attr, None) if func is None or not inspect.isfunction(func): continue if func.__module__.startswith("cmd2."): continue cmd_name = attr[3:] bound_cb = cast("Callable[..., Any]", func.__get__(self)) completer_spec = self._extract_completion_spec(func) self.__inner_app.command(name=cmd_name)(bound_cb) if completer_spec is None: continue # bind completer_spec into function default to capture current loop value def completer( text: str, line: str, begidx: int, endidx: int, _completer_spec: CompleterSpec = completer_spec, ) -> list[str]: try: # Call the user-provided completion function return _completer_spec.completer(self, text, line, begidx) except (AttributeError, TypeError, ValueError): return [] setattr(self, f"complete_{cmd_name}", completer) def onecmd( self, statement: Statement | str, *, add_to_history: bool = True ) -> bool: cmds = typer.main.get_command(self.__inner_app) if isinstance(cmds, click.Group) and isinstance(statement, Statement): if statement.command not in cmds.commands: return super().onecmd(statement, add_to_history=add_to_history) return cmds.commands[statement.command].main( args=list(statement.arg_list), prog_name=statement.command, standalone_mode=False, obj=self.__inner_app, ) raise CommandError("Invalid command or statement") def __init__(self, *args: object, **kwargs: object) -> None: # pragma: no cover super().__init__(*args, **kwargs) logger.debug("cmd2_typer plugin_start for {}", type(self).__name__) self.__inner_app = typer.Typer(add_completion=False) self.cmd_regiseter() __all__ = ["plugin_start", "Cmd2TyperPlugin", "CompleterSpec", "reregister_completers"]
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 1 comment
-
@KelvinChung2000 Thanks for the cool example. Feel free to submit a PR if you would like to add it under the examples/ directory. Or feel free to publish it to PyPI as a separate module and we can add a link to the documentation.
Beta Was this translation helpful? Give feedback.