File: advanced.rst

package info (click to toggle)
python-questionary 2.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 960 kB
  • sloc: python: 3,917; makefile: 66
file content (362 lines) | stat: -rw-r--r-- 10,802 bytes parent folder | download
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
*****************
Advanced Concepts
*****************

This page describes some of the more advanced uses of Questionary.

Validation
##########

Many of the prompts support a ``validate`` argument, which allows
the answer to be validated before being submitted. A user can not
submit an answer if it doesn't pass the validation.

The example below shows :meth:`~questionary.text` input with
a validation:

.. code-block:: python3

  import questionary
  from questionary import Validator, ValidationError, prompt

  class NameValidator(Validator):
      def validate(self, document):
          if len(document.text) == 0:
              raise ValidationError(
                  message="Please enter a value",
                  cursor_position=len(document.text),
              )

  questionary.text("What's your name?", validate=NameValidator).ask()

In this example, the user can not enter a non empty value. If the
prompt is submitted without a value. Questionary will show the error
message and reject the submission until the user enters a value.

Alternatively, we can replace the ``NameValidator`` class with a simple
function, as seen below:

.. code-block:: python3

  import questionary

  print(questionary.text(
    "What's your name?",
    validate=lambda text: True if len(text) > 0 else "Please enter a value"
  ).ask())

Finally, if we do not care about the error message being displayed, we can omit
the error message from the final example to use the default:

.. code-block:: python3

  import questionary

  print(questionary.text("What's your name?", validate=lambda text: len(text) > 0).ask())

.. admonition:: example
  :class: info

  The :meth:`~questionary.checkbox` prompt does not support passing a
  ``Validator``. See the :ref:`API Reference <api-reference>` for all the prompts which
  support the ``validate`` parameter.

A Validation Example using the Password Question
************************************************

Here we see an example of ``validate`` being used on a
:meth:`~questionary.password` prompt to enforce complexity requirements:

.. code-block:: python3

  import re
  import questionary

  def password_validator(password):

      if len(password) < 10:
          return "Password must be at least 10 characters"

      elif re.search("[0-9]", password) is None:
          return "Password must contain a number"

      elif re.search("[a-z]", password) is None:
          return "Password must contain an lower-case letter"

      elif re.search("[A-Z]", password) is None:
          return "Password must contain an upper-case letter"

      else:
          return True

  print(questionary.password("Enter your password", validate=password_validator).ask())

Keyboard Interrupts
###################

Prompts can be invoked in either a 'safe' or 'unsafe' way. The safe way
captures keyboard interrupts and handles them by catching the interrupt
and returning ``None`` for the asked question. If a question is asked
using unsafe functions, the keyboard interrupts are not caught.

Safe
****

The following are safe (capture keyboard interrupts):

* :meth:`~questionary.prompt`;

* :attr:`~questionary.Form.ask` on :class:`~questionary.Form` (returned by
  :meth:`~questionary.form`);

* :attr:`~questionary.Question.ask` on :class:`~questionary.Question`, which
  is returned by the various prompt functions (e.g. :meth:`~questionary.text`,
  :meth:`~questionary.checkbox`).

When a keyboard interrupt is captured, the message ``"Cancelled by user"`` is
displayed (or a custom message, if one is given) and ``None`` is returned.
Here is an example:

.. code:: python3

    # Questionary handles keyboard interrupt and returns `None` if the
    # user hits e.g. `Ctrl+C`
    prompt(...)

Unsafe
******

The following are unsafe (do not catch keyboard interrupts):

* :meth:`~questionary.unsafe_prompt`;

* :attr:`~questionary.Form.unsafe_ask` on :class:`~questionary.Form` (returned by
  :meth:`~questionary.form`);

* :attr:`~questionary.Question.unsafe_ask` on :class:`~questionary.Question`,
  which is returned by the various prompt functions (e.g. :meth:`~questionary.text`,
  :meth:`~questionary.checkbox`).
  
As a caller you must handle keyboard interrupts yourself
when calling these methods. Here is an example:

.. code:: python3

    try:
        unsafe_prompt(...)

    except KeyboardInterrupt:
        # your chance to handle the keyboard interrupt
        print("Cancelled by user")


Asynchronous Usage
##################

If you are running asynchronous code and you want to avoid blocking your
async loop, you can ask your questions using ``await``.
:class:`questionary.Question` and :class:`questionary.Form` have
``ask_async`` and ``unsafe_ask_async`` methods to invoke the
question using :mod:`python:asyncio`:

.. code-block:: python3

  import questionary

  answer = await questionary.text("What's your name?").ask_async()

Themes & Styling
################

You can customize all the colors used for the prompts. Every part of the prompt
has an identifier, which you can use to style it. Let's create your own custom
style:

.. code-block:: python3

  from questionary import Style

  custom_style_fancy = Style([
      ('qmark', 'fg:#673ab7 bold'),       # token in front of the question
      ('question', 'bold'),               # question text
      ('answer', 'fg:#f44336 bold'),      # submitted answer text behind the question
      ('pointer', 'fg:#673ab7 bold'),     # pointer used in select and checkbox prompts
      ('highlighted', 'fg:#673ab7 bold'), # pointed-at choice in select and checkbox prompts
      ('selected', 'fg:#cc5454'),         # style for a selected item of a checkbox
      ('separator', 'fg:#cc5454'),        # separator in lists
      ('instruction', ''),                # user instructions for select, rawselect, checkbox
      ('text', ''),                       # plain text
      ('disabled', 'fg:#858585 italic')   # disabled choices for select and checkbox prompts
  ])

To use the custom style, you need to pass it to the question as a parameter:

.. code-block:: python3

  questionary.text("What's your phone number", style=custom_style_fancy).ask()

.. note::

  Default values will be used for any token types not specified in your custom style.

Styling Choices in Select & Checkbox Questions
**********************************************

It is also possible to use a list of token tuples as a ``Choice`` title to
change how an option is displayed in :class:`questionary.select` and
:class:`questionary.checkbox`. Make sure to define any additional styles
as part of your custom style definition.

.. code-block:: python3

  import questionary
  from questionary import Choice, Style

  custom_style_fancy = questionary.Style([
      ("highlighted", "bold"),  # style for a token which should appear highlighted
  ])

  choices = [Choice(title=[("class:text", "order a "),
                           ("class:highlighted", "big pizza")])]

  questionary.select(
     "What do you want to do?",
     choices=choices,
     style=custom_style_fancy).ask()

Conditionally Skip Questions
############################

Sometimes it is helpful to be able to skip a question based on a condition.
To avoid the need for an ``if`` around the question, you can pass the
condition when you create the question:

.. code-block:: python3

  import questionary

  DISABLED = True
  response = questionary.confirm("Are you amazed?").skip_if(DISABLED, default=True).ask()

If the condition (in this case ``DISABLED``) is ``True``, the question
will be skipped and the default value gets returned, otherwise the user will
be prompted as usual and the default value will be ignored.

.. _question_dictionaries:

Create Questions from Dictionaries
##################################

Instead of creating questions using the Python functions, you can also create
them using a configuration dictionary:

.. code-block:: python3

  from questionary import prompt

  questions = [
      {
          'type': 'text',
          'name': 'phone',
          'message': "What's your phone number",
      },
      {
          'type': 'confirm',
          'message': 'Do you want to continue?',
          'name': 'continue',
          'default': True,
      }
  ]

  answers = prompt(questions)

The questions will be prompted one after another and ``prompt`` will return
as soon as all of them are answered. The returned ``answers``
will be a dictionary containing the responses, e.g.

.. code-block:: python3

  {"phone": "0123123", "continue": False}.

Each configuration dictionary for a question must contain the
following keys:

``type`` (required)
   The type of the question.

``name`` (required)
  The name of the question (will be used as key in the
  ``answers`` dictionary).

``message`` (required)
  Message that will be shown to the user.

In addition to these required configuration parameters, you can
add the following optional parameters:

``qmark`` (optional)
  Question mark to use - defaults to ``?``.

``default`` (optional)
  Preselected value.

``choices`` (optional)
  List of choices (applies when ``'type': 'select'``)
  or function returning a list of choices.

``when`` (optional)
  Function checking if this question should be shown
  or skipped (same functionality as :attr:`~questionary.Question.skip_if`).

``validate`` (optional)
  Function or Validator Class performing validation (will
  be performed in real time as users type).

``filter`` (optional)
  Receive the user input and return the filtered value to be
  used inside the program. 

Further information can be found at the :class:`questionary.prompt`
documentation.

.. _random_label:

A Complex Example using a Dictionary Configuration
**************************************************

Questionary allows creating quite complex workflows when combining all of the
above concepts:

.. literalinclude:: ../../examples/advanced_workflow.py
   :language: python3


The above workflow will show to the user the following prompts:

1. Yes/No question ``"Would you like the next question?"``.

2. ``"Name this library?"`` - only shown when the first question is answered
   with yes.

3. A question to select an item from a list.
4. Free text input if ``"other"`` is selected in step 3.

Depending on the route the user took, the result will look like the following:

.. code-block:: python3

  { 
      'conditional_step': False,
      'second_question': 'Test input'   # Free form text
  }

.. code-block:: python3

  { 
      'conditional_step': True,
      'next_question': 'questionary',
      'second_question': 'Test input'   # Free form text
  }

You can test this workflow yourself by running the
`advanced_workflow.py example <https://github.com/tmbo/questionary/blob/master/examples/advanced_workflow.py>`_.