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
« 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
7import pytest 1abcdefg
8import typer 1abcdefg
9from typer import _click, models 1abcdefg
10from typer.testing import CliRunner 1abcdefg
12from tests.utils import needs_linux, needs_windows 1abcdefg
14runner = CliRunner() 1abcdefg
17def test_optional(): 1abcdefg
18 app = typer.Typer() 1abcdefg
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
27 result = runner.invoke(app) 1abcdefg
28 assert result.exit_code == 0 1abcdefg
29 assert "No user" in result.output 1abcdefg
31 result = runner.invoke(app, ["--user", "Camila"]) 1abcdefg
32 assert result.exit_code == 0 1abcdefg
33 assert "User: Camila" in result.output 1abcdefg
36def test_union_type_optional(): 1abcdefg
37 app = typer.Typer() 1abcdefg
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
46 result = runner.invoke(app) 1abcdefg
47 assert result.exit_code == 0 1abcdefg
48 assert "No user" in result.output 1abcdefg
50 result = runner.invoke(app, ["--user", "Camila"]) 1abcdefg
51 assert result.exit_code == 0 1abcdefg
52 assert "User: Camila" in result.output 1abcdefg
55def test_optional_tuple(): 1abcdefg
56 app = typer.Typer() 1abcdefg
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
65 result = runner.invoke(app) 1abcdefg
66 assert result.exit_code == 0 1abcdefg
67 assert "No number" in result.output 1abcdefg
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
74def test_no_type(): 1abcdefg
75 app = typer.Typer() 1abcdefg
77 @app.command() 1abcdefg
78 def no_type(user): 1abcdefg
79 print(f"User: {user}") 1abcdefg
81 result = runner.invoke(app, ["Camila"]) 1abcdefg
82 assert result.exit_code == 0 1abcdefg
83 assert "User: Camila" in result.output 1abcdefg
86class SomeEnum(Enum): 1abcdefg
87 ONE = "one" 1abcdefg
88 TWO = "two" 1abcdefg
89 THREE = "three" 1abcdefg
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
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
108 result = runner.invoke(app, ["one", "two", "three"]) 1abcdefg
109 assert result.exit_code == 0 1abcdefg
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
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
136 result = runner.invoke(app, ["one", "two"]) 1abcdefg
137 assert result.exit_code == 0 1abcdefg
140def test_tuple_wrong_arity(): 1abcdefg
141 app = typer.Typer() 1abcdefg
143 @app.command() 1abcdefg
144 def tuple_arity(value: tuple[str, str] = typer.Option(...)): 1abcdefg
145 print(value) # pragma: no cover
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
152def test_custom_parse(): 1abcdefg
153 app = typer.Typer() 1abcdefg
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
161 result = runner.invoke(app, ["0x56"]) 1abcdefg
162 assert result.exit_code == 0 1abcdefg
165def test_custom_parse_value_error(): 1abcdefg
166 app = typer.Typer() 1abcdefg
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
174 result = runner.invoke(app, ["not-a-hex"]) 1abcdefg
175 assert result.exit_code == 2 1abcdefg
176 assert "Invalid value" in result.output 1abcdefg
179def test_custom_click_type(): 1abcdefg
180 class BaseNumberParamType(_click.types.ParamType): 1abcdefg
181 name = "base_integer" 1abcdefg
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
191 app = typer.Typer() 1abcdefg
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
199 result = runner.invoke(app, ["0x56"]) 1abcdefg
200 assert result.exit_code == 0 1abcdefg
203def test_int_range_open_bound_clamp(): 1abcdefg
204 app = typer.Typer() 1abcdefg
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
215 result = runner.invoke(app, ["1"]) 1abcdefg
216 assert result.exit_code == 0 1abcdefg
217 assert "2" in result.output 1abcdefg
220def test_bool_convert_invalid(): 1abcdefg
221 app = typer.Typer() 1abcdefg
223 @app.command() 1abcdefg
224 def main(value: bool): 1abcdefg
225 print(value) # pragma: no cover
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
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
252 @app.command() 1abcdefg
253 def show(name: str = typer.Option(...)): 1abcdefg
254 print(name) 1abcdefg
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
260 monkeypatch.setattr(_click.types, "_get_argv_encoding", lambda: arg_enc) 1abcdefg
261 monkeypatch.setattr(_click.types.sys, "getfilesystemencoding", lambda: system_enc) 1abcdefg
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
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
273 @app.command() 1abcdefg
274 def show(path: Any = typer.Option(..., path_type=path_type)): 1abcdefg
275 print(path) 1abcdefg
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
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
301 @app.command() 1abcdefg
302 def show(path: Path = typer.Option(..., **option_kwargs)): 1abcdefg
303 print(path) # pragma: no cover
305 if deny_mode is not None: 1abcdefg
306 original_access = os.access 1abcdefg
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
313 monkeypatch.setattr(models.os, "access", fake_access) 1abcdefg
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
322 assert result.exit_code != 0 1abcdefg
323 assert expected_error in result.output 1abcdefg
326def test_convert_type(): 1abcdefg
327 from typer._click.types import convert_type 1abcdefg
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
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 ]
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 ]
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
354 param_type = _click.types.IntRange(min=0, max=10) 1abcdefg
355 assert convert_type(param_type) is param_type 1abcdefg
357 guessed_int = convert_type(None, default=42) 1abcdefg
358 assert guessed_int is _click.types.INT 1abcdefg
360 # custom type
361 class CustomType: 1abcdefg
362 pass 1abcdefg
364 guessed_unknown = convert_type(None, default=CustomType()) 1abcdefg
365 assert guessed_unknown is _click.types.STRING 1abcdefg
367 func_type = convert_type(CustomType) 1abcdefg
368 assert isinstance(func_type, _click.types.FuncParamType) 1abcdefg
369 assert func_type.name == "CustomType" 1abcdefg
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
390 monkeypatch.setattr(locale, "getpreferredencoding", lambda: "latin-1") 1cd
391 else:
393 class FakeStdin: 1ab
394 def __init__(self, encoding: str | None) -> None: 1ab
395 self.encoding = encoding 1ab
397 monkeypatch.setattr(sys, "stdin", FakeStdin(stdin_encoding)) 1ab
398 monkeypatch.setattr(sys, "getfilesystemencoding", lambda: filesystem_encoding) 1ab
400 converted = _click.types.STRING.convert(b"\xff", None, None) 1abcd
401 assert converted == "ÿ" 1abcd
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
435 @app.command() 1abcdefg
436 # TODO: type-specific default
437 def with_default(value: parameter = "my_default"): 1abcdefg
438 pass # pragma: no cover
440 @app.command() 1abcdefg
441 def without_default(value: parameter): 1abcdefg
442 pass # pragma: no cover
444 result = runner.invoke(app, ["with-default", "--help"]) 1abcdefg
445 assert result.exit_code == 0 1abcdefg
446 assert expected_metavar in result.output 1abcdefg
448 result = runner.invoke(app, ["without-default", "--help"]) 1abcdefg
449 assert result.exit_code == 0 1abcdefg
450 assert expected_metavar in result.output 1abcdefg
453def test_metavar_preformatted_square_brackets() -> None: 1abcdefg
454 app = typer.Typer() 1abcdefg
456 @app.command() 1abcdefg
457 def main(value: Annotated[str, typer.Argument(metavar="[VALUE]")]): 1abcdefg
458 pass # pragma: no cover
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