File: meta_app.rst

package info (click to toggle)
python-cyclopts 3.12.0-3
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 3,288 kB
  • sloc: python: 11,445; makefile: 24
file content (162 lines) | stat: -rw-r--r-- 6,911 bytes parent folder | download | duplicates (2)
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.