Coverage for tests / test_core.py: 100%

233 statements  

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

1from typing import Annotated 1abcdefg

2 

3import pytest 1abcdefg

4import typer 1abcdefg

5import typer._completion_shared 1abcdefg

6import typer.completion 1abcdefg

7from typer import _click 1abcdefg

8from typer.core import TyperArgument, TyperCommand, TyperGroup, TyperOption, _split_opt 1abcdefg

9from typer.testing import CliRunner 1abcdefg

10 

11runner = CliRunner() 1abcdefg

12 

13 

14def test_human_readable_name() -> None: 1abcdefg

15 app = typer.Typer() 1abcdefg

16 

17 @app.command() 1abcdefg

18 def main( 1abcdefg

19 my_arg_1: Annotated[str, typer.Argument()], 

20 my_arg_2: Annotated[str, typer.Argument(metavar="META_ARG")], 

21 my_opt: Annotated[str, typer.Option()], 

22 ): 

23 pass # pragma: no cover 

24 

25 command = typer.main.get_command(app) 1abcdefg

26 params = {param.name: param for param in command.params} 1abcdefg

27 

28 assert params["my_arg_1"].human_readable_name == "my_arg_1" 1abcdefg

29 assert params["my_arg_2"].human_readable_name == "META_ARG" 1abcdefg

30 assert params["my_opt"].human_readable_name == "my_opt" 1abcdefg

31 

32 

33def test_parameter_metavar() -> None: 1abcdefg

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

35 

36 @app.command() 1abcdefg

37 def cmd(name: Annotated[str, typer.Option(metavar="CUSTOM")]) -> None: 1abcdefg

38 pass # pragma: no cover 

39 

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

41 assert result.exit_code == 0 1abcdefg

42 assert "--name CUSTOM" in result.output 1abcdefg

43 

44 

45def test_parameter_nargs_gt_1() -> None: 1abcdefg

46 param = TyperArgument(param_decls=["value"], type=str, nargs=2) 1abcdefg

47 ctx = _click.Context(TyperCommand(name="cmd")) 1abcdefg

48 

49 assert param.type_cast_value(ctx, ("one", "two")) == ("one", "two") 1abcdefg

50 

51 with pytest.raises( 1abcdefg

52 _click.exceptions.BadParameter, match="Takes 2 values but 1 given." 

53 ): 

54 param.type_cast_value(ctx, ("one",)) 1abcdefg

55 

56 

57def test_parameter_constructor() -> None: 1abcdefg

58 # no param_decl and expose_value is False: sets name to None 

59 arg = TyperArgument(param_decls=[], expose_value=False) 1abcdefg

60 assert arg.name is None 1abcdefg

61 assert arg.opts == [] 1abcdefg

62 assert arg.secondary_opts == [] 1abcdefg

63 

64 # no param_decl and expose_value is True: raises 

65 with pytest.raises(TypeError, match="does not have a name."): 1abcdefg

66 TyperArgument(param_decls=[], expose_value=True) 1abcdefg

67 

68 # len(param_decl) > 1: raises 

69 with pytest.raises(TypeError, match="take exactly one parameter declaration"): 1abcdefg

70 TyperArgument(param_decls=["first", "second"]) 1abcdefg

71 

72 # duplicated identifier in option declarations: raises 

73 with pytest.raises(TypeError, match="Name 'name' defined twice"): 1abcdefg

74 TyperOption(param_decls=["name", "name"], required=False) 1abcdefg

75 

76 # same true/false flag in boolean option declaration: raises 

77 with pytest.raises(ValueError, match="cannot use the same flag for true/false"): 1abcdefg

78 TyperOption(param_decls=["flag", "--flag/--flag"], required=False, is_flag=True) 1abcdefg

79 

80 # inferred name is not a valid identifier: sets name to None 

81 unnamed_option = TyperOption(param_decls=["--123"], required=False) 1abcdefg

82 assert unnamed_option.name is None 1abcdefg

83 

84 # no param_decl and prompt=True: raises 

85 with pytest.raises(TypeError, match="'name' is required with 'prompt=True'."): 1abcdefg

86 TyperOption(param_decls=[], expose_value=False, prompt=True, required=False) 1abcdefg

87 

88 # count works 

89 option = TyperOption( 1abcdefg

90 param_decls=["verbose", "--verbose", "-v"], 

91 type=None, 

92 default=0, 

93 required=False, 

94 count=True, 

95 ) 

96 assert isinstance(option.type, _click.types.IntRange) 1abcdefg

97 assert option.type.min == 0 1abcdefg

98 

99 

100def test_option_error_hint() -> None: 1abcdefg

101 option = TyperOption( 1abcdefg

102 param_decls=["name", "--name"], 

103 required=False, 

104 show_envvar=True, 

105 envvar="APP_NAME", 

106 ) 

107 hint = option.get_error_hint(_click.Context(TyperCommand(name="cmd"))) 1abcdefg

108 assert "(env var: 'APP_NAME')" in hint 1abcdefg

109 

110 

111def test_group_init() -> None: 1abcdefg

112 group_no_commands = TyperGroup(name="root", commands=None) 1abcdefg

113 assert group_no_commands.commands == {} 1abcdefg

114 

115 named = TyperCommand(name="named") 1abcdefg

116 unnamed = TyperCommand(name=None) 1abcdefg

117 group_command_sequence = TyperGroup(name="root", commands=[named, unnamed]) 1abcdefg

118 assert group_command_sequence.commands == {"named": named} 1abcdefg

119 

120 

121@pytest.mark.parametrize("with_result_callback", [False, True]) 1abcdefg

122def test_group_result_callback(with_result_callback: bool) -> None: 1abcdefg

123 called = {"child": False, "result_callback": False} 1abcdefg

124 

125 def child_callback() -> None: 1abcdefg

126 called["child"] = True 1abcdefg

127 return None 1abcdefg

128 

129 def result_callback(value, **kwargs): # type: ignore[no-untyped-def] 1abcdefg

130 called["result_callback"] = True 1abcdefg

131 return value 1abcdefg

132 

133 child = TyperCommand(name="child", callback=child_callback) 1abcdefg

134 group = TyperGroup( 1abcdefg

135 name="root", 

136 commands={"child": child}, 

137 result_callback=result_callback if with_result_callback else None, 

138 ) 

139 ctx = group.make_context("root", ["child"]) 1abcdefg

140 

141 result = group.invoke(ctx) 1abcdefg

142 

143 assert result is None 1abcdefg

144 assert called["child"] is True 1abcdefg

145 assert called["result_callback"] is with_result_callback 1abcdefg

146 assert ctx.invoked_subcommand == "child" 1abcdefg

147 

148 

149def test_group_add_command() -> None: 1abcdefg

150 group = TyperGroup(name="root") 1abcdefg

151 unnamed_command = TyperCommand(name=None) 1abcdefg

152 

153 with pytest.raises(TypeError, match="Command has no name."): 1abcdefg

154 group.add_command(unnamed_command) 1abcdefg

155 

156 

157def test_group_click_resolve_command() -> None: 1abcdefg

158 child = TyperCommand(name="child") 1abcdefg

159 group = TyperGroup(name="root", commands={"child": child}) 1abcdefg

160 ctx = group.make_context("root", ["CHILD"], token_normalize_func=str.lower) 1abcdefg

161 

162 cmd_name, cmd, remaining = group._click_resolve_command(ctx, ["CHILD"]) 1abcdefg

163 

164 assert cmd_name == "child" 1abcdefg

165 assert cmd is child 1abcdefg

166 assert remaining == [] 1abcdefg

167 

168 

169@pytest.mark.parametrize( 1abcdefg

170 ("envvar", "auto_prefix", "set_env", "expected"), 

171 [ 

172 ("APP_NAME", None, True, "my-precious"), 

173 (None, "APP", True, "my-precious"), 

174 (None, None, False, None), 

175 ], 

176) 

177def test_option_resolve_envvar( 1abcdefg

178 monkeypatch: pytest.MonkeyPatch, 

179 envvar: str | None, 

180 auto_prefix: str | None, 

181 set_env: bool, 

182 expected: str | None, 

183) -> None: 

184 option = TyperOption( 1abcdefg

185 param_decls=["name", "--name"], 

186 required=False, 

187 envvar=envvar, 

188 ) 

189 if set_env: 1abcdefg

190 monkeypatch.setenv("APP_NAME", "my-precious") 1abcdefg

191 

192 ctx = _click.Context(TyperCommand(name="cmd"), auto_envvar_prefix=auto_prefix) 1abcdefg

193 assert option.resolve_envvar_value(ctx) == expected 1abcdefg

194 

195 

196def test_option_resolve_envvar_list( 1abcdefg

197 monkeypatch: pytest.MonkeyPatch, 

198) -> None: 

199 option = TyperOption( 1abcdefg

200 param_decls=["name", "--name"], 

201 required=False, 

202 envvar=["APP_NAME_1", "APP_NAME_2"], 

203 ) 

204 monkeypatch.delenv("APP_NAME_1", raising=False) 1abcdefg

205 monkeypatch.delenv("APP_NAME_2", raising=False) 1abcdefg

206 ctx = _click.Context(TyperCommand(name="cmd")) 1abcdefg

207 

208 assert option.resolve_envvar_value(ctx) is None 1abcdefg

209 

210 

211def test_context_auto_envvar() -> None: 1abcdefg

212 app = typer.Typer(context_settings={"auto_envvar_prefix": "APP"}) 1abcdefg

213 sub_app = typer.Typer() 1abcdefg

214 

215 @sub_app.command() 1abcdefg

216 def clone(ctx: typer.Context) -> None: 1abcdefg

217 print(ctx.auto_envvar_prefix) 1abcdefg

218 

219 app.add_typer(sub_app, name="beth") 1abcdefg

220 

221 result = runner.invoke(app, ["beth", "clone"]) 1abcdefg

222 assert result.exit_code == 0 1abcdefg

223 assert "APP_BETH_CLONE" in result.stdout 1abcdefg

224 

225 

226def test_context_with_resource() -> None: 1abcdefg

227 events: list[str] = [] 1abcdefg

228 

229 class DemoResource: 1abcdefg

230 def __enter__(self) -> str: 1abcdefg

231 events.append("enter") 1abcdefg

232 return "pickle-rick" 1abcdefg

233 

234 def __exit__(self, *args: object) -> None: 1abcdefg

235 events.append("exit") 1abcdefg

236 

237 app = typer.Typer() 1abcdefg

238 

239 @app.command() 1abcdefg

240 def cmd(ctx: typer.Context) -> None: 1abcdefg

241 value = ctx.with_resource(DemoResource()) 1abcdefg

242 assert value == "pickle-rick" 1abcdefg

243 assert events == ["enter"] 1abcdefg

244 print("I'm a pickle") 1abcdefg

245 

246 result = runner.invoke(app) 1abcdefg

247 

248 assert result.exit_code == 0 1abcdefg

249 assert "I'm a pickle" in result.stdout 1abcdefg

250 assert events == ["enter", "exit"] 1abcdefg

251 

252 

253def test_context_find_root() -> None: 1abcdefg

254 app = typer.Typer() 1abcdefg

255 sub_app = typer.Typer() 1abcdefg

256 

257 @sub_app.command() 1abcdefg

258 def child(ctx: typer.Context) -> None: 1abcdefg

259 root = ctx.find_root() 1abcdefg

260 assert root.parent is None 1abcdefg

261 assert root is ctx.parent.parent 1abcdefg

262 print("ok") 1abcdefg

263 

264 app.add_typer(sub_app, name="sub") 1abcdefg

265 

266 result = runner.invoke(app, ["sub", "child"]) 1abcdefg

267 assert result.exit_code == 0 1abcdefg

268 assert "ok" in result.stdout 1abcdefg

269 

270 

271def test_context_find_object() -> None: 1abcdefg

272 class Marker: 1abcdefg

273 pass 1abcdefg

274 

275 marker = Marker() 1abcdefg

276 app = typer.Typer() 1abcdefg

277 

278 @app.callback() 1abcdefg

279 def callback(ctx: typer.Context) -> None: 1abcdefg

280 ctx.obj = marker 1abcdefg

281 

282 @app.command() 1abcdefg

283 def child(ctx: typer.Context) -> None: 1abcdefg

284 assert ctx.find_object(Marker) is marker 1abcdefg

285 assert ctx.find_object(str) is None 1abcdefg

286 print("ok") 1abcdefg

287 

288 result = runner.invoke(app, ["child"]) 1abcdefg

289 assert result.exit_code == 0 1abcdefg

290 assert "ok" in result.stdout 1abcdefg

291 

292 

293def test_context_lookup_default_callable() -> None: 1abcdefg

294 app = typer.Typer() 1abcdefg

295 

296 @app.command() 1abcdefg

297 def child(ctx: typer.Context) -> None: 1abcdefg

298 ctx.default_map = {"planet": lambda: "Earth"} 1abcdefg

299 assert ctx.lookup_default("planet") == "Earth" 1abcdefg

300 value = ctx.lookup_default("planet", call=False) 1abcdefg

301 assert callable(value) 1abcdefg

302 print("ok") 1abcdefg

303 

304 result = runner.invoke(app) 1abcdefg

305 assert result.exit_code == 0 1abcdefg

306 assert "ok" in result.stdout 1abcdefg

307 

308 

309def test_context_abort() -> None: 1abcdefg

310 app = typer.Typer() 1abcdefg

311 

312 @app.command() 1abcdefg

313 def cmd(ctx: typer.Context) -> None: 1abcdefg

314 ctx.abort() 1abcdefg

315 

316 result = runner.invoke(app, standalone_mode=False) 1abcdefg

317 assert result.exit_code == 1 1abcdefg

318 assert isinstance(result.exception, _click.core.Abort) 1abcdefg

319 

320 

321def test_command_help_disabled() -> None: 1abcdefg

322 app = typer.Typer() 1abcdefg

323 

324 @app.command(add_help_option=False) 1abcdefg

325 def cmd() -> None: 1abcdefg

326 pass # pragma: no cover 

327 

328 result = runner.invoke(app, ["--help"], standalone_mode=False) 1abcdefg

329 assert result.exit_code == 1 1abcdefg

330 assert isinstance(result.exception, _click.exceptions.NoSuchOption) 1abcdefg

331 assert result.exception.option_name == "--help" 1abcdefg

332 

333 

334def test_command_help_deprecated() -> None: 1abcdefg

335 app = typer.Typer(rich_markup_mode=None, epilog="Built with love") 1abcdefg

336 

337 @app.command(short_help="Shorty", help="Regular help text.", deprecated=True) 1abcdefg

338 def one() -> None: 1abcdefg

339 pass # pragma: no cover 

340 

341 @app.command() 1abcdefg

342 def two() -> None: 1abcdefg

343 pass # pragma: no cover 

344 

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

346 assert result.exit_code == 0 1abcdefg

347 assert "Regular help text. (DEPRECATED)" in result.output 1abcdefg

348 

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

350 assert result.exit_code == 0 1abcdefg

351 assert "Built with love" in result.output 1abcdefg

352 assert "oneShorty(DEPRECATED)" in result.output.replace(" ", "") 1abcdefg

353 

354 

355@pytest.mark.parametrize( 1abcdefg

356 ("value", "expected_prefix", "expected_opt"), 

357 [ 

358 ("--verbose", "--", "verbose"), 

359 ("//verbose", "//", "verbose"), 

360 ("-verbose", "-", "verbose"), 

361 ("verbose", "", "verbose"), 

362 ], 

363) 

364def test_split_opt(value: str, expected_prefix: str, expected_opt: str) -> None: 1abcdefg

365 prefix, opt = _split_opt(value) 1abcdefg

366 assert prefix == expected_prefix 1abcdefg

367 assert opt == expected_opt 1abcdefg

368 

369 

370def test_nargs_default_map(): 1abcdefg

371 app = typer.Typer() 1abcdefg

372 

373 @app.command() 1abcdefg

374 def main(names: list[str] = typer.Option(None)): 1abcdefg

375 print(names) # pragma: no cover 

376 

377 result = runner.invoke(app, [], default_map={"names": "not-a-list"}) 1abcdefg

378 assert result.exit_code == 2 1abcdefg

379 assert "Invalid value" in result.output 1abcdefg

380 

381 

382def test_parameter_name_casing(): 1abcdefg

383 app = typer.Typer() 1abcdefg

384 

385 @app.command() 1abcdefg

386 def main( 1abcdefg

387 arg1: int, 

388 arg2: int = 42, 

389 arg3: int = typer.Argument(...), 

390 ARG4: int = typer.Argument(42), 

391 ARG5: int = typer.Option(...), 

392 arg6: int = typer.Option(42), 

393 arg7: int = typer.Argument(42, metavar="meta7"), 

394 arg8: int = typer.Argument(metavar="ARG8"), 

395 arg9: int = typer.Option(metavar="ARG9"), 

396 ): 

397 print( 1abcdefg

398 f"arg1={arg1} arg2={arg2} arg3={arg3} ARG4={ARG4} ARG5={ARG5} " 

399 f"arg6={arg6} arg7={arg7} arg8={arg8} arg9={arg9}" 

400 ) 

401 

402 result = runner.invoke( 1abcdefg

403 app, 

404 [ 

405 "1", 

406 "3", 

407 "4", 

408 "7", 

409 "8", 

410 "--arg2", 

411 "2", 

412 "--ARG5", 

413 "5", 

414 "--arg6", 

415 "6", 

416 "--arg9", 

417 "9", 

418 ], 

419 ) 

420 assert result.exit_code == 0 1abcdefg

421 assert ( 1abcdefg

422 "arg1=1 arg2=2 arg3=3 ARG4=4 ARG5=5 arg6=6 arg7=7 arg8=8 arg9=9" 

423 in result.output 

424 ) 

425 

426 result = runner.invoke(app, ["1", "3", "4", "7", "8", "--ARG5", "5", "--arg9", "9"]) 1abcdefg

427 assert result.exit_code == 0 1abcdefg

428 assert ( 1abcdefg

429 "arg1=1 arg2=42 arg3=3 ARG4=4 ARG5=5 arg6=42 arg7=7 arg8=8 arg9=9" 

430 in result.output 

431 ) 

432 

433 result = runner.invoke(app, ["1", "3", "4", "7", "8", "--arg5", "5", "--arg9", "9"]) 1abcdefg

434 assert result.exit_code != 0 1abcdefg

435 assert "No such option: --arg5" in result.output 1abcdefg

436 

437 result = runner.invoke(app, ["1", "3", "4", "7", "8", "--ARG5", "5", "--ARG9", "9"]) 1abcdefg

438 assert result.exit_code != 0 1abcdefg

439 assert "No such option: --ARG9" in result.output 1abcdefg