Coverage for src / questions / __init__.py: 70%
83 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 16:36 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 16:36 +0000
1"""
2Ask questions and answer them by prompting the user
4Special cases to be handled:
5- ctrl-d: Set value to None
7"""
9import sys
10from contextlib import nullcontext
12import prompt_toolkit
13from prompt_toolkit.key_binding import KeyBindings
14from prompt_toolkit.validation import ValidationError, Validator
16from .. import jinja, utils
17from .validators import allowed, max_length, max_value, min_length, min_value, nullable, vartype
19bindings = KeyBindings()
22# pylint: disable=redefined-outer-name
23def _stdin_input(prompt):
24 """
25 Convenience function in order to mock the builtin
26 """
27 return input(prompt)
30def _validate(value, schema):
31 """
32 Validate the answer and return modified answer if needed.
34 Return value + stop (True/False)
35 """
36 # Order matters. By checking nullable and default first
37 # we can make assumptions on values (null? not null? etc.)
38 validators = [
39 vartype,
40 nullable,
41 allowed,
42 max_length,
43 min_length,
44 min_value,
45 max_value,
46 ]
48 # Returns (value, True) if we are to stop iterating,
49 # we are to replace value by a new value
50 for validator in validators:
51 value, stop = validator.validate(value, schema)
52 if stop is True:
53 return value
54 return value
57def _prepare(question, env, ctx):
58 """
59 Determine if the question is to be asked, and
60 if so, build a prompt
62 Return true if question is to be asked.
63 """
65 # Prepare all values in order to have an json snapshot of
66 # every question
67 name = question["name"]
69 # Schema might not be present by default
70 schema = question.get("schema", {})
71 question["schema"] = schema
73 prompt = name
74 prompt = str(jinja.evaluate(prompt, env, ctx))
75 prompt += ": "
76 question["prompt"] = prompt
78 description = question.get("description")
79 question["description"] = str(jinja.evaluate(description, env, ctx))
81 hidden = question.get("hidden", False)
82 question["hidden"] = jinja.evaluate(hidden, env, ctx)
84 # Will we prompt this question?
85 condition = question.get("if")
86 condition = jinja.evaluate(condition, env, ctx)
87 if condition is not None:
88 question["if"] = condition
89 if not condition:
90 return False
92 # Set the answer as the "default", override if necessary
93 # any previous "default" value
94 # It will be eval'd during the "default" code block
95 answer = ctx["scaffold"].get(question["name"])
96 if answer is not None: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 schema["default"] = answer
99 # Prepare the default value
100 default = schema.get("default")
101 default = jinja.evaluate(default, env, ctx)
102 if default is None:
103 schema.pop("default", None)
104 else:
105 schema["default"] = default
106 return True
109def prompt(tpl, ctx):
110 """
111 For every question in the input file, ask the question
112 and record the answer in the context
113 """
114 env, _ = jinja.create(tpl.data["jinja2"])
116 questions = tpl.data["questions"]
117 for question in questions:
118 if not _prepare(question, env, ctx):
119 continue
121 name, value = _prompt(question)
123 question["value"] = value
124 ctx = jinja.ctx_add(ctx, name, value)
126 return tpl, env, ctx
129def _prompt(question):
130 """
131 Ask the question until it is answered or canceled
133 Returns key, value
134 """
135 name = question.get("name")
136 prompt = question.get("prompt")
137 hidden = question.get("hidden")
138 description = question.get("description")
139 schema = question.get("schema", {}) or {}
140 default = schema.get("default")
142 def prevalidate(x, schema):
143 # Exceptions are raised, always return True
144 _validate(x, schema)
145 return True
147 validator = Validator.from_callable(lambda x: prevalidate(x, schema))
149 while True:
150 try:
151 # if hidden, then return default, which was previously set to
152 # the value in answer if present, and otherwise
153 # to the default from scaffold.yml
154 if hidden:
155 answer = default
156 elif sys.stdin.isatty(): 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true
157 kwargs = {}
158 if default is not None:
159 kwargs["default"] = str(default)
161 answer = prompt_toolkit.prompt(
162 prompt,
163 validator=validator,
164 bottom_toolbar=description,
165 key_bindings=bindings,
166 validate_while_typing=False,
167 **kwargs,
168 )
169 else:
170 answer = _stdin_input(prompt)
171 answer = _validate(answer, schema)
172 return name, answer
174 except EOFError:
175 # ctrl-d was used
176 try:
177 answer = _validate(None, schema)
178 return name, answer
179 except ValidationError:
180 continue