A fully type-safe1, focused on developer experience, CLI framework for building "shell-like" REPL style apps! Currently only supporting Python 3.13+
Built on the idea that less is more, you can get started with 3 lines of code.
Important
This project is in early development and very much a work in progress.
Lunar Engine allows you to make a command-line app that acts like a shell. You simply define some commands, a shell prompt, and you're ready!
By default, commands will also be accepted straight from the normal command line.
See examples for more in-depth detail.
A bare minimum Lunar Engine app could look like this.
from lunar_engine.shell import Shell
from lunar_engine.command import command
@command()
def adder(a: int, b: int) -> None:
"""Adds two numbers."""
print(f"{a} + {b} = {a+b}")
Shell().run()
Commands in Lunar Engine are created via the @command
decorator. It automatically registers the command in the command registry and determines description and name based on the function's signature.
These can also be customized with parameters to the decorator.
@command()
def adder(a: int, b: int) -> None: # cmd name from function name
"""Adds two numbers.""" # description
print(f"{a} + {b} = {a+b}")
@command(register=False) # manual registration
def manual() -> None:
"""Nothing important."""
print("Hello, world!")
registry = get_registry() # global command registry
registry.register(manual) # register the command yourself
Note: command
is an alias of get_registry().command
which is the global command registry.
Lunar Engine supports keyword-only arguments, which are treated as flags (e.g., --my-flag
).
Arguments that take a value are passed using a space separator (e.g., --name Ash
). Boolean flags act as switches; their presence means True
.
from typing import Optional
@command()
def greet(*, name: str, formal: bool = False, title: Optional[str] = None):
"""
Greets a person.
"""
message = "Hello" if not formal else "Greetings"
if title:
message += f", {title}"
message += f" {name}!"
print(message)
You can run this from the interactive shell or the command line:
# Interactive shell
>> greet --name "John Doe" --formal
Greetings, John Doe!
>> greet --name Jane --title Dr.
Hello, Dr. Jane!
# From your system's command line
$ python your_app.py greet --name "John Doe" --formal
Greetings, John Doe!
The Prompt handles all user input to the app. You will likely want to use it in combination with Shell, which actually handles execution of commands.
prompt = Prompt(">> ", rprompt="Hi there!")
You can also set a custom completer on the prompt. By default, it uses CommandCompleter on the global command registry.
prompt = Prompt(">> ", rprompt="Hi there!", completer=CommandCompleter())
Note: If the prompt is used in a shell, its completer must be of type CommandCompleter.
Handling events like command errors, or unknown commands is an important part of the shell. There are some reasonable defaults, but you will likely want to customize the handlers to fit your app.
@handlers.on_unknown_command
def unknown_command(name: str) -> None:
print(f"Oops! {name} is not a command.")
@handlers.on_interrupt
def interrupt() -> None:
print("App is terminating!")
There are a few different handlers that you can hook into.
You may also create different sets of handlers which can be switched at runtime.
# "handlers" is the global set of handlers
my_handlers = HandlerRegistry()
@my_handlers.on_unknown_command
def unknown_command(name: str) -> None:
print(f"Oops! {name} is not a command.")
...
shell = Shell()
shell.handlers = my_handlers # defaults to global handlers, but can be switched at runtime
If you wish to have multiple command registries, you simply have to define them and register commands with their specific decorator.
Note: Make sure you set your registry as the completer for the prompt, otherwise you will get results from the global registry.
my_registry = CommandRegistry()
@my_registry.command(register=False)
def test() -> None:
print("Hello, world!")
# Run Shell
prompt = Prompt(">> ", completer=CommandCompleter(my_registry))
shell = Shell(my_registry)
shell.run()
## --- OR --- ##
prompt = Prompt(">> ")
shell = Shell()
shell.registry = my_registry # sets values automatically