1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
.. _Meta App:
========
Meta App
========
What if you want more control over the application launch process?
Cyclopts provides the option of launching an app from an app; a meta app!
------------
Meta Sub App
------------
Typically, a Cyclopts application is launched by calling the :class:`.App` object:
.. code-block:: python
from cyclopts import App
app = App()
# Register some commands here (not shown)
app() # Run the app
To change how the primary app is run, you can use the meta-app feature of Cyclopts.
The meta app is just like a normal Cyclopts :class:`.App`, the only thing special about
it is that it's help-page gets merged in with it's parenting app.
.. code-block:: python
from cyclopts import App, Group, Parameter
from typing import Annotated
app = App()
# Rename the meta's "Parameter" -> "Session Parameters".
# Set sort_key so it will be drawn higher up the help-page.
app.meta.group_parameters = Group("Session Parameters", sort_key=0)
@app.command
def foo(loops: int):
for i in range(loops):
print(f"Looping! {i}")
@app.meta.default
def my_app_launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
print(f"Hello {user}")
app(tokens)
app.meta()
.. code-block:: console
$ my-script --user=Bob foo 3
Hello Bob
Looping! 0
Looping! 1
Looping! 2
The variable positional ``*tokens`` will aggregate all remaining tokens, including those starting with a hyphen (typically options).
We can then pass them along to the primary ``app``.
The ``meta`` app is mostly a normal Cyclopts app; the only thing special about it is that it will
be additionally scanned when generate help screens
``*tokens`` is annotated with ``show=False`` since we do not want this variable to show up in the help screen.
.. code-block:: console
$ my-script --help
Usage: my-script COMMAND
╭─ Session Parameters ────────────────────────────────────────────────────╮
│ * --user [required] │
╰─────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────╮
│ foo │
│ --help,-h Display this message and exit. │
│ --version Display application version. │
╰─────────────────────────────────────────────────────────────────────────╯
-------------
Meta Commands
-------------
If you want a command to circumvent ``my_app_launcher``, add it as you would any other command to the meta app.
.. code-block:: python
@app.meta.command
def info():
print("CLI didn't have to provide --user to call this.")
.. code-block:: console
$ my-script info
CLI didn't have to provide --user to call this.
$ my-script --help
Usage: my-script COMMAND
╭─ Session Parameters ────────────────────────────────────────────────────╮
│ * --user [required] │
╰─────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────╮
│ foo │
│ info │
│ --help,-h Display this message and exit. │
│ --version Display application version. │
╰─────────────────────────────────────────────────────────────────────────╯
Just like a standard application, the parsed ``command`` executes instead of ``default``.
-------------------------
Custom Command Invocation
-------------------------
The core logic of :meth:`App.__call__` method is the following:
.. code-block:: python
def __call__(self, tokens=None, **kwargs):
command, bound, ignored = self.parse_args(tokens, **kwargs)
return command(*bound.args, **bound.kwargs)
Knowing this, we can easily customize how we actually invoke actions with Cyclopts.
Let's imagine that we want to instantiate an object, ``User`` in our meta app, and pass it to subsequent commands that need it.
This might be useful to share an expensive-to-create object amongst commands in a single session; see :ref:`Command Chaining`.
.. code-block:: python
from cyclopts import App, Parameter
from typing import Annotated
app = App()
class User:
def __init__(self, name):
self.name = name
@app.command
def create(
age: int,
*,
user_obj: Annotated[User, Parameter(parse=False)],
):
print(f"Creating user {user_obj.name} with age {age}.")
@app.meta.default
def launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
additional_kwargs = {}
command, bound, ignored = app.parse_args(tokens)
# "ignored" is a dict mapping python-variable-name to it's type annotation for parameters with "parse=False".
if "user_obj" in ignored:
# 'ignored["user_obj"]' is the class "User"
additional_kwargs["user_obj"] = ignored["user_obj"](user)
return command(*bound.args, **bound.kwargs, **additional_kwargs)
if __name__ == "__main__":
app.meta()
.. code-block:: console
$ my-script create --user Alice 30
Creating user Alice with age 30.
The ``parse=False`` configuration tells Cyclopts to not try and bind arguments to this parameter.
Cyclopts will pass it along to ``ignored`` to make custom meta-app logic easier.
The annotated parameter **must** be a keyword-only parameter.
|