Skip to content

Sayer and Sub-apps

This guide offers an exhaustive, richly annotated explanation of Sayer’s Sayer app system, including main applications, sub-applications, command mounting, and advanced usage.

Overview

Sayer’s CLI framework is built around the Sayer app, a container for commands, callbacks, and middleware.

Sub-apps enable modular design by nesting independent Sayer applications within a parent app, allowing reusable logic, dynamic structures, and clear separation of concerns.

Key Concepts

  • Sayer App (Sayer): Represents a root CLI application.
  • Subapp: A nested Sayer app mounted under a parent’s namespace.
  • add.sayer(): Method to mount a sub-app, including its commands and callback logic.
  • Command Resolution: Sayer resolves commands hierarchically from the parent to sub-apps.
  • Middleware Integration: Sub-apps can define their own middleware and callbacks.

Creating a Sayer App

from sayer import Sayer

app = Sayer(help="Main CLI App")

@app.command()
def greet():
    return "Hello!"

Why: The Sayer instance initializes the CLI, registering commands and global options. How: @command() registers greet as a top-level command.

Adding Sub-apps

from sayer import Sayer

sub_app = Sayer(help="Subapp CLI")

@sub-app.command()
def sub_hello():
    return "Hello from sub-app!"

app.add_sayer("sub", sub_app)

Why: This modularizes commands, encapsulating them under sub namespace. How: add_sayer("sub", sub-app) mounts sub-app's commands as sub sub_hello.

Combining Middleware and Sub-apps

from sayer.middleware import register

register("audit", before=[lambda n,a: print(f"[Audit] {n}")])

app.add_sayer("nested", sub_app, middleware=["audit"])

Why: Sub-apps can have their own middleware or callbacks. How: Middleware like audit runs before/after sub-app commands.

Controlling Execution Flow

  • Hierarchical Resolution: Commands are resolved from the parent down to sub-apps.
  • Callback Isolation: Each Sayer app can have a callback, only affecting its own commands.
  • Scoped Middleware: Middleware in sub-apps only applies to their scope.

Complex Example: Nested Apps

from sayer import Sayer

main = Sayer(help="Main App")
admin = Sayer(help="Admin Subapp")
reports = Sayer(help="Reports Subapp")

@main.command()
def home():
    return "Home command"

@admin.command()
def manage():
    return "Admin management"

@reports.command()
def summary():
    return "Report summary"

main.add_sayer("admin", admin)
admin.add_sayer("reports", reports)

Why: Supports deeply nested CLIs for complex projects. How: Commands are invoked as admin reports summary.

Dynamic Subapp Loading

from sayer import Sayer
from sayer.utils.loader import load_commands_from

reports_app = Sayer()

load_commands_from("myproject.reports.commands")
main.add_sayer("reports", reports_app)

Why: Dynamically discovers and registers commands from modules. How: load_commands_from scans modules and auto-registers commands.

Best Practices

  • ✅ Use sub-apps to modularize large CLIs.
  • ✅ Keep each sub-app self-contained with its own commands and middleware.
  • ✅ Clearly define app and sub-app help strings.
  • ✅ Isolate callbacks and global state to their app.
  • ❌ Avoid cross-app state dependencies.
  • ❌ Don’t overload the root app with excessive sub-apps; structure them logically.

Conclusion

Sayer’s app and sub-app system enables scalable, modular CLI architectures. Mastery of this system allows developers to build flexible, maintainable command-line applications.