Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A plugin for Typer command define within cmd2 #1530

KelvinChung2000 started this conversation in Show and tell
Discussion options

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"]
You must be logged in to vote

Replies: 1 comment

Comment options

@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.

You must be logged in to vote
0 replies
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /