Coverage for tests / test_others.py: 100%

267 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1import os 1abcdefg

2import subprocess 1abcdefg

3import sys 1abcdefg

4import typing 1abcdefg

5from pathlib import Path 1abcdefg

6from typing import Annotated 1abcdefg

7from unittest import mock 1abcdefg

8 

9import pytest 1abcdefg

10import typer 1abcdefg

11import typer._completion_shared 1abcdefg

12import typer.completion 1abcdefg

13from typer import _click 1abcdefg

14from typer.main import solve_typer_info_defaults, solve_typer_info_help 1abcdefg

15from typer.models import ParameterInfo, TyperInfo 1abcdefg

16from typer.testing import CliRunner 1abcdefg

17 

18from .utils import requires_completion_permission 1abcdefg

19 

20runner = CliRunner() 1abcdefg

21 

22 

23def test_help_from_info(): 1abcdefg

24 # Mainly for coverage/completeness 

25 value = solve_typer_info_help(TyperInfo()) 1abcdefg

26 assert value is None 1abcdefg

27 

28 

29def test_defaults_from_info(): 1abcdefg

30 # Mainly for coverage/completeness 

31 value = solve_typer_info_defaults(TyperInfo()) 1abcdefg

32 assert value 1abcdefg

33 

34 

35def test_too_many_parsers(): 1abcdefg

36 def custom_parser(value: str) -> int: 1abcdefg

37 return int(value) # pragma: no cover 

38 

39 class CustomClickParser(_click.types.ParamType): 1abcdefg

40 name = "custom_parser" 1abcdefg

41 

42 def convert( 1abcdefg

43 self, 

44 value: str, 

45 param: _click.Parameter | None, 

46 ctx: _click.Context | None, 

47 ) -> typing.Any: 

48 return int(value) # pragma: no cover 

49 

50 expected_error = ( 1abcdefg

51 "Multiple custom type parsers provided. " 

52 "`parser` and `click_type` may not both be provided." 

53 ) 

54 

55 with pytest.raises(ValueError, match=expected_error): 1abcdefg

56 ParameterInfo(parser=custom_parser, click_type=CustomClickParser()) 1abcdefg

57 

58 

59def test_valid_parser_permutations(): 1abcdefg

60 def custom_parser(value: str) -> int: 1abcdefg

61 return int(value) # pragma: no cover 

62 

63 class CustomClickParser(_click.types.ParamType): 1abcdefg

64 name = "custom_parser" 1abcdefg

65 

66 def convert( 1abcdefg

67 self, 

68 value: str, 

69 param: _click.Parameter | None, 

70 ctx: _click.Context | None, 

71 ) -> typing.Any: 

72 return int(value) # pragma: no cover 

73 

74 ParameterInfo() 1abcdefg

75 ParameterInfo(parser=custom_parser) 1abcdefg

76 ParameterInfo(click_type=CustomClickParser()) 1abcdefg

77 

78 

79@requires_completion_permission 1abcdefg

80def test_install_invalid_shell(): 1abcdefg

81 app = typer.Typer() 1abcdefg

82 

83 @app.command() 1abcdefg

84 def main(): 1abcdefg

85 print("Hello World") 1abcdefg

86 

87 with mock.patch.object( 1abcdefg

88 typer._completion_shared, "_get_shell_name", return_value="xshell" 

89 ): 

90 result = runner.invoke(app, ["--install-completion"]) 1abcdefg

91 assert "Shell xshell is not supported." in result.stdout 1abcdefg

92 result = runner.invoke(app) 1abcdefg

93 assert "Hello World" in result.stdout 1abcdefg

94 

95 

96def test_callback_too_many_parameters(): 1abcdefg

97 app = typer.Typer() 1abcdefg

98 

99 def name_callback(ctx, param, val1, val2): 1abcdefg

100 pass # pragma: no cover 

101 

102 @app.command() 1abcdefg

103 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg

104 pass # pragma: no cover 

105 

106 with pytest.raises(_click.ClickException) as exc_info: 1abcdefg

107 runner.invoke(app, ["--name", "Camila"]) 1abcdefg

108 assert ( 1abcdefg

109 exc_info.value.message == "Too many CLI parameter callback function parameters" 

110 ) 

111 

112 

113def test_callback_2_untyped_parameters(): 1abcdefg

114 app = typer.Typer() 1abcdefg

115 

116 def name_callback(ctx, value): 1abcdefg

117 print(f"info name is: {ctx.info_name}") 1abcdefg

118 print(f"value is: {value}") 1abcdefg

119 

120 @app.command() 1abcdefg

121 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg

122 print("Hello World") 1abcdefg

123 

124 result = runner.invoke(app, ["--name", "Camila"]) 1abcdefg

125 assert "info name is: main" in result.stdout 1abcdefg

126 assert "value is: Camila" in result.stdout 1abcdefg

127 

128 

129@pytest.mark.parametrize( 1abcdefg

130 ("param_hint", "option_decls", "expected_message"), 

131 [ 

132 ("--name", (), "Invalid value for --name"), 

133 (None, ("--name", "-n"), "Invalid value for '--name' / '-n'"), 

134 ], 

135) 

136def test_bad_parameter_callback( 1abcdefg

137 param_hint: str | None, option_decls: tuple[str, ...], expected_message: str 

138) -> None: 

139 app = typer.Typer() 1abcdefg

140 

141 def my_bad(value: str) -> str: 1abcdefg

142 kwargs = {"param_hint": param_hint} if param_hint is not None else {} 1abcdefg

143 raise typer.BadParameter("custom validation failed", **kwargs) 1abcdefg

144 

145 @app.command() 1abcdefg

146 def main(name: str = typer.Option(..., *option_decls, callback=my_bad)) -> None: 1abcdefg

147 typer.echo(name) # pragma: no cover 

148 

149 result = runner.invoke(app, ["--name", "Camila"]) 1abcdefg

150 assert result.exit_code == 2 1abcdefg

151 assert expected_message in result.stderr 1abcdefg

152 assert "custom validation failed" in result.stderr 1abcdefg

153 

154 

155def test_bad_parameter_main() -> None: 1abcdefg

156 app = typer.Typer() 1abcdefg

157 

158 @app.command() 1abcdefg

159 def main() -> None: 1abcdefg

160 raise typer.BadParameter("custom validation failed") 1abcdefg

161 

162 result = runner.invoke(app, []) 1abcdefg

163 assert result.exit_code == 2 1abcdefg

164 assert "Invalid value: custom validation failed" in result.stderr 1abcdefg

165 

166 

167@pytest.mark.parametrize( 1abcdefg

168 ("kw", "msg"), 

169 [ 

170 ( 

171 {"param_hint": ["--name", "-n"], "param_type": "parameter"}, 

172 "Missing parameter '--name' / '-n'.", 

173 ), 

174 ({"param_type": "value"}, "Missing value."), 

175 ], 

176) 

177def test_missing_parameter_msg(kw: dict[str, object], msg: str) -> None: 1abcdefg

178 app = typer.Typer(rich_markup_mode=None) 1abcdefg

179 

180 @app.command() 1abcdefg

181 def main() -> None: 1abcdefg

182 raise typer._click.exceptions.MissingParameter(**kw) 1abcdefg

183 

184 result = runner.invoke(app, []) 1abcdefg

185 assert result.exit_code == 2 1abcdefg

186 assert msg in result.stderr 1abcdefg

187 

188 

189def test_missing_parameter_callback_msg() -> None: 1abcdefg

190 def my_cb(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str: 1abcdefg

191 raise typer._click.exceptions.MissingParameter( 1abcdefg

192 message="My bad", ctx=ctx, param=param, param_type="parameter" 

193 ) 

194 

195 app = typer.Typer(rich_markup_mode=None) 1abcdefg

196 

197 @app.command() 1abcdefg

198 def main( 1abcdefg

199 mode: Annotated[ 

200 typing.Literal["alpha", "beta"], 

201 typer.Option(..., "--mode", callback=my_cb), 

202 ], 

203 ) -> None: 

204 typer.echo(mode) # pragma: no cover 

205 

206 result = runner.invoke(app, ["--mode", "alpha"]) 1abcdefg

207 assert result.exit_code == 2 1abcdefg

208 assert "Missing parameter '--mode'." in result.stderr 1abcdefg

209 assert "My bad. Choose from:" in result.stderr 1abcdefg

210 assert "alpha" in result.stderr 1abcdefg

211 assert "beta" in result.stderr 1abcdefg

212 result_msg = runner.invoke(app, ["--mode", "alpha"], standalone_mode=False) 1abcdefg

213 assert isinstance(result_msg.exception, typer._click.exceptions.MissingParameter) 1abcdefg

214 assert str(result_msg.exception) == "My bad" 1abcdefg

215 

216 

217def test_missing_parameter_str() -> None: 1abcdefg

218 def my_cb(ctx: typer.Context, param: typer.CallbackParam, value: str) -> str: 1abcdefg

219 raise typer._click.exceptions.MissingParameter(ctx=ctx, param=param) 1abcdefg

220 

221 app = typer.Typer() 1abcdefg

222 

223 @app.command() 1abcdefg

224 def main(mode: str = typer.Option(..., "--mode", callback=my_cb)) -> None: 1abcdefg

225 typer.echo(mode) # pragma: no cover 

226 

227 result2 = runner.invoke(app, ["--mode", "alpha"], standalone_mode=False) 1abcdefg

228 assert isinstance(result2.exception, typer._click.exceptions.MissingParameter) 1abcdefg

229 assert str(result2.exception) == "Missing parameter: mode" 1abcdefg

230 

231 

232def test_click_exception_show_default_file() -> None: 1abcdefg

233 app = typer.Typer(rich_markup_mode=None) 1abcdefg

234 

235 @app.command() 1abcdefg

236 def main() -> None: 1abcdefg

237 raise typer._click.ClickException("custom click failure") 1abcdefg

238 

239 result = runner.invoke(app, []) 1abcdefg

240 assert result.exit_code == 1 1abcdefg

241 assert "custom click" in result.stderr 1abcdefg

242 assert "failure" in result.stderr 1abcdefg

243 

244 

245def test_no_args_is_help_show() -> None: 1abcdefg

246 app = typer.Typer(rich_markup_mode=None) 1abcdefg

247 

248 @app.callback(invoke_without_command=True, no_args_is_help=True) 1abcdefg

249 def main() -> None: 1abcdefg

250 return None # pragma: no cover 

251 

252 result = runner.invoke(app, []) 1abcdefg

253 assert result.exit_code == 2 1abcdefg

254 assert "Usage:" in result.stderr 1abcdefg

255 assert "Show this message and exit." in result.stderr 1abcdefg

256 

257 

258def test_callback_3_untyped_parameters(): 1abcdefg

259 app = typer.Typer() 1abcdefg

260 

261 def name_callback(ctx, param, value): 1abcdefg

262 print(f"info name is: {ctx.info_name}") 1abcdefg

263 print(f"param name is: {param.name}") 1abcdefg

264 print(f"value is: {value}") 1abcdefg

265 

266 @app.command() 1abcdefg

267 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg

268 print("Hello World") 1abcdefg

269 

270 result = runner.invoke(app, ["--name", "Camila"]) 1abcdefg

271 assert "info name is: main" in result.stdout 1abcdefg

272 assert "param name is: name" in result.stdout 1abcdefg

273 assert "value is: Camila" in result.stdout 1abcdefg

274 

275 

276def test_callback_4_list_none(): 1abcdefg

277 app = typer.Typer() 1abcdefg

278 

279 def names_callback(ctx, param, values: list[str] | None): 1abcdefg

280 if values is None: 1abcdefg

281 return values 1abcdefg

282 return [value.upper() for value in values] 1abcdefg

283 

284 @app.command() 1abcdefg

285 def main( 1abcdefg

286 names: list[str] | None = typer.Option(None, "--name", callback=names_callback), 

287 ): 

288 if names is None: 1abcdefg

289 print("Hello World") 1abcdefg

290 else: 

291 print(f"Hello {', '.join(names)}") 1abcdefg

292 

293 result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) 1abcdefg

294 assert "Hello SIDESHOW, BOB" in result.stdout 1abcdefg

295 

296 result = runner.invoke(app, []) 1abcdefg

297 assert "Hello World" in result.stdout 1abcdefg

298 

299 

300def test_multiple_bool_flags() -> None: 1abcdefg

301 app = typer.Typer() 1abcdefg

302 

303 @app.command() 1abcdefg

304 def main(choices: list[bool] = typer.Option([], "--accept/--reject")) -> None: 1abcdefg

305 print(choices) 1abcdefg

306 

307 result = runner.invoke(app, ["--accept", "--reject", "--accept"]) 1abcdefg

308 assert result.exit_code == 0 1abcdefg

309 assert "[True, False, True]" in result.stdout 1abcdefg

310 

311 

312def test_empty_list_default_generator(): 1abcdefg

313 def empty_list() -> list[str]: 1abcdefg

314 return [] 1abcdefg

315 

316 app = typer.Typer() 1abcdefg

317 

318 @app.command() 1abcdefg

319 def main( 1abcdefg

320 names: Annotated[list[str], typer.Option(default_factory=empty_list)], 

321 ): 

322 print(names) 1abcdefg

323 

324 result = runner.invoke(app) 1abcdefg

325 assert "[]" in result.output 1abcdefg

326 

327 

328def test_option_envvar(): 1abcdefg

329 app = typer.Typer() 1abcdefg

330 

331 @app.command() 1abcdefg

332 def main(user: Annotated[str, typer.Option(envvar="ME")]): 1abcdefg

333 print(f"Hello {user}") 1abcdefg

334 

335 result = runner.invoke(app, env={"ME": "rick"}) 1abcdefg

336 assert result.exit_code == 0 1abcdefg

337 assert "Hello rick" in result.output 1abcdefg

338 

339 

340def test_option_envvar_list(): 1abcdefg

341 app = typer.Typer() 1abcdefg

342 

343 @app.command() 1abcdefg

344 def main(users: Annotated[list[str], typer.Option(envvar="ME")]): 1abcdefg

345 for u in users: 1abcdefg

346 print(f"Hello {u}") 1abcdefg

347 

348 result = runner.invoke(app, env={"ME": "rick morty"}) 1abcdefg

349 assert result.exit_code == 0 1abcdefg

350 assert "Hello rick" in result.output 1abcdefg

351 assert "Hello morty" in result.output 1abcdefg

352 

353 

354def test_completion_argument(): 1abcdefg

355 file_path = Path(__file__).parent / "assets/completion_argument.py" 1abcdefg

356 result = subprocess.run( 1abcdefg

357 [sys.executable, "-m", "coverage", "run", str(file_path), "E"], 

358 capture_output=True, 

359 encoding="utf-8", 

360 env={ 

361 **os.environ, 

362 "_COMPLETION_ARGUMENT.PY_COMPLETE": "complete_zsh", 

363 "_TYPER_COMPLETE_ARGS": "completion_argument.py E", 

364 "_TYPER_COMPLETE_TESTING": "True", 

365 }, 

366 ) 

367 assert "Emma" in result.stdout or "_files" in result.stdout 1abcdefg

368 assert "ctx: completion_argument" in result.stderr 1abcdefg

369 assert "arg is: name" in result.stderr 1abcdefg

370 assert "incomplete is: E" in result.stderr 1abcdefg

371 

372 

373def test_completion_untyped_parameters(): 1abcdefg

374 file_path = Path(__file__).parent / "assets/completion_no_types.py" 1abcdefg

375 result = subprocess.run( 1abcdefg

376 [sys.executable, "-m", "coverage", "run", str(file_path)], 

377 capture_output=True, 

378 encoding="utf-8", 

379 env={ 

380 **os.environ, 

381 "_COMPLETION_NO_TYPES.PY_COMPLETE": "complete_zsh", 

382 "_TYPER_COMPLETE_ARGS": "completion_no_types.py --name Sebastian --name Ca", 

383 }, 

384 ) 

385 assert "info name is: completion_no_types.py" in result.stderr 1abcdefg

386 assert "args is: []" in result.stderr 1abcdefg

387 assert "incomplete is: Ca" in result.stderr 1abcdefg

388 assert '"Camila":"The reader of books."' in result.stdout 1abcdefg

389 assert '"Carlos":"The writer of scripts."' in result.stdout 1abcdefg

390 

391 result = subprocess.run( 1abcdefg

392 [sys.executable, "-m", "coverage", "run", str(file_path)], 

393 capture_output=True, 

394 encoding="utf-8", 

395 ) 

396 assert "Hello World" in result.stdout 1abcdefg

397 

398 

399def test_completion_untyped_parameters_different_order_correct_names(): 1abcdefg

400 file_path = Path(__file__).parent / "assets/completion_no_types_order.py" 1abcdefg

401 result = subprocess.run( 1abcdefg

402 [sys.executable, "-m", "coverage", "run", str(file_path)], 

403 capture_output=True, 

404 encoding="utf-8", 

405 env={ 

406 **os.environ, 

407 "_COMPLETION_NO_TYPES_ORDER.PY_COMPLETE": "complete_zsh", 

408 "_TYPER_COMPLETE_ARGS": "completion_no_types_order.py --name Sebastian --name Ca", 

409 }, 

410 ) 

411 assert "info name is: completion_no_types_order.py" in result.stderr 1abcdefg

412 assert "args is: []" in result.stderr 1abcdefg

413 assert "incomplete is: Ca" in result.stderr 1abcdefg

414 assert '"Camila":"The reader of books."' in result.stdout 1abcdefg

415 assert '"Carlos":"The writer of scripts."' in result.stdout 1abcdefg

416 

417 result = subprocess.run( 1abcdefg

418 [sys.executable, "-m", "coverage", "run", str(file_path)], 

419 capture_output=True, 

420 encoding="utf-8", 

421 ) 

422 assert "Hello World" in result.stdout 1abcdefg

423 

424 

425def test_autocompletion_too_many_parameters(): 1abcdefg

426 app = typer.Typer() 1abcdefg

427 

428 def name_callback(ctx, args, incomplete, val2): 1abcdefg

429 pass # pragma: no cover 

430 

431 @app.command() 1abcdefg

432 def main(name: str = typer.Option(..., autocompletion=name_callback)): 1abcdefg

433 pass # pragma: no cover 

434 

435 with pytest.raises(_click.ClickException) as exc_info: 1abcdefg

436 runner.invoke(app, ["--name", "Camila"]) 1abcdefg

437 assert exc_info.value.message == "Invalid autocompletion callback parameters: val2" 1abcdefg

438 

439 

440def test_forward_references(): 1abcdefg

441 app = typer.Typer() 1abcdefg

442 

443 @app.command() 1abcdefg

444 def main(arg1, arg2: int, arg3: "int", arg4: bool = False, arg5: "bool" = False): 1abcdefg

445 print(f"arg1: {type(arg1)} {arg1}") 1abcdefg

446 print(f"arg2: {type(arg2)} {arg2}") 1abcdefg

447 print(f"arg3: {type(arg3)} {arg3}") 1abcdefg

448 print(f"arg4: {type(arg4)} {arg4}") 1abcdefg

449 print(f"arg5: {type(arg5)} {arg5}") 1abcdefg

450 

451 result = runner.invoke(app, ["Hello", "2", "invalid"]) 1abcdefg

452 

453 assert "Invalid value for 'arg3'" in result.output 1abcdefg

454 assert "'invalid' is not a valid int" in result.output 1abcdefg

455 result = runner.invoke(app, ["Hello", "2", "3", "--arg4", "--arg5"]) 1abcdefg

456 assert ( 1abcdefg

457 "arg1: <class 'str'> Hello\narg2: <class 'int'> 2\narg3: <class 'int'> 3\narg4: <class 'bool'> True\narg5: <class 'bool'> True\n" 

458 in result.stdout 

459 ) 

460 

461 

462def test_context_settings_inheritance_single_command(): 1abcdefg

463 app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}) 1abcdefg

464 

465 @app.command() 1abcdefg

466 def main(name: str): 1abcdefg

467 pass # pragma: no cover 

468 

469 result = runner.invoke(app, ["main", "-h"]) 1abcdefg

470 assert "Show this message and exit." in result.stdout 1abcdefg

471 

472 

473def test_options_metadata_typer_default(): 1abcdefg

474 app = typer.Typer(options_metavar="[options]") 1abcdefg

475 

476 @app.command() 1abcdefg

477 def c1(): 1abcdefg

478 pass # pragma: no cover 

479 

480 @app.command(options_metavar="[OPTS]") 1abcdefg

481 def c2(): 1abcdefg

482 pass # pragma: no cover 

483 

484 result = runner.invoke(app, ["c1", "--help"]) 1abcdefg

485 assert "Usage: root c1 [options]" in result.stdout 1abcdefg

486 

487 result = runner.invoke(app, ["c2", "--help"]) 1abcdefg

488 assert "Usage: root c2 [OPTS]" in result.stdout 1abcdefg