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

1""" 

2Ask questions and answer them by prompting the user 

3 

4Special cases to be handled: 

5- ctrl-d: Set value to None 

6 

7""" 

8 

9import sys 

10from contextlib import nullcontext 

11 

12import prompt_toolkit 

13from prompt_toolkit.key_binding import KeyBindings 

14from prompt_toolkit.validation import ValidationError, Validator 

15 

16from .. import jinja, utils 

17from .validators import allowed, max_length, max_value, min_length, min_value, nullable, vartype 

18 

19bindings = KeyBindings() 

20 

21 

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) 

28 

29 

30def _validate(value, schema): 

31 """ 

32 Validate the answer and return modified answer if needed. 

33 

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 ] 

47 

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 

55 

56 

57def _prepare(question, env, ctx): 

58 """ 

59 Determine if the question is to be asked, and 

60 if so, build a prompt 

61 

62 Return true if question is to be asked. 

63 """ 

64 

65 # Prepare all values in order to have an json snapshot of 

66 # every question 

67 name = question["name"] 

68 

69 # Schema might not be present by default 

70 schema = question.get("schema", {}) 

71 question["schema"] = schema 

72 

73 prompt = name 

74 prompt = str(jinja.evaluate(prompt, env, ctx)) 

75 prompt += ": " 

76 question["prompt"] = prompt 

77 

78 description = question.get("description") 

79 question["description"] = str(jinja.evaluate(description, env, ctx)) 

80 

81 hidden = question.get("hidden", False) 

82 question["hidden"] = jinja.evaluate(hidden, env, ctx) 

83 

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 

91 

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 

98 

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 

107 

108 

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"]) 

115 

116 questions = tpl.data["questions"] 

117 for question in questions: 

118 if not _prepare(question, env, ctx): 

119 continue 

120 

121 name, value = _prompt(question) 

122 

123 question["value"] = value 

124 ctx = jinja.ctx_add(ctx, name, value) 

125 

126 return tpl, env, ctx 

127 

128 

129def _prompt(question): 

130 """ 

131 Ask the question until it is answered or canceled 

132 

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") 

141 

142 def prevalidate(x, schema): 

143 # Exceptions are raised, always return True 

144 _validate(x, schema) 

145 return True 

146 

147 validator = Validator.from_callable(lambda x: prevalidate(x, schema)) 

148 

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) 

160 

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 

173 

174 except EOFError: 

175 # ctrl-d was used 

176 try: 

177 answer = _validate(None, schema) 

178 return name, answer 

179 except ValidationError: 

180 continue