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
« 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
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
18from .utils import requires_completion_permission 1abcdefg
20runner = CliRunner() 1abcdefg
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
29def test_defaults_from_info(): 1abcdefg
30 # Mainly for coverage/completeness
31 value = solve_typer_info_defaults(TyperInfo()) 1abcdefg
32 assert value 1abcdefg
35def test_too_many_parsers(): 1abcdefg
36 def custom_parser(value: str) -> int: 1abcdefg
37 return int(value) # pragma: no cover
39 class CustomClickParser(_click.types.ParamType): 1abcdefg
40 name = "custom_parser" 1abcdefg
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
50 expected_error = ( 1abcdefg
51 "Multiple custom type parsers provided. "
52 "`parser` and `click_type` may not both be provided."
53 )
55 with pytest.raises(ValueError, match=expected_error): 1abcdefg
56 ParameterInfo(parser=custom_parser, click_type=CustomClickParser()) 1abcdefg
59def test_valid_parser_permutations(): 1abcdefg
60 def custom_parser(value: str) -> int: 1abcdefg
61 return int(value) # pragma: no cover
63 class CustomClickParser(_click.types.ParamType): 1abcdefg
64 name = "custom_parser" 1abcdefg
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
74 ParameterInfo() 1abcdefg
75 ParameterInfo(parser=custom_parser) 1abcdefg
76 ParameterInfo(click_type=CustomClickParser()) 1abcdefg
79@requires_completion_permission 1abcdefg
80def test_install_invalid_shell(): 1abcdefg
81 app = typer.Typer() 1abcdefg
83 @app.command() 1abcdefg
84 def main(): 1abcdefg
85 print("Hello World") 1abcdefg
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
96def test_callback_too_many_parameters(): 1abcdefg
97 app = typer.Typer() 1abcdefg
99 def name_callback(ctx, param, val1, val2): 1abcdefg
100 pass # pragma: no cover
102 @app.command() 1abcdefg
103 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg
104 pass # pragma: no cover
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 )
113def test_callback_2_untyped_parameters(): 1abcdefg
114 app = typer.Typer() 1abcdefg
116 def name_callback(ctx, value): 1abcdefg
117 print(f"info name is: {ctx.info_name}") 1abcdefg
118 print(f"value is: {value}") 1abcdefg
120 @app.command() 1abcdefg
121 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg
122 print("Hello World") 1abcdefg
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
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
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
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
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
155def test_bad_parameter_main() -> None: 1abcdefg
156 app = typer.Typer() 1abcdefg
158 @app.command() 1abcdefg
159 def main() -> None: 1abcdefg
160 raise typer.BadParameter("custom validation failed") 1abcdefg
162 result = runner.invoke(app, []) 1abcdefg
163 assert result.exit_code == 2 1abcdefg
164 assert "Invalid value: custom validation failed" in result.stderr 1abcdefg
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
180 @app.command() 1abcdefg
181 def main() -> None: 1abcdefg
182 raise typer._click.exceptions.MissingParameter(**kw) 1abcdefg
184 result = runner.invoke(app, []) 1abcdefg
185 assert result.exit_code == 2 1abcdefg
186 assert msg in result.stderr 1abcdefg
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 )
195 app = typer.Typer(rich_markup_mode=None) 1abcdefg
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
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
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
221 app = typer.Typer() 1abcdefg
223 @app.command() 1abcdefg
224 def main(mode: str = typer.Option(..., "--mode", callback=my_cb)) -> None: 1abcdefg
225 typer.echo(mode) # pragma: no cover
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
232def test_click_exception_show_default_file() -> None: 1abcdefg
233 app = typer.Typer(rich_markup_mode=None) 1abcdefg
235 @app.command() 1abcdefg
236 def main() -> None: 1abcdefg
237 raise typer._click.ClickException("custom click failure") 1abcdefg
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
245def test_no_args_is_help_show() -> None: 1abcdefg
246 app = typer.Typer(rich_markup_mode=None) 1abcdefg
248 @app.callback(invoke_without_command=True, no_args_is_help=True) 1abcdefg
249 def main() -> None: 1abcdefg
250 return None # pragma: no cover
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
258def test_callback_3_untyped_parameters(): 1abcdefg
259 app = typer.Typer() 1abcdefg
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
266 @app.command() 1abcdefg
267 def main(name: str = typer.Option(..., callback=name_callback)): 1abcdefg
268 print("Hello World") 1abcdefg
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
276def test_callback_4_list_none(): 1abcdefg
277 app = typer.Typer() 1abcdefg
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
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
293 result = runner.invoke(app, ["--name", "Sideshow", "--name", "Bob"]) 1abcdefg
294 assert "Hello SIDESHOW, BOB" in result.stdout 1abcdefg
296 result = runner.invoke(app, []) 1abcdefg
297 assert "Hello World" in result.stdout 1abcdefg
300def test_multiple_bool_flags() -> None: 1abcdefg
301 app = typer.Typer() 1abcdefg
303 @app.command() 1abcdefg
304 def main(choices: list[bool] = typer.Option([], "--accept/--reject")) -> None: 1abcdefg
305 print(choices) 1abcdefg
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
312def test_empty_list_default_generator(): 1abcdefg
313 def empty_list() -> list[str]: 1abcdefg
314 return [] 1abcdefg
316 app = typer.Typer() 1abcdefg
318 @app.command() 1abcdefg
319 def main( 1abcdefg
320 names: Annotated[list[str], typer.Option(default_factory=empty_list)],
321 ):
322 print(names) 1abcdefg
324 result = runner.invoke(app) 1abcdefg
325 assert "[]" in result.output 1abcdefg
328def test_option_envvar(): 1abcdefg
329 app = typer.Typer() 1abcdefg
331 @app.command() 1abcdefg
332 def main(user: Annotated[str, typer.Option(envvar="ME")]): 1abcdefg
333 print(f"Hello {user}") 1abcdefg
335 result = runner.invoke(app, env={"ME": "rick"}) 1abcdefg
336 assert result.exit_code == 0 1abcdefg
337 assert "Hello rick" in result.output 1abcdefg
340def test_option_envvar_list(): 1abcdefg
341 app = typer.Typer() 1abcdefg
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
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
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
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
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
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
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
425def test_autocompletion_too_many_parameters(): 1abcdefg
426 app = typer.Typer() 1abcdefg
428 def name_callback(ctx, args, incomplete, val2): 1abcdefg
429 pass # pragma: no cover
431 @app.command() 1abcdefg
432 def main(name: str = typer.Option(..., autocompletion=name_callback)): 1abcdefg
433 pass # pragma: no cover
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
440def test_forward_references(): 1abcdefg
441 app = typer.Typer() 1abcdefg
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
451 result = runner.invoke(app, ["Hello", "2", "invalid"]) 1abcdefg
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 )
462def test_context_settings_inheritance_single_command(): 1abcdefg
463 app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}) 1abcdefg
465 @app.command() 1abcdefg
466 def main(name: str): 1abcdefg
467 pass # pragma: no cover
469 result = runner.invoke(app, ["main", "-h"]) 1abcdefg
470 assert "Show this message and exit." in result.stdout 1abcdefg
473def test_options_metadata_typer_default(): 1abcdefg
474 app = typer.Typer(options_metavar="[options]") 1abcdefg
476 @app.command() 1abcdefg
477 def c1(): 1abcdefg
478 pass # pragma: no cover
480 @app.command(options_metavar="[OPTS]") 1abcdefg
481 def c2(): 1abcdefg
482 pass # pragma: no cover
484 result = runner.invoke(app, ["c1", "--help"]) 1abcdefg
485 assert "Usage: root c1 [options]" in result.stdout 1abcdefg
487 result = runner.invoke(app, ["c2", "--help"]) 1abcdefg
488 assert "Usage: root c2 [OPTS]" in result.stdout 1abcdefg