Coverage for tests / test_type_conversion.py: 100%

233 statements  

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

1import os 1abcdefg

2from datetime import datetime 1abcdefg

3from enum import Enum 1abcdefg

4from pathlib import Path 1abcdefg

5from typing import Annotated, Any, Literal 1abcdefg

6 

7import pytest 1abcdefg

8import typer 1abcdefg

9from typer import _click, models 1abcdefg

10from typer.testing import CliRunner 1abcdefg

11 

12from tests.utils import needs_linux, needs_windows 1abcdefg

13 

14runner = CliRunner() 1abcdefg

15 

16 

17def test_optional(): 1abcdefg

18 app = typer.Typer() 1abcdefg

19 

20 @app.command() 1abcdefg

21 def opt(user: str | None = None): 1abcdefg

22 if user: 1abcdefg

23 print(f"User: {user}") 1abcdefg

24 else: 

25 print("No user") 1abcdefg

26 

27 result = runner.invoke(app) 1abcdefg

28 assert result.exit_code == 0 1abcdefg

29 assert "No user" in result.output 1abcdefg

30 

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

32 assert result.exit_code == 0 1abcdefg

33 assert "User: Camila" in result.output 1abcdefg

34 

35 

36def test_union_type_optional(): 1abcdefg

37 app = typer.Typer() 1abcdefg

38 

39 @app.command() 1abcdefg

40 def opt(user: str | None = None): 1abcdefg

41 if user: 1abcdefg

42 print(f"User: {user}") 1abcdefg

43 else: 

44 print("No user") 1abcdefg

45 

46 result = runner.invoke(app) 1abcdefg

47 assert result.exit_code == 0 1abcdefg

48 assert "No user" in result.output 1abcdefg

49 

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

51 assert result.exit_code == 0 1abcdefg

52 assert "User: Camila" in result.output 1abcdefg

53 

54 

55def test_optional_tuple(): 1abcdefg

56 app = typer.Typer() 1abcdefg

57 

58 @app.command() 1abcdefg

59 def opt(number: tuple[int, int] | None = None): 1abcdefg

60 if number: 1abcdefg

61 print(f"Number: {number}") 1abcdefg

62 else: 

63 print("No number") 1abcdefg

64 

65 result = runner.invoke(app) 1abcdefg

66 assert result.exit_code == 0 1abcdefg

67 assert "No number" in result.output 1abcdefg

68 

69 result = runner.invoke(app, ["--number", "4", "2"]) 1abcdefg

70 assert result.exit_code == 0 1abcdefg

71 assert "Number: (4, 2)" in result.output 1abcdefg

72 

73 

74def test_no_type(): 1abcdefg

75 app = typer.Typer() 1abcdefg

76 

77 @app.command() 1abcdefg

78 def no_type(user): 1abcdefg

79 print(f"User: {user}") 1abcdefg

80 

81 result = runner.invoke(app, ["Camila"]) 1abcdefg

82 assert result.exit_code == 0 1abcdefg

83 assert "User: Camila" in result.output 1abcdefg

84 

85 

86class SomeEnum(Enum): 1abcdefg

87 ONE = "one" 1abcdefg

88 TWO = "two" 1abcdefg

89 THREE = "three" 1abcdefg

90 

91 

92@pytest.mark.parametrize( 1abcdefg

93 "type_annotation", 

94 [list[Path], list[SomeEnum], list[str]], 

95) 

96def test_list_parameters_convert_to_lists(type_annotation): 1abcdefg

97 # Lists containing objects that are converted by Click (i.e. not Path or Enum) 

98 # should not be inadvertently converted to tuples 

99 expected_element_type = type_annotation.__args__[0] 1abcdefg

100 app = typer.Typer() 1abcdefg

101 

102 @app.command() 1abcdefg

103 def list_conversion(container: type_annotation): 1abcdefg

104 assert isinstance(container, list) 1abcdefg

105 for element in container: 1abcdefg

106 assert isinstance(element, expected_element_type) 1abcdefg

107 

108 result = runner.invoke(app, ["one", "two", "three"]) 1abcdefg

109 assert result.exit_code == 0 1abcdefg

110 

111 

112@pytest.mark.parametrize( 1abcdefg

113 "type_annotation", 

114 [ 

115 tuple[str, str], 

116 tuple[str, Path], 

117 tuple[Path, Path], 

118 tuple[str, SomeEnum], 

119 tuple[SomeEnum, SomeEnum], 

120 ], 

121) 

122def test_tuple_parameter_elements_are_converted_recursively(type_annotation): 1abcdefg

123 # Tuple elements that aren't converted by Click (i.e. Path or Enum) 

124 # should be recursively converted by Typer 

125 expected_element_types = type_annotation.__args__ 1abcdefg

126 app = typer.Typer() 1abcdefg

127 

128 @app.command() 1abcdefg

129 def tuple_recursive_conversion(container: type_annotation): 1abcdefg

130 assert isinstance(container, tuple) 1abcdefg

131 for element, expected_type in zip( 1abcdefg

132 container, expected_element_types, strict=True 

133 ): 

134 assert isinstance(element, expected_type) 1abcdefg

135 

136 result = runner.invoke(app, ["one", "two"]) 1abcdefg

137 assert result.exit_code == 0 1abcdefg

138 

139 

140def test_tuple_wrong_arity(): 1abcdefg

141 app = typer.Typer() 1abcdefg

142 

143 @app.command() 1abcdefg

144 def tuple_arity(value: tuple[str, str] = typer.Option(...)): 1abcdefg

145 print(value) # pragma: no cover 

146 

147 result = runner.invoke(app, [], default_map={"value": ("only-one",)}) 1abcdefg

148 assert result.exit_code == 2 1abcdefg

149 assert "2 values are required, but 1 given." in result.output 1abcdefg

150 

151 

152def test_custom_parse(): 1abcdefg

153 app = typer.Typer() 1abcdefg

154 

155 @app.command() 1abcdefg

156 def custom_parser( 1abcdefg

157 hex_value: int = typer.Argument(None, parser=lambda x: int(x, 0)), 

158 ): 

159 assert hex_value == 0x56 1abcdefg

160 

161 result = runner.invoke(app, ["0x56"]) 1abcdefg

162 assert result.exit_code == 0 1abcdefg

163 

164 

165def test_custom_parse_value_error(): 1abcdefg

166 app = typer.Typer() 1abcdefg

167 

168 @app.command() 1abcdefg

169 def custom_parser( 1abcdefg

170 hex_value: int = typer.Argument(None, parser=lambda x: int(x, 0)), 

171 ): 

172 print(hex_value) # pragma: no cover 

173 

174 result = runner.invoke(app, ["not-a-hex"]) 1abcdefg

175 assert result.exit_code == 2 1abcdefg

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

177 

178 

179def test_custom_click_type(): 1abcdefg

180 class BaseNumberParamType(_click.types.ParamType): 1abcdefg

181 name = "base_integer" 1abcdefg

182 

183 def convert( 1abcdefg

184 self, 

185 value: Any, 

186 param: _click.Parameter | None, 

187 ctx: _click.Context | None, 

188 ) -> Any: 

189 return int(value, 0) 1abcdefg

190 

191 app = typer.Typer() 1abcdefg

192 

193 @app.command() 1abcdefg

194 def custom_click_type( 1abcdefg

195 hex_value: int = typer.Argument(None, click_type=BaseNumberParamType()), 

196 ): 

197 assert hex_value == 0x56 1abcdefg

198 

199 result = runner.invoke(app, ["0x56"]) 1abcdefg

200 assert result.exit_code == 0 1abcdefg

201 

202 

203def test_int_range_open_bound_clamp(): 1abcdefg

204 app = typer.Typer() 1abcdefg

205 

206 @app.command() 1abcdefg

207 def custom_click_type( 1abcdefg

208 value: int = typer.Argument( 

209 ..., 

210 click_type=_click.types.IntRange(min=1, min_open=True, clamp=True), 

211 ), 

212 ): 

213 print(value) 1abcdefg

214 

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

216 assert result.exit_code == 0 1abcdefg

217 assert "2" in result.output 1abcdefg

218 

219 

220def test_bool_convert_invalid(): 1abcdefg

221 app = typer.Typer() 1abcdefg

222 

223 @app.command() 1abcdefg

224 def main(value: bool): 1abcdefg

225 print(value) # pragma: no cover 

226 

227 result = runner.invoke(app, ["maybe"]) 1abcdefg

228 assert result.exit_code == 2 1abcdefg

229 assert "is not a valid boolean" in result.output 1abcdefg

230 assert "yes" in result.output 1abcdefg

231 assert "false" in result.output 1abcdefg

232 

233 

234@pytest.mark.parametrize( 1abcdefg

235 ("arg_enc", "system_enc", "raw_value", "expected_output"), 

236 [ 

237 pytest.param("latin-1", "utf-8", b"\xff", "ÿ"), 

238 pytest.param("ascii", "latin-1", b"\xff", "ÿ"), 

239 pytest.param("ascii", "utf-16", b"\xff", "�"), 

240 pytest.param("ascii", "ascii", b"\xff", "�"), 

241 ], 

242) 

243def test_string_param_type_converts_bytes( 1abcdefg

244 monkeypatch: pytest.MonkeyPatch, 

245 arg_enc: str, 

246 system_enc: str, 

247 raw_value: bytes, 

248 expected_output: str, 

249): 

250 app = typer.Typer() 1abcdefg

251 

252 @app.command() 1abcdefg

253 def show(name: str = typer.Option(...)): 1abcdefg

254 print(name) 1abcdefg

255 

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

257 name_param = next(param for param in command.params if param.name == "name") 1abcdefg

258 assert repr(name_param.type) == "STRING" 1abcdefg

259 

260 monkeypatch.setattr(_click.types, "_get_argv_encoding", lambda: arg_enc) 1abcdefg

261 monkeypatch.setattr(_click.types.sys, "getfilesystemencoding", lambda: system_enc) 1abcdefg

262 

263 result = runner.invoke(app, [], default_map={"name": raw_value}) 1abcdefg

264 assert result.exit_code == 0 1abcdefg

265 assert expected_output in result.output 1abcdefg

266 

267 

268@pytest.mark.parametrize("path_type", [str, bytes, Path]) 1abcdefg

269def test_path_coerced(path_type) -> None: 1abcdefg

270 # Ensure coerce_path_result works correctly 

271 app = typer.Typer() 1abcdefg

272 

273 @app.command() 1abcdefg

274 def show(path: Any = typer.Option(..., path_type=path_type)): 1abcdefg

275 print(path) 1abcdefg

276 

277 result = runner.invoke(app, ["--path", "dir/my_awesome_file.txt"]) 1abcdefg

278 assert result.exit_code == 0 1abcdefg

279 assert "my_awesome_file" in result.output 1abcdefg

280 

281 

282@pytest.mark.parametrize( 1abcdefg

283 ("create_file", "option_kwargs", "deny_mode", "expected_error"), 

284 [ 

285 (True, {"file_okay": False, "dir_okay": True}, None, "is a file"), 

286 (False, {"file_okay": True, "dir_okay": False}, None, "is a directory"), 

287 (True, {"readable": True}, os.R_OK, "is not readable"), 

288 (True, {"readable": False, "writable": True}, os.W_OK, "is not writable"), 

289 ], 

290) 

291def test_path_convert_failures( 1abcdefg

292 monkeypatch: pytest.MonkeyPatch, 

293 tmp_path: Path, 

294 create_file: bool, 

295 option_kwargs: dict[str, bool], 

296 deny_mode: int | None, 

297 expected_error: str, 

298) -> None: 

299 app = typer.Typer() 1abcdefg

300 

301 @app.command() 1abcdefg

302 def show(path: Path = typer.Option(..., **option_kwargs)): 1abcdefg

303 print(path) # pragma: no cover 

304 

305 if deny_mode is not None: 1abcdefg

306 original_access = os.access 1abcdefg

307 

308 def fake_access(path: str, mode: int) -> bool: 1abcdefg

309 if mode == deny_mode: 1abcdefg

310 return False 1abcdefg

311 return original_access(path, mode) # pragma: no cover 

312 

313 monkeypatch.setattr(models.os, "access", fake_access) 1abcdefg

314 

315 path = tmp_path / "some_path" 1abcdefg

316 if create_file: 1abcdefg

317 path.write_text("hello") 1abcdefg

318 else: 

319 path.mkdir() 1abcdefg

320 result = runner.invoke(app, ["--path", str(path)]) 1abcdefg

321 

322 assert result.exit_code != 0 1abcdefg

323 assert expected_error in result.output 1abcdefg

324 

325 

326def test_convert_type(): 1abcdefg

327 from typer._click.types import convert_type 1abcdefg

328 

329 # str 

330 assert convert_type(str) is _click.types.STRING 1abcdefg

331 assert convert_type(None) is _click.types.STRING 1abcdefg

332 assert convert_type(None, default=["a"]) is _click.types.STRING 1abcdefg

333 

334 # tuples 

335 tuple_type = convert_type((str, int)) 1abcdefg

336 assert isinstance(tuple_type, _click.types.Tuple) 1abcdefg

337 assert [type(item) for item in tuple_type.types] == [ 1abcdefg

338 type(_click.types.STRING), 

339 type(_click.types.INT), 

340 ] 

341 

342 guessed_tuple = convert_type(None, default=[(1, "x")]) 1abcdefg

343 assert isinstance(guessed_tuple, _click.types.Tuple) 1abcdefg

344 assert [type(item) for item in guessed_tuple.types] == [ 1abcdefg

345 type(_click.types.INT), 

346 type(_click.types.STRING), 

347 ] 

348 

349 # numbers 

350 assert convert_type(int) is _click.types.INT 1abcdefg

351 assert convert_type(float) is _click.types.FLOAT 1abcdefg

352 assert convert_type(bool) is _click.types.BOOL 1abcdefg

353 

354 param_type = _click.types.IntRange(min=0, max=10) 1abcdefg

355 assert convert_type(param_type) is param_type 1abcdefg

356 

357 guessed_int = convert_type(None, default=42) 1abcdefg

358 assert guessed_int is _click.types.INT 1abcdefg

359 

360 # custom type 

361 class CustomType: 1abcdefg

362 pass 1abcdefg

363 

364 guessed_unknown = convert_type(None, default=CustomType()) 1abcdefg

365 assert guessed_unknown is _click.types.STRING 1abcdefg

366 

367 func_type = convert_type(CustomType) 1abcdefg

368 assert isinstance(func_type, _click.types.FuncParamType) 1abcdefg

369 assert func_type.name == "CustomType" 1abcdefg

370 

371 

372@pytest.mark.parametrize( 1abcdefg

373 ("platform_case", "stdin_encoding", "filesystem_encoding"), 

374 [ 

375 pytest.param("windows", None, "utf-8", marks=needs_windows), 

376 pytest.param("linux", "latin-1", "utf-8", marks=needs_linux), 

377 pytest.param("linux", None, "latin-1", marks=needs_linux), 

378 ], 

379) 

380def test_argv_encoding( 1abcdefg

381 monkeypatch: pytest.MonkeyPatch, 

382 platform_case: str, 

383 stdin_encoding: str | None, 

384 filesystem_encoding: str, 

385) -> None: 

386 sys = _click._compat.sys 1abcd

387 if platform_case == "windows": 1abcd

388 import locale 1cd

389 

390 monkeypatch.setattr(locale, "getpreferredencoding", lambda: "latin-1") 1cd

391 else: 

392 

393 class FakeStdin: 1ab

394 def __init__(self, encoding: str | None) -> None: 1ab

395 self.encoding = encoding 1ab

396 

397 monkeypatch.setattr(sys, "stdin", FakeStdin(stdin_encoding)) 1ab

398 monkeypatch.setattr(sys, "getfilesystemencoding", lambda: filesystem_encoding) 1ab

399 

400 converted = _click.types.STRING.convert(b"\xff", None, None) 1abcd

401 assert converted == "ÿ" 1abcd

402 

403 

404@pytest.mark.parametrize( 1abcdefg

405 ("parameter", "expected_metavar"), 

406 [ 

407 pytest.param(Annotated[str, typer.Option(...)], "<str>"), 

408 pytest.param(Annotated[str, typer.Argument(...)], "<str>"), 

409 pytest.param(Annotated[int, typer.Option(...)], "<int>"), 

410 pytest.param(Annotated[int, typer.Argument(...)], "<int>"), 

411 pytest.param(Annotated[float, typer.Option(...)], "<float>"), 

412 pytest.param( 

413 Annotated[float, typer.Option(..., min=0.666, max=3.42)], "<float range>" 

414 ), 

415 pytest.param(Annotated[tuple[str, int], typer.Option(...)], "<str int>..."), 

416 pytest.param(Annotated[tuple[Path, str], typer.Option(...)], "<path str>..."), 

417 pytest.param(Annotated[str, typer.Option(..., resolve_path=True)], "<str>"), 

418 pytest.param(Annotated[Path, typer.Option(...)], "<path>"), 

419 pytest.param(Annotated[Path, typer.Option(..., dir_okay=False)], "<file>"), 

420 pytest.param( 

421 Annotated[Path, typer.Option(..., file_okay=False)], "<directory>" 

422 ), 

423 pytest.param(Annotated[SomeEnum, typer.Option(...)], "<one|two|three>"), 

424 pytest.param(Annotated[SomeEnum, typer.Argument()], "<one|two|three>"), 

425 pytest.param(Annotated[Literal["x", "y"], typer.Option(...)], "<x|y>"), 

426 pytest.param( 

427 Annotated[datetime, typer.Option(..., formats=["%Y-%m-%d", "%d/%m/%Y"])], 

428 "<%Y-%m-%d|%d/%m/%Y>", 

429 ), 

430 ], 

431) 

432def test_param_type_help_metavar(parameter: Any, expected_metavar: str) -> None: 1abcdefg

433 app = typer.Typer() 1abcdefg

434 

435 @app.command() 1abcdefg

436 # TODO: type-specific default 

437 def with_default(value: parameter = "my_default"): 1abcdefg

438 pass # pragma: no cover 

439 

440 @app.command() 1abcdefg

441 def without_default(value: parameter): 1abcdefg

442 pass # pragma: no cover 

443 

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

445 assert result.exit_code == 0 1abcdefg

446 assert expected_metavar in result.output 1abcdefg

447 

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

449 assert result.exit_code == 0 1abcdefg

450 assert expected_metavar in result.output 1abcdefg

451 

452 

453def test_metavar_preformatted_square_brackets() -> None: 1abcdefg

454 app = typer.Typer() 1abcdefg

455 

456 @app.command() 1abcdefg

457 def main(value: Annotated[str, typer.Argument(metavar="[VALUE]")]): 1abcdefg

458 pass # pragma: no cover 

459 

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

461 assert result.exit_code == 0 1abcdefg

462 assert "[VALUE]" in result.output 1abcdefg

463 assert "[[VALUE]]" not in result.output 1abcdefg