File: groups.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 (278 lines) | stat: -rw-r--r-- 15,403 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
.. _Groups:

======
Groups
======
Groups offer a way of organizing parameters and commands on the help-page; for example:

.. code-block:: console

   Usage: my-script.py create [OPTIONS]

   ╭─ Vehicle (choose one) ───────────────────────────────────────────────────────╮
   │ --car    [default: False]                                                    │
   │ --truck  [default: False]                                                    │
   ╰──────────────────────────────────────────────────────────────────────────────╯
   ╭─ Engine ─────────────────────────────────────────────────────────────────────╮
   │ --hp         [default: 200]                                                  │
   │ --cylinders  [default: 6]                                                    │
   ╰──────────────────────────────────────────────────────────────────────────────╯
   ╭─ Wheels ─────────────────────────────────────────────────────────────────────╮
   │ --wheel-diameter  [default: 18]                                              │
   │ --rims,--no-rims  [default: False]                                           │
   ╰──────────────────────────────────────────────────────────────────────────────╯

They also provide an additional abstraction layer that :ref:`validators <API Validators>` can operate on.

Groups can be created in two ways:

1. Explicitly creating a :class:`.Group` object.

2. Implicitly with a **string**.
   This will implicitly create a group, ``Group(my_str_group_name)``, if it doesn't exist.
   If there exists a :class:`.Group` object with the same name within the command/parameter context, it will join that group.

   .. warning::
      While convenient and terse, mistyping a group name as a string will unintentionally create a new group!

Every command and parameter belongs to at least one group.

Group(s) can be provided to the ``group`` keyword argument of :meth:`app.command <cyclopts.App.command>` and :class:`.Parameter`.
Like :class:`.Parameter`, the :class:`.Group` class itself only marks objects with metadata; the group does **not** contain direct references to it's members.
This means that groups can be re-used across commands.

--------------
Command Groups
--------------
An example of using groups to organize commands:

.. code-block:: python

   from cyclopts import App

   app = App()

   # Change the group of "--help" and "--version" to the implicitly created "Admin" group.
   app["--help"].group = "Admin"
   app["--version"].group = "Admin"

   @app.command(group="Admin")
   def info():
       """Print debugging system information."""
       print("Displaying system info.")

   @app.command
   def download(path, url):
       """Download a file."""
       print(f"Downloading {url} to {path}.")

   @app.command
   def upload(path, url):
       """Upload a file."""
       print(f"Downloading {url} to {path}.")

   app()

.. code-block:: console

   $ python my-script.py --help
   Usage: my-script.py COMMAND

   ╭─ Admin ──────────────────────────────────────────────────────────────────────╮
   │ info       Print debugging system information.                               │
   │ --help,-h  Display this message and exit.                                    │
   │ --version  Display application version.                                      │
   ╰──────────────────────────────────────────────────────────────────────────────╯
   ╭─ Commands ───────────────────────────────────────────────────────────────────╮
   │ download  Download a file.                                                   │
   │ upload    Upload a file.                                                     │
   ╰──────────────────────────────────────────────────────────────────────────────╯

The default group is defined by the registering app's :attr:`.App.group_commands`, which defaults to a group named ``"Commands"``.

.. _Parameter Groups:

----------------
Parameter Groups
----------------
Like commands above, parameter groups allow us to organize parameters on the help page.
They also allow us to add additional inter-parameter validators (e.g. mutually-exclusive parameters).
An example of using groups with parameters:

.. code-block:: python

   from cyclopts import App, Group, Parameter, validators
   from typing import Annotated

   app = App()

   vehicle_type_group = Group(
       "Vehicle (choose one)",
       default_parameter=Parameter(negative=""),  # Disable "--no-" flags
       validator=validators.MutuallyExclusive(),  # Only one option is allowed to be selected.
   )

   @app.command
   def create(
       *,  # force all subsequent variables to be keyword-only
       # Using an explicitly created group object.
       car: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
       truck: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
       # Implicitly creating an "Engine" group.
       hp: Annotated[float, Parameter(group="Engine")] = 200,
       cylinders: Annotated[int, Parameter(group="Engine")] = 6,
       # You can explicitly create groups in-line.
       wheel_diameter: Annotated[float, Parameter(group=Group("Wheels"))] = 18,
       # Groups within the function signature can always be referenced with a string.
       rims: Annotated[bool, Parameter(group="Wheels")] = False,
   ):
       pass

   app()

.. code-block:: console

   $ python my-script.py create --help
   Usage: my-script.py create [OPTIONS]

   ╭─ Engine ──────────────────────────────────────────────────────╮
   │ --hp         [default: 200]                                   │
   │ --cylinders  [default: 6]                                     │
   ╰───────────────────────────────────────────────────────────────╯
   ╭─ Vehicle (choose one) ────────────────────────────────────────╮
   │ --car    [default: False]                                     │
   │ --truck  [default: False]                                     │
   ╰───────────────────────────────────────────────────────────────╯
   ╭─ Wheels ──────────────────────────────────────────────────────╮
   │ --wheel-diameter  [default: 18]                               │
   │ --rims --no-rims  [default: False]                            │
   ╰───────────────────────────────────────────────────────────────╯

   $ python my-script.py create --car --truck
   ╭─ Error ───────────────────────────────────────────────────────╮
   │ Invalid values for group "Vehicle (choose one)". Mutually     │
   │ exclusive arguments: {--car, --truck}                         │
   ╰───────────────────────────────────────────────────────────────╯

In this example, we use the :class:`~.validators.MutuallyExclusive` validator to make it so the user can only specify ``--car`` or ``--truck``.

The default groups are defined by the registering app:

* :attr:`.App.group_arguments` for positional-only arguments, which defaults to a group named ``"Arguments"``.

* :attr:`.App.group_parameters` for all other parameters, which defaults to a group named ``"Parameters"``.

----------
Validators
----------
Group validators offer a way of jointly validating group parameter members of CLI-provided values.
Groups with an empty name, or with ``show=False``, are a way of using group validators without impacting the help-page.

.. code-block:: python

   from cyclopts import App, Group, Parameter, validators
   from typing import Annotated

   app = App()

   mutually_exclusive = Group(
      # This Group has no name, so it won't impact the help page.
      validator=validators.MutuallyExclusive(),
      # show_default=False - Showing "[default: False]" isn't too meaningful for mutually-exclusive options.
      # negative="" - Don't create a "--no-" flag
      default_parameter=Parameter(show_default=False, negative=""),
   )

   @app.command
   def foo(
       car: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
       truck: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
   ):
       print(f"{car=} {truck=}")

   app()

.. code-block:: console

   $ python demo.py foo --help
   Usage: demo.py foo [ARGS] [OPTIONS]

   ╭─ Parameters ──────────────────────────────────────────────────────╮
   │ CAR,--car                                                         │
   │ TRUCK,--truck                                                     │
   ╰───────────────────────────────────────────────────────────────────╯

   $ python demo.py foo --car
   car=True truck=False

   $ python demo.py foo --truck
   car=False truck=True

   $ python demo.py foo --car --truck
   ╭─ Error ───────────────────────────────────────────────────────────╮
   │  Mutually exclusive arguments: {--car, --truck}                   │
   ╰───────────────────────────────────────────────────────────────────╯

See :attr:`.Group.validator` for details.

Cyclopts has some :ref:`builtin group-validators for common use-cases.<Group Validators>`

---------
Help Page
---------
Groups form titled panels on the help-page.

Groups with an empty name, or with :attr:`show=False <.Group.show>`, are **not** shown on the help-page.
This is useful for applying additional grouping logic (such as applying a :class:`.LimitedChoice` validator) without impacting the help-page.

By default, the ordering of panels is **alphabetical**.
However, the sorting can be manipulated by :attr:`.Group.sort_key`. See it's documentation for usage.

The :meth:`.Group.create_ordered` convenience classmethod creates a :class:`.Group` with a :attr:`~.Group.sort_key` value drawn drawn from a global monotonically increasing counter.
This means that the order in the help-page will match the order that the groups were instantiated.

.. code-block:: python

   from cyclopts import App, Group

   app = App()

   plants = Group.create_ordered("Plants")
   animals = Group.create_ordered("Animals")
   fungi = Group.create_ordered("Fungi")

   @app.command(group=animals)
   def zebra():
       pass

   @app.command(group=plants)
   def daisy():
       pass

   @app.command(group=fungi)
   def portobello():
       pass

   app()

.. code-block:: console

   $ my-script --help

   Usage: scratch.py COMMAND

   ╭─ Plants ───────────────────────────────────────────────────────────╮
   │ daisy                                                              │
   ╰────────────────────────────────────────────────────────────────────╯
   ╭─ Animals ──────────────────────────────────────────────────────────╮
   │ zebra                                                              │
   ╰────────────────────────────────────────────────────────────────────╯
   ╭─ Fungi ────────────────────────────────────────────────────────────╮
   │ portobello                                                         │
   ╰────────────────────────────────────────────────────────────────────╯
   ╭─ Commands ─────────────────────────────────────────────────────────╮
   │ --help -h  Display this message and exit.                          │
   │ --version  Display application version.                            │
   ╰────────────────────────────────────────────────────────────────────╯

Even when using :meth:`.Group.create_ordered`, a :attr:`~.Group.sort_key` can still be supplied; the global counter will only be used to break sorting ties.